Skip to content

Commit

Permalink
feat(cli): revamp command structure and a tenant create
Browse files Browse the repository at this point in the history
  • Loading branch information
Plopix committed Jan 15, 2025
1 parent 45e4e78 commit 84a2dad
Show file tree
Hide file tree
Showing 23 changed files with 342 additions and 85 deletions.
10 changes: 4 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
};

Expand All @@ -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('<folder>', 'The folder to install the boilerplate into.'));
command.addArgument(new Argument('[tenant-identifier]', 'The tenant identifier to use.'));
Expand Down Expand Up @@ -69,6 +75,8 @@ export const createInstallBoilerplateCommand = ({
<InstallBoilerplateJourney
queryBus={queryBus}
commandBus={commandBus}
fetchAvailableTenantIdentifier={fetchAvailableTenantIdentifier}
createCrystallizeClient={createCrystallizeClient}
logger={logger}
store={atoms}
credentialsRetriever={credentialsRetriever}
Expand Down
16 changes: 16 additions & 0 deletions src/command/changelog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Command } from 'commander';
import { marked } from 'marked';
import { markedTerminal } from 'marked-terminal';
//@ts-expect-error - This is a workaround to import the changelog file as text. It's working with bun.
import Changelog from '../../CHANGELOG.md' with { type: 'text' };
// @ts-ignore
marked.use(markedTerminal());

export const createChangelogCommand = (): Command => {
const command = new Command('changelog');
command.description('Render the changelog.');
command.action(async () => {
console.log(marked.parse(Changelog));
});
return command;
};
Original file line number Diff line number Diff line change
@@ -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('<tenant-identifier>', 'The tenant identifier to use.'));
command.addArgument(new Argument('<file>', 'The file that contains the Operations.'));
command.option('--token_id <token_id>', 'Your access token id.');
command.option('--token_secret <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;
Expand Down Expand Up @@ -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,
Expand Down
55 changes: 55 additions & 0 deletions src/command/tenant/create.ts
Original file line number Diff line number Diff line change
@@ -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('<tenant-identifier>', 'The tenant identifier that you would like.'));
command.option('--token_id <token_id>', 'Your access token id.');
command.option('--token_secret <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;
};
File renamed without changes.
62 changes: 48 additions & 14 deletions src/content/completion_file.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 0 additions & 13 deletions src/core/create-credentials-retriever.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
};
Loading

0 comments on commit 84a2dad

Please sign in to comment.