From 84a2dad05e3d2fef3df644f755fe9c3f16e9c223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morel=20Se=CC=81bastien?= Date: Tue, 14 Jan 2025 23:07:15 -0800 Subject: [PATCH] feat(cli): revamp command structure and a tenant create --- CHANGELOG.md | 10 ++- package.json | 2 +- .../install.tsx} | 24 ++++--- src/command/changelog.ts | 16 +++++ .../run.tsx} | 22 +++--- src/command/tenant/create.ts | 55 +++++++++++++++ src/command/{whoami.tsx => whoami.ts} | 0 src/content/completion_file.bash | 62 +++++++++++++---- src/core/create-credentials-retriever.ts | 13 ---- src/core/di.ts | 51 +++++++++++--- .../fetch-available-tenant-identifier.ts | 20 ++++++ .../interactive-get-user-if-possible.tsx | 69 +++++++++++++++++++ src/domain/contracts/credential-retriever.ts | 1 - .../fetch-available-tenant-identifier.ts | 3 + .../contracts/get-authenticated-user.ts | 11 +++ .../use-cases/setup-boilerplate-project.ts | 2 +- src/index.ts | 25 ++++++- .../actions/download-project.tsx | 4 +- .../actions/execute-recipes.tsx | 4 +- .../install-boilerplate/create-store.ts | 0 .../install-boilerplate.journey.tsx | 23 ++++--- .../questions/select-boilerplate.tsx | 6 +- .../questions/select-tenant.tsx | 4 +- 23 files changed, 342 insertions(+), 85 deletions(-) rename src/command/{install-boilerplate.tsx => boilerplate/install.tsx} (72%) create mode 100644 src/command/changelog.ts rename src/command/{run-mass-operation.tsx => mass-operation/run.tsx} (86%) create mode 100644 src/command/tenant/create.ts rename src/command/{whoami.tsx => whoami.ts} (100%) create mode 100644 src/core/helpers/fetch-available-tenant-identifier.ts create mode 100644 src/core/helpers/interactive-get-user-if-possible.tsx create mode 100644 src/domain/contracts/fetch-available-tenant-identifier.ts create mode 100644 src/domain/contracts/get-authenticated-user.ts rename src/{core => ui}/journeys/install-boilerplate/actions/download-project.tsx (93%) rename src/{core => ui}/journeys/install-boilerplate/actions/execute-recipes.tsx (97%) rename src/{core => ui}/journeys/install-boilerplate/create-store.ts (100%) rename src/{core => ui}/journeys/install-boilerplate/install-boilerplate.journey.tsx (77%) rename src/{core => ui}/journeys/install-boilerplate/questions/select-boilerplate.tsx (89%) rename src/{core => ui}/journeys/install-boilerplate/questions/select-tenant.tsx (98%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e65523..5704fa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,15 +7,13 @@ All notable changes to this project will be documented in this file. ### Added - `CHANGELOG.md` file is not present and can be used. +- add the `changelog` command. - script completion file is installed on program run, and the install bash is reloading the SHELL. +- create tenant command -### Change +### Changed -- - -### Fixed - -- +- revamped the command organization. (BC) ## [5.0.1] diff --git a/package.json b/package.json index f986ca9..74f6c04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@crystallize/cli", - "version": "5.0.1", + "version": "5.1.0", "description": "Crystallize CLI", "module": "src/index.ts", "repository": "https://github.com/CrystallizeAPI/crystallize-cli", diff --git a/src/command/install-boilerplate.tsx b/src/command/boilerplate/install.tsx similarity index 72% rename from src/command/install-boilerplate.tsx rename to src/command/boilerplate/install.tsx index e584f0e..c38156b 100644 --- a/src/command/install-boilerplate.tsx +++ b/src/command/boilerplate/install.tsx @@ -1,22 +1,26 @@ import { Newline, render } from 'ink'; import { Box, Text } from 'ink'; -import { InstallBoilerplateJourney } from '../core/journeys/install-boilerplate/install-boilerplate.journey'; +import { InstallBoilerplateJourney } from '../../ui/journeys/install-boilerplate/install-boilerplate.journey'; import { Argument, Command, Option } from 'commander'; -import { boilerplates } from '../content/boilerplates'; -import type { FlySystem } from '../domain/contracts/fly-system'; -import type { InstallBoilerplateStore } from '../core/journeys/install-boilerplate/create-store'; +import { boilerplates } from '../../content/boilerplates'; +import type { FlySystem } from '../../domain/contracts/fly-system'; +import type { InstallBoilerplateStore } from '../../ui/journeys/install-boilerplate/create-store'; import { Provider } from 'jotai'; -import type { CredentialRetriever } from '../domain/contracts/credential-retriever'; -import type { Logger } from '../domain/contracts/logger'; -import type { QueryBus, CommandBus } from '../domain/contracts/bus'; +import type { CredentialRetriever } from '../../domain/contracts/credential-retriever'; +import type { Logger } from '../../domain/contracts/logger'; +import type { QueryBus, CommandBus } from '../../domain/contracts/bus'; +import type { createClient } from '@crystallize/js-api-client'; +import type { FetchAvailableTenantIdentifier } from '../../domain/contracts/fetch-available-tenant-identifier'; type Deps = { logLevels: ('info' | 'debug')[]; flySystem: FlySystem; installBoilerplateCommandStore: InstallBoilerplateStore; credentialsRetriever: CredentialRetriever; + createCrystallizeClient: typeof createClient; logger: Logger; queryBus: QueryBus; + fetchAvailableTenantIdentifier: FetchAvailableTenantIdentifier; commandBus: CommandBus; }; @@ -27,9 +31,11 @@ export const createInstallBoilerplateCommand = ({ credentialsRetriever, queryBus, commandBus, + createCrystallizeClient, + fetchAvailableTenantIdentifier, logLevels, }: Deps): Command => { - const command = new Command('install-boilerplate'); + const command = new Command('install'); command.description('Install a boilerplate into a folder.'); command.addArgument(new Argument('', 'The folder to install the boilerplate into.')); command.addArgument(new Argument('[tenant-identifier]', 'The tenant identifier to use.')); @@ -69,6 +75,8 @@ export const createInstallBoilerplateCommand = ({ { + const command = new Command('changelog'); + command.description('Render the changelog.'); + command.action(async () => { + console.log(marked.parse(Changelog)); + }); + return command; +}; diff --git a/src/command/run-mass-operation.tsx b/src/command/mass-operation/run.tsx similarity index 86% rename from src/command/run-mass-operation.tsx rename to src/command/mass-operation/run.tsx index ba2d36b..087ed6d 100644 --- a/src/command/run-mass-operation.tsx +++ b/src/command/mass-operation/run.tsx @@ -1,32 +1,35 @@ import { Argument, Command } from 'commander'; -import type { Logger } from '../domain/contracts/logger'; -import type { CommandBus } from '../domain/contracts/bus'; +import type { Logger } from '../../domain/contracts/logger'; +import type { CommandBus } from '../../domain/contracts/bus'; import type { Operation, Operations } from '@crystallize/schema/mass-operation'; -import type { CredentialRetriever } from '../domain/contracts/credential-retriever'; +import type { CredentialRetriever } from '../../domain/contracts/credential-retriever'; import pc from 'picocolors'; import { ZodError } from 'zod'; import type { createClient } from '@crystallize/js-api-client'; +import type { GetAuthenticatedUser } from '../../domain/contracts/get-authenticated-user'; type Deps = { logger: Logger; commandBus: CommandBus; credentialsRetriever: CredentialRetriever; createCrystallizeClient: typeof createClient; + getAuthenticatedUserWithInteractivityIfPossible: GetAuthenticatedUser; }; export const createRunMassOperationCommand = ({ logger, commandBus, - credentialsRetriever, createCrystallizeClient, + getAuthenticatedUserWithInteractivityIfPossible: getAuthenticatedUser, }: Deps): Command => { - const command = new Command('run-mass-operation'); + const command = new Command('run'); command.description('Upload and start an Mass Operation Task in your tenant.'); command.addArgument(new Argument('', 'The tenant identifier to use.')); command.addArgument(new Argument('', 'The file that contains the Operations.')); command.option('--token_id ', 'Your access token id.'); command.option('--token_secret ', 'Your access token secret.'); command.option('--legacy-spec', 'Use legacy spec format.'); + command.option('--no-interactive', 'Disable the interactive mode.'); command.action(async (tenantIdentifier: string, file: string, flags) => { let operationsContent: Operations; @@ -59,16 +62,11 @@ export const createRunMassOperationCommand = ({ } try { - const credentials = await credentialsRetriever.getCredentials({ + const { credentials } = await getAuthenticatedUser({ + isInteractive: !flags.noInteractive, token_id: flags.token_id, token_secret: flags.token_secret, }); - const authenticatedUser = await credentialsRetriever.checkCredentials(credentials); - if (!authenticatedUser) { - throw new Error( - 'Credentials are invalid. Please run `crystallize login` to setup your credentials or provide correct credentials.', - ); - } const intent = commandBus.createCommand('RunMassOperation', { tenantIdentifier, operations: operationsContent, diff --git a/src/command/tenant/create.ts b/src/command/tenant/create.ts new file mode 100644 index 0000000..2528fc0 --- /dev/null +++ b/src/command/tenant/create.ts @@ -0,0 +1,55 @@ +import { Argument, Command } from 'commander'; +import type { Logger } from '../../domain/contracts/logger'; +import type { CommandBus } from '../../domain/contracts/bus'; +import type { GetAuthenticatedUser } from '../../domain/contracts/get-authenticated-user'; +import type { FetchAvailableTenantIdentifier } from '../../domain/contracts/fetch-available-tenant-identifier'; + +type Deps = { + logger: Logger; + commandBus: CommandBus; + getAuthenticatedUserWithInteractivityIfPossible: GetAuthenticatedUser; + fetchAvailableTenantIdentifier: FetchAvailableTenantIdentifier; +}; + +export const createCreateTenantCommand = ({ + logger, + commandBus, + fetchAvailableTenantIdentifier, + getAuthenticatedUserWithInteractivityIfPossible, +}: Deps): Command => { + const command = new Command('create'); + command.description('Create a tenant in Crystallize'); + command.addArgument(new Argument('', 'The tenant identifier that you would like.')); + command.option('--token_id ', 'Your access token id.'); + command.option('--token_secret ', 'Your access token secret.'); + command.option('--no-interactive', 'Disable the interactive mode.'); + command.option('--fail-if-not-available', 'Stop execution if the tenant identifier is not available.'); + command.action(async (tenantIdentifier: string, flags) => { + const { credentials } = await getAuthenticatedUserWithInteractivityIfPossible({ + isInteractive: !flags.noInteractive, + token_id: flags.token_id, + token_secret: flags.token_secret, + }); + const finalTenantIdentifier = await fetchAvailableTenantIdentifier(credentials, tenantIdentifier); + if (flags.failIfNotAvailable && finalTenantIdentifier !== tenantIdentifier) { + throw new Error( + `The tenant identifier ${tenantIdentifier} is not available. Suggestion: ${finalTenantIdentifier}.`, + ); + } + const intent = commandBus.createCommand('CreateCleanTenant', { + credentials, + tenant: { + identifier: finalTenantIdentifier, + }, + }); + const { result } = await commandBus.dispatch(intent); + if (!result) { + throw new Error('Failed to create tenant.'); + } + if (result.identifier !== tenantIdentifier) { + logger.note(`Please note we change the identifier for you as the requested one was taken.`); + } + logger.success(`Tenant created with identifier: ${result.identifier}`); + }); + return command; +}; diff --git a/src/command/whoami.tsx b/src/command/whoami.ts similarity index 100% rename from src/command/whoami.tsx rename to src/command/whoami.ts diff --git a/src/content/completion_file.bash b/src/content/completion_file.bash index 4063f26..fdcb7da 100644 --- a/src/content/completion_file.bash +++ b/src/content/completion_file.bash @@ -4,33 +4,67 @@ _crystallize_completions() { local subcmd="${COMP_WORDS[2]}" local subsubcmd="${COMP_WORDS[3]}" - local commands="help install-boilerplate login whoami run-mass-operation" + local commands="help changelog boilerplate login whoami mass-operation" local program_options="--version" local default_options="--help" + local i_login_options="--no-interactive --token_id= --token_secret=" COMPREPLY=() + if [[ "${COMP_CWORD}" -eq 1 ]]; then + COMPREPLY=($(compgen -W "${commands} ${program_options} ${default_options}" -- "${cur}")) + return 0 + fi + case "${cmd}" in - login) + login|whoami) local options="${default_options}" COMPREPLY=($(compgen -W "${options}" -- "${cur}")) return 0 ;; - install-boilerplate) - local options="${default_options} --bootstrap-tenant" - COMPREPLY=($(compgen -W "${options}" -- "${cur}")) - return 0 + boilerplate) + if [[ "${COMP_CWORD}" -eq 2 ]]; then + local options="install ${default_options}" + COMPREPLY=($(compgen -W "${options}" -- "${cur}")) + return 0 + fi + case "${subcmd}" in + install) + local options="--bootstrap-tenant ${default_options}" + COMPREPLY=($(compgen -W "${options}" -- "${cur}")) + return 0 + ;; + esac ;; - run-mass-operation) - local options="${default_options} --token_id= --token_secret= --legacy-spec" - COMPREPLY=($(compgen -W "${options}" -- "${cur}")) - return 0 + mass-operation) + if [[ "${COMP_CWORD}" -eq 2 ]]; then + local options="run ${default_options}" + COMPREPLY=($(compgen -W "${options}" -- "${cur}")) + return 0 + fi + case "${subcmd}" in + run) + local options="${i_login_options} --legacy-spec ${default_options}" + COMPREPLY=($(compgen -W "${options}" -- "${cur}")) + return 0 + ;; + esac ;; + tenant) + if [[ "${COMP_CWORD}" -eq 2 ]]; then + local options="create ${default_options}" + COMPREPLY=($(compgen -W "${options}" -- "${cur}")) + return 0 + fi + case "${subcmd}" in + create) + local options="${i_login_options} --fail-if-not-available ${default_options}" + COMPREPLY=($(compgen -W "${options}" -- "${cur}")) + return 0 + ;; + esac + ;; esac - - if [[ "${COMP_CWORD}" -eq 1 ]]; then - COMPREPLY=($(compgen -W "${commands} ${program_options} ${default_options}" -- "${cur}")) - fi } complete -F _crystallize_completions crystallize diff --git a/src/core/create-credentials-retriever.ts b/src/core/create-credentials-retriever.ts index a4fec09..c40cda5 100644 --- a/src/core/create-credentials-retriever.ts +++ b/src/core/create-credentials-retriever.ts @@ -78,23 +78,10 @@ export const createCredentialsRetriever = ({ await flySystem.saveFile(fallbackFile, JSON.stringify(credentials)); }; - const fetchAvailableTenantIdentifier = async (credentials: PimCredentials, identifier: string) => { - const apiClient = createCrystallizeClient({ - tenantIdentifier: '', - accessTokenId: credentials.ACCESS_TOKEN_ID, - accessTokenSecret: credentials.ACCESS_TOKEN_SECRET, - }); - const result = await apiClient.pimApi( - `query { tenant { suggestIdentifier ( desired: "${identifier}" ) { suggestion } } }`, - ); - return result.tenant?.suggestIdentifier?.suggestion || identifier; - }; - return { getCredentials, checkCredentials, removeCredentials, saveCredentials, - fetchAvailableTenantIdentifier, }; }; diff --git a/src/core/di.ts b/src/core/di.ts index f3b6740..2db15c6 100644 --- a/src/core/di.ts +++ b/src/core/di.ts @@ -2,7 +2,7 @@ import { asFunction, asValue, createContainer, InjectionMode } from 'awilix'; import type { Logger } from '../domain/contracts/logger'; import { createCommandBus, createQueryBus, type LoggerInterface } from 'missive.js'; import type { CommandBus, CommandDefinitions, QueryBus, QueryDefinitions } from '../domain/contracts/bus'; -import { createInstallBoilerplateCommand } from '../command/install-boilerplate'; +import { createInstallBoilerplateCommand } from '../command/boilerplate/install'; import type { Command } from 'commander'; import { createLogger } from './create-logger'; import type { FlySystem } from '../domain/contracts/fly-system'; @@ -13,16 +13,22 @@ import { createFetchTipsHandler } from '../domain/use-cases/fetch-tips'; import { createCredentialsRetriever } from './create-credentials-retriever'; import type { CredentialRetriever } from '../domain/contracts/credential-retriever'; import { createSetupBoilerplateProjectHandler } from '../domain/use-cases/setup-boilerplate-project'; -import { createInstallBoilerplateCommandStore } from './journeys/install-boilerplate/create-store'; +import { createInstallBoilerplateCommandStore } from '../ui/journeys/install-boilerplate/create-store'; import { createRunner } from './create-runner'; import { createLoginCommand } from '../command/login'; import { createWhoAmICommand } from '../command/whoami'; import { createS3Uploader } from './create-s3-uploader'; import os from 'os'; import { createRunMassOperationHandler } from '../domain/use-cases/run-mass-operation'; -import { createRunMassOperationCommand } from '../command/run-mass-operation'; +import { createRunMassOperationCommand } from '../command/mass-operation/run'; import type { createClient } from '@crystallize/js-api-client'; import { createCrystallizeClientBuilder } from './create-crystallize-client-builder'; +import { createChangelogCommand } from '../command/changelog'; +import { createCreateTenantCommand } from '../command/tenant/create'; +import { createFetchAvailableTenantIdentifier } from './helpers/fetch-available-tenant-identifier'; +import { createGetAuthenticatedUserWithInteractivityIfPossible } from './helpers/interactive-get-user-if-possible'; +import type { GetAuthenticatedUser } from '../domain/contracts/get-authenticated-user'; +import type { FetchAvailableTenantIdentifier } from '../domain/contracts/fetch-available-tenant-identifier'; export const buildServices = () => { const logLevels = ( @@ -41,6 +47,9 @@ export const buildServices = () => { createCrystallizeClient: typeof createClient; runner: ReturnType; s3Uploader: ReturnType; + fetchAvailableTenantIdentifier: FetchAvailableTenantIdentifier; + getAuthenticatedUserWithInteractivityIfPossible: GetAuthenticatedUser; + // use cases createCleanTenant: ReturnType; downloadBoilerplateArchive: ReturnType; @@ -54,6 +63,8 @@ export const buildServices = () => { loginCommand: Command; whoAmICommand: Command; runMassOperationCommand: Command; + changeLogCommand: Command; + createTenantCommand: Command; }>({ injectionMode: InjectionMode.PROXY, strict: true, @@ -74,6 +85,10 @@ export const buildServices = () => { createCrystallizeClient: asFunction(createCrystallizeClientBuilder).singleton(), runner: asFunction(createRunner).singleton(), s3Uploader: asFunction(createS3Uploader).singleton(), + fetchAvailableTenantIdentifier: asFunction(createFetchAvailableTenantIdentifier).singleton(), + getAuthenticatedUserWithInteractivityIfPossible: asFunction( + createGetAuthenticatedUserWithInteractivityIfPossible, + ).singleton(), // Use Cases createCleanTenant: asFunction(createCreateCleanTenantHandler).singleton(), @@ -89,6 +104,8 @@ export const buildServices = () => { loginCommand: asFunction(createLoginCommand).singleton(), whoAmICommand: asFunction(createWhoAmICommand).singleton(), runMassOperationCommand: asFunction(createRunMassOperationCommand).singleton(), + changeLogCommand: asFunction(createChangelogCommand).singleton(), + createTenantCommand: asFunction(createCreateTenantCommand).singleton(), }); container.cradle.commandBus.register('CreateCleanTenant', container.cradle.createCleanTenant); container.cradle.queryBus.register('DownloadBoilerplateArchive', container.cradle.downloadBoilerplateArchive); @@ -109,11 +126,27 @@ export const buildServices = () => { createQuery: container.cradle.queryBus.createQuery, dispatchQuery: container.cradle.queryBus.dispatch, runner: container.cradle.runner, - commands: [ - container.cradle.installBoilerplateCommand, - container.cradle.loginCommand, - container.cradle.whoAmICommand, - container.cradle.runMassOperationCommand, - ], + commands: { + root: { + description: undefined, + commands: [ + container.cradle.loginCommand, + container.cradle.whoAmICommand, + container.cradle.changeLogCommand, + ], + }, + boilerplate: { + description: 'All the commands related to Boilerplates.', + commands: [container.cradle.installBoilerplateCommand], + }, + 'mass-operation': { + description: 'All the commands related to Mass Operations.', + commands: [container.cradle.runMassOperationCommand], + }, + tenant: { + description: 'All the commands related to Tenants.', + commands: [container.cradle.createTenantCommand], + }, + }, }; }; diff --git a/src/core/helpers/fetch-available-tenant-identifier.ts b/src/core/helpers/fetch-available-tenant-identifier.ts new file mode 100644 index 0000000..49ee236 --- /dev/null +++ b/src/core/helpers/fetch-available-tenant-identifier.ts @@ -0,0 +1,20 @@ +import type { createClient } from '@crystallize/js-api-client'; +import type { PimCredentials } from '../../domain/contracts/models/credentials'; + +type Deps = { + createCrystallizeClient: typeof createClient; +}; + +export const createFetchAvailableTenantIdentifier = + ({ createCrystallizeClient }: Deps) => + async (credentials: PimCredentials, identifier: string) => { + const apiClient = createCrystallizeClient({ + tenantIdentifier: '', + accessTokenId: credentials.ACCESS_TOKEN_ID, + accessTokenSecret: credentials.ACCESS_TOKEN_SECRET, + }); + const result = await apiClient.pimApi( + `query { tenant { suggestIdentifier ( desired: "${identifier}" ) { suggestion } } }`, + ); + return result.tenant?.suggestIdentifier?.suggestion || identifier; + }; diff --git a/src/core/helpers/interactive-get-user-if-possible.tsx b/src/core/helpers/interactive-get-user-if-possible.tsx new file mode 100644 index 0000000..5174e7b --- /dev/null +++ b/src/core/helpers/interactive-get-user-if-possible.tsx @@ -0,0 +1,69 @@ +import { Box, render } from 'ink'; +import type { CredentialRetriever } from '../../domain/contracts/credential-retriever'; +import type { Logger } from '../../domain/contracts/logger'; +import type { PimAuthenticatedUser } from '../../domain/contracts/models/authenticated-user'; +import type { PimCredentials } from '../../domain/contracts/models/credentials'; +import { SetupCredentials } from '../../ui/components/setup-credentials'; + +type Deps = { + logger: Logger; + credentialsRetriever: CredentialRetriever; +}; +type Args = { + isInteractive: boolean; + token_id?: string; + token_secret?: string; +}; +export const createGetAuthenticatedUserWithInteractivityIfPossible = + ({ logger, credentialsRetriever }: Deps) => + async ({ + isInteractive, + token_id, + token_secret, + }: Args): Promise<{ + authenticatedUser: PimAuthenticatedUser; + credentials: PimCredentials; + }> => { + try { + const credentials = await credentialsRetriever.getCredentials({ token_id, token_secret }); + const authenticatedUser = await credentialsRetriever.checkCredentials(credentials); + if (!authenticatedUser) { + throw new Error('Credentials are invalid'); + } + return { authenticatedUser, credentials }; + } catch (error) { + if (!isInteractive) { + throw new Error( + 'Credentials are invalid. Please run `crystallize login` to setup your credentials or provide correct credentials.', + ); + } + let authenticatedUser: PimAuthenticatedUser | undefined; + let credentials: PimCredentials | undefined; + + logger.warn('Credentials are invalid. Starting interactive mode...'); + const { waitUntilExit, unmount } = render( + + + { + authenticatedUser = user; + credentials = creds; + unmount(); + }} + /> + + , + { + exitOnCtrlC: true, + }, + ); + await waitUntilExit(); + if (!authenticatedUser || !credentials) { + throw new Error( + 'Credentials are invalid. Please run `crystallize login` to setup your credentials or provide correct credentials.', + ); + } + return { authenticatedUser, credentials }; + } + }; diff --git a/src/domain/contracts/credential-retriever.ts b/src/domain/contracts/credential-retriever.ts index 2251d85..b0a03c9 100644 --- a/src/domain/contracts/credential-retriever.ts +++ b/src/domain/contracts/credential-retriever.ts @@ -11,5 +11,4 @@ export type CredentialRetriever = { checkCredentials: (credentials: PimCredentials) => Promise; removeCredentials: () => Promise; saveCredentials: (credentials: PimCredentials) => Promise; - fetchAvailableTenantIdentifier: (credentials: PimCredentials, identifier: string) => Promise; }; diff --git a/src/domain/contracts/fetch-available-tenant-identifier.ts b/src/domain/contracts/fetch-available-tenant-identifier.ts new file mode 100644 index 0000000..0029d04 --- /dev/null +++ b/src/domain/contracts/fetch-available-tenant-identifier.ts @@ -0,0 +1,3 @@ +import type { PimCredentials } from './models/credentials'; + +export type FetchAvailableTenantIdentifier = (credentials: PimCredentials, identifier: string) => Promise; diff --git a/src/domain/contracts/get-authenticated-user.ts b/src/domain/contracts/get-authenticated-user.ts new file mode 100644 index 0000000..4713ad8 --- /dev/null +++ b/src/domain/contracts/get-authenticated-user.ts @@ -0,0 +1,11 @@ +import type { PimAuthenticatedUser } from './models/authenticated-user'; +import type { PimCredentials } from './models/credentials'; + +export type GetAuthenticatedUser = (options: { + isInteractive: boolean; + token_id?: string; + token_secret?: string; +}) => Promise<{ + authenticatedUser: PimAuthenticatedUser; + credentials: PimCredentials; +}>; diff --git a/src/domain/use-cases/setup-boilerplate-project.ts b/src/domain/use-cases/setup-boilerplate-project.ts index 849b0ca..7e78100 100644 --- a/src/domain/use-cases/setup-boilerplate-project.ts +++ b/src/domain/use-cases/setup-boilerplate-project.ts @@ -4,7 +4,7 @@ import type { Tenant } from '../contracts/models/tenant'; import type { PimCredentials } from '../contracts/models/credentials'; import type { Logger } from '../contracts/logger'; import type { Runner } from '../../core/create-runner'; -import type { InstallBoilerplateStore } from '../../core/journeys/install-boilerplate/create-store'; +import type { InstallBoilerplateStore } from '../../ui/journeys/install-boilerplate/create-store'; import type { CredentialRetriever } from '../contracts/credential-retriever'; type Deps = { diff --git a/src/index.ts b/src/index.ts index c97c93c..8292585 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ #!/usr/bin/env bun -import { Command, Option } from 'commander'; +import { Argument, Command, Option } from 'commander'; import packageJson from '../package.json'; import pc from 'picocolors'; import { buildServices } from './core/di'; @@ -49,11 +49,28 @@ program.action(async () => { program.help(); }); -commands.forEach((command) => { +commands.root.commands.forEach((command) => { command.configureHelp(helpStyling); program.addCommand(command); }); +Object.keys(commands).forEach((key) => { + if (key === 'root') return; + const group = new Command(key); + const description = commands[key as keyof typeof commands].description; + if (description) { + group.description(description); + } + commands[key as keyof typeof commands].commands.forEach((command) => { + command.configureHelp(helpStyling); + group.addCommand(command); + group.addArgument(new Argument(`[${command.name()}]`, command.description())); + }); + + group.configureHelp(helpStyling); + program.addCommand(group); +}); + const logMemory = () => { const used = process.memoryUsage(); logger.debug( @@ -69,6 +86,10 @@ try { logger.flush(); if (exception instanceof Error) { logger.fatal(`[${pc.bold(exception.name)}] ${exception.message} `); + } else if (typeof exception === 'string') { + logger.fatal(exception); + } else if (exception instanceof Object && 'message' in exception) { + logger.fatal(exception.message); } else { logger.fatal(`Unknown error.`); } diff --git a/src/core/journeys/install-boilerplate/actions/download-project.tsx b/src/ui/journeys/install-boilerplate/actions/download-project.tsx similarity index 93% rename from src/core/journeys/install-boilerplate/actions/download-project.tsx rename to src/ui/journeys/install-boilerplate/actions/download-project.tsx index 66441fe..ed2752f 100644 --- a/src/core/journeys/install-boilerplate/actions/download-project.tsx +++ b/src/ui/journeys/install-boilerplate/actions/download-project.tsx @@ -1,7 +1,7 @@ import { Box, Text } from 'ink'; import { useEffect } from 'react'; -import { colors } from '../../../styles'; -import { Spinner } from '../../../../ui/components/spinner'; +import { colors } from '../../../../core/styles'; +import { Spinner } from '../../../components/spinner'; import type { InstallBoilerplateStore } from '../create-store'; import { useAtom } from 'jotai'; import type { QueryBus } from '../../../../domain/contracts/bus'; diff --git a/src/core/journeys/install-boilerplate/actions/execute-recipes.tsx b/src/ui/journeys/install-boilerplate/actions/execute-recipes.tsx similarity index 97% rename from src/core/journeys/install-boilerplate/actions/execute-recipes.tsx rename to src/ui/journeys/install-boilerplate/actions/execute-recipes.tsx index 6f800de..6e33698 100644 --- a/src/core/journeys/install-boilerplate/actions/execute-recipes.tsx +++ b/src/ui/journeys/install-boilerplate/actions/execute-recipes.tsx @@ -1,7 +1,7 @@ import { Box, Text } from 'ink'; import { useEffect, useState } from 'react'; -import { colors } from '../../../styles'; -import { Spinner } from '../../../../ui/components/spinner'; +import { colors } from '../../../../core/styles'; +import { Spinner } from '../../../components/spinner'; import type { InstallBoilerplateStore } from '../create-store'; import { useAtom } from 'jotai'; import type { CommandBus } from '../../../../domain/contracts/bus'; diff --git a/src/core/journeys/install-boilerplate/create-store.ts b/src/ui/journeys/install-boilerplate/create-store.ts similarity index 100% rename from src/core/journeys/install-boilerplate/create-store.ts rename to src/ui/journeys/install-boilerplate/create-store.ts diff --git a/src/core/journeys/install-boilerplate/install-boilerplate.journey.tsx b/src/ui/journeys/install-boilerplate/install-boilerplate.journey.tsx similarity index 77% rename from src/core/journeys/install-boilerplate/install-boilerplate.journey.tsx rename to src/ui/journeys/install-boilerplate/install-boilerplate.journey.tsx index 0314575..45f57e7 100644 --- a/src/core/journeys/install-boilerplate/install-boilerplate.journey.tsx +++ b/src/ui/journeys/install-boilerplate/install-boilerplate.journey.tsx @@ -3,25 +3,29 @@ import { SelectBoilerplate } from './questions/select-boilerplate'; import { SelectTenant } from './questions/select-tenant'; import { ExecuteRecipes } from './actions/execute-recipes'; import { Text } from 'ink'; -import { colors } from '../../styles'; -import { SetupCredentials } from '../../../ui/components/setup-credentials'; +import { colors } from '../../../core/styles'; +import { SetupCredentials } from '../../components/setup-credentials'; import type { PimAuthenticatedUser } from '../../../domain/contracts/models/authenticated-user'; import type { PimCredentials } from '../../../domain/contracts/models/credentials'; -import { Messages } from '../../../ui/components/messages'; -import { Tips } from '../../../ui/components/tips'; -import { Success } from '../../../ui/components/success'; +import { Messages } from '../../components/messages'; +import { Tips } from '../../components/tips'; +import { Success } from '../../components/success'; import type { InstallBoilerplateStore } from './create-store'; import { useAtom } from 'jotai'; import type { CredentialRetriever } from '../../../domain/contracts/credential-retriever'; import type { CommandBus, QueryBus } from '../../../domain/contracts/bus'; import type { Logger } from '../../../domain/contracts/logger'; +import type { createClient } from '@crystallize/js-api-client'; +import type { FetchAvailableTenantIdentifier } from '../../../domain/contracts/fetch-available-tenant-identifier'; type InstallBoilerplateJourneyProps = { store: InstallBoilerplateStore['atoms']; credentialsRetriever: CredentialRetriever; queryBus: QueryBus; logger: Logger; + createCrystallizeClient: typeof createClient; commandBus: CommandBus; + fetchAvailableTenantIdentifier: FetchAvailableTenantIdentifier; }; export const InstallBoilerplateJourney = ({ store, @@ -29,6 +33,7 @@ export const InstallBoilerplateJourney = ({ queryBus, commandBus, logger, + fetchAvailableTenantIdentifier, }: InstallBoilerplateJourneyProps) => { const [state] = useAtom(store.stateAtom); const [, changeTenant] = useAtom(store.changeTenantAtom); @@ -51,14 +56,14 @@ export const InstallBoilerplateJourney = ({ { - credentialsRetriever - .fetchAvailableTenantIdentifier(credentials, state.tenant!.identifier) - .then((newIdentifier: string) => { + fetchAvailableTenantIdentifier(credentials, state.tenant!.identifier).then( + (newIdentifier: string) => { changeTenant({ identifier: newIdentifier, }); setCredentials(credentials); - }); + }, + ); }} /> )} diff --git a/src/core/journeys/install-boilerplate/questions/select-boilerplate.tsx b/src/ui/journeys/install-boilerplate/questions/select-boilerplate.tsx similarity index 89% rename from src/core/journeys/install-boilerplate/questions/select-boilerplate.tsx rename to src/ui/journeys/install-boilerplate/questions/select-boilerplate.tsx index 70fff2b..81a5200 100644 --- a/src/core/journeys/install-boilerplate/questions/select-boilerplate.tsx +++ b/src/ui/journeys/install-boilerplate/questions/select-boilerplate.tsx @@ -1,9 +1,9 @@ import { Text } from 'ink'; import { boilerplates } from '../../../../content/boilerplates'; import type { Boilerplate } from '../../../../domain/contracts/models/boilerplate'; -import { BoilerplateChoice } from '../../../../ui/components/boilerplate-choice'; -import { colors } from '../../../styles'; -import { Select } from '../../../../ui/components/select'; +import { BoilerplateChoice } from '../../../components/boilerplate-choice'; +import { colors } from '../../../../core/styles'; +import { Select } from '../../../components/select'; import type { InstallBoilerplateStore } from '../create-store'; import { useAtom } from 'jotai'; diff --git a/src/core/journeys/install-boilerplate/questions/select-tenant.tsx b/src/ui/journeys/install-boilerplate/questions/select-tenant.tsx similarity index 98% rename from src/core/journeys/install-boilerplate/questions/select-tenant.tsx rename to src/ui/journeys/install-boilerplate/questions/select-tenant.tsx index 33199e5..2b92735 100644 --- a/src/core/journeys/install-boilerplate/questions/select-tenant.tsx +++ b/src/ui/journeys/install-boilerplate/questions/select-tenant.tsx @@ -2,9 +2,9 @@ import { Box, Newline, Text } from 'ink'; import Link from 'ink-link'; import { UncontrolledTextInput } from 'ink-text-input'; import { useState } from 'react'; -import { Select } from '../../../../ui/components/select'; +import { Select } from '../../../components/select'; import type { Tenant } from '../../../../domain/contracts/models/tenant'; -import { colors } from '../../../styles'; +import { colors } from '../../../../core/styles'; import type { InstallBoilerplateStore } from '../create-store'; import { useAtom } from 'jotai';