Skip to content

Commit c7cb4f7

Browse files
committed
feat(cli): update libs, use js api client to upload and handle file upload with extra mutations
1 parent 49db396 commit c7cb4f7

22 files changed

Lines changed: 364 additions & 271 deletions

.vscode/settings.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,25 @@
22
"editor.detectIndentation": false,
33
"editor.tabSize": 4,
44
"editor.formatOnSave": true,
5-
"editor.formatOnPaste": true
5+
"editor.formatOnPaste": true,
6+
"workbench.colorCustomizations": {
7+
"activityBar.activeBackground": "#355151",
8+
"activityBar.background": "#355151",
9+
"activityBar.foreground": "#e7e7e7",
10+
"activityBar.inactiveForeground": "#e7e7e799",
11+
"activityBarBadge.background": "#130d13",
12+
"activityBarBadge.foreground": "#e7e7e7",
13+
"commandCenter.border": "#e7e7e799",
14+
"sash.hoverBorder": "#355151",
15+
"statusBar.background": "#213232",
16+
"statusBar.foreground": "#e7e7e7",
17+
"statusBarItem.hoverBackground": "#355151",
18+
"statusBarItem.remoteBackground": "#213232",
19+
"statusBarItem.remoteForeground": "#e7e7e7",
20+
"titleBar.activeBackground": "#213232",
21+
"titleBar.activeForeground": "#e7e7e7",
22+
"titleBar.inactiveBackground": "#21323299",
23+
"titleBar.inactiveForeground": "#e7e7e799"
24+
},
25+
"peacock.color": "213232"
626
}

components/cli/bun.lockb

-7.11 KB
Binary file not shown.

components/cli/package.json

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@crystallize/cli",
3-
"version": "5.7.6",
3+
"version": "5.8",
44
"description": "Crystallize CLI",
55
"module": "src/index.ts",
66
"repository": "https://github.com/CrystallizeAPI/crystallize-cli",
@@ -10,40 +10,41 @@
1010
],
1111
"type": "module",
1212
"peerDependencies": {
13-
"typescript": "^5.8.3"
13+
"typescript": "^5"
1414
},
1515
"engines": {
16-
"bun": ">=1.2.19"
16+
"bun": ">=1.3"
1717
},
1818
"devDependencies": {
19-
"@commitlint/cli": "^19.8.1",
20-
"@commitlint/config-conventional": "^19.8.1",
19+
"@commitlint/cli": "^20.1.0",
20+
"@commitlint/config-conventional": "^20.0.0",
2121
"@types/bun": "latest",
2222
"@types/marked": "^6.0.0",
2323
"@types/marked-terminal": "^6.1.1",
24-
"@types/react": "^19.1.8",
24+
"@types/react": "^19.2.2",
2525
"@types/signale": "^1.4.7",
26-
"react-devtools-core": "^6.1.2"
26+
"react-devtools-core": "^7.0.0"
2727
},
2828
"dependencies": {
29-
"@crystallize/js-api-client": "^4.3.0",
30-
"@crystallize/schema": "^4.0.0",
29+
"@crystallize/js-api-client": "^5",
30+
"@crystallize/schema": "^5",
3131
"awilix": "^12.0.5",
32-
"cli-spinners": "^3.2.0",
33-
"commander": "^14.0.0",
34-
"ink": "^6.0.1",
35-
"ink-link": "^4.1.0",
32+
"cli-spinners": "^3.3.0",
33+
"commander": "^14.0.1",
34+
"ink": "^6.3.1",
35+
"ink-link": "^5.0.0",
3636
"ink-text-input": "^6.0.0",
37-
"jotai": "^2.12.5",
37+
"jotai": "^2.15.0",
3838
"json-to-graphql-query": "^2.3.0",
39-
"marked": "^15.0.12",
39+
"marked": "^16.0.4",
4040
"marked-terminal": "^7.3.0",
41-
"meow": "^13.2.0",
41+
"meow": "^14.0.0",
4242
"missive.js": "^0.5.0",
4343
"picocolors": "^1.1.1",
44-
"prettier": "^3.6.1",
45-
"react": "^19.1.0",
44+
"prettier": "^3.6.2",
45+
"react": "^19.2.0",
4646
"signale": "^1.4.0",
47-
"tar": "^7.4.3"
47+
"tar": "^7.5.1",
48+
"zod": "^4.1.12"
4849
}
4950
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { Command, Argument, Option } from 'commander';
2+
import { addInteractiveAndTokenOption } from '../../core/helpers/add-iteractive-and-token-option';
3+
import type { Logger } from '../../domain/contracts/logger';
4+
import type { CommandBus } from '../../domain/contracts/bus';
5+
import type { GetAuthenticatedUser } from '../../domain/contracts/get-authenticated-user';
6+
import type { FlySystem } from '../../domain/contracts/fly-system';
7+
import type { AsyncCreateClient } from '../../domain/contracts/credential-retriever';
8+
import pc from 'picocolors';
9+
10+
type Deps = {
11+
logger: Logger;
12+
commandBus: CommandBus;
13+
flySystem: FlySystem;
14+
getAuthenticatedUserWithInteractivityIfPossible: GetAuthenticatedUser;
15+
createCrystallizeClient: AsyncCreateClient;
16+
};
17+
export const createFileUploadCommand = ({
18+
getAuthenticatedUserWithInteractivityIfPossible,
19+
commandBus,
20+
logger,
21+
flySystem,
22+
}: Deps): Command => {
23+
const command = new Command('upload');
24+
command.description('Upload file(s) to a Tenant.');
25+
command.addArgument(new Argument('<tenant-identifier>', 'The tenant identifier to upload on.'));
26+
command.addArgument(new Argument('<file>', 'The file or the folder that contains the Files.'));
27+
command.addArgument(new Argument('[output-file]', 'An optional file that will contain the mapping path:key.'));
28+
command.addOption(new Option('-f, --force', 'Force and override the output-file if it exits.'));
29+
30+
addInteractiveAndTokenOption(command);
31+
command.action(async (tenantIdentifier: string, file: string, outputFile: string, flags) => {
32+
if (outputFile && (await flySystem.isFileExists(outputFile)) && !flags.force) {
33+
throw new Error(`File ${outputFile} already exist.`);
34+
}
35+
36+
const { credentials } = await getAuthenticatedUserWithInteractivityIfPossible({
37+
isInteractive: !flags.noInteractive,
38+
token_id: flags.token_id,
39+
token_secret: flags.token_secret,
40+
});
41+
42+
const images: string[] = [];
43+
if (await flySystem.isFileExists(file)) {
44+
images.push(file);
45+
}
46+
47+
for await (const image of flySystem.loopInDirectory(file)) {
48+
images.push(image);
49+
}
50+
51+
const intent = commandBus.createCommand('UploadBinaries', {
52+
paths: images,
53+
credentials,
54+
type: 'STATIC',
55+
tenant: {
56+
identifier: tenantIdentifier,
57+
},
58+
});
59+
const { result } = await commandBus.dispatch(intent);
60+
61+
if (!result) {
62+
logger.error(`Failed to upload files.`);
63+
return;
64+
}
65+
logger.success(`Files uploaded.`);
66+
if (!outputFile) {
67+
for (const [path, key] of Object.entries(result.keys)) {
68+
logger.complete(`\t - ${path} -> ${pc.yellowBright(key)}`);
69+
}
70+
return;
71+
}
72+
73+
await flySystem.saveFile(outputFile, JSON.stringify(result.keys, null, 2) + `\r\n`);
74+
logger.complete(`Mapping saved at ${outputFile}`);
75+
return;
76+
});
77+
78+
return command;
79+
};

components/cli/src/command/images/upload.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const createImageUploadCommand = ({
2222
}: Deps): Command => {
2323
const command = new Command('upload');
2424
command.description('Upload images(s) to a Tenant.');
25-
command.addArgument(new Argument('<tenant-identifier>', 'The tenant identifier to invite on.'));
25+
command.addArgument(new Argument('<tenant-identifier>', 'The tenant identifier to upload on.'));
2626
command.addArgument(new Argument('<file>', 'The file or the folder that contains the Images.'));
2727
command.addArgument(new Argument('[output-file]', 'An optional file that will contain the mapping path:key.'));
2828
command.addOption(new Option('-f, --force', 'Force and override the output-file if it exits.'));
@@ -48,9 +48,10 @@ export const createImageUploadCommand = ({
4848
images.push(image);
4949
}
5050

51-
const intent = commandBus.createCommand('UploadImages', {
51+
const intent = commandBus.createCommand('UploadBinaries', {
5252
paths: images,
5353
credentials,
54+
type: 'MEDIA',
5455
tenant: {
5556
identifier: tenantIdentifier,
5657
},

components/cli/src/command/mass-operation/execute-mutations.ts

Lines changed: 54 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -23,49 +23,65 @@ export const createExecuteMutationsCommand = ({
2323
command.description('Execute mutations with different set (client-side).');
2424
command.addArgument(new Argument('<tenant-identifier>', 'The tenant identifier to use.'));
2525
command.addArgument(new Argument('<file>', 'The file that contains the Mutations.'));
26-
command.addArgument(new Argument('[image-mapping-file]', 'An optional file for images mapping.'));
26+
command.addArgument(new Argument('[image-mapping-file]', 'An optional file for images mapping. Use - to skip'));
27+
command.addArgument(new Argument('[file-mapping-file]', 'An optional file for files mapping.'));
2728
addInteractiveAndTokenOption(command);
2829

29-
command.action(async (tenantIdentifier: string, inputFile: string, imageMappingFile: string, flags) => {
30-
if (!(await flySystem.isFileExists(inputFile))) {
31-
throw new Error(`File ${inputFile} was not found.`);
32-
}
33-
try {
34-
const { credentials } = await getAuthenticatedUser({
35-
isInteractive: !flags.noInteractive,
36-
token_id: flags.token_id,
37-
token_secret: flags.token_secret,
38-
});
39-
40-
let imageMapping: Record<string, string> = {};
41-
if (imageMappingFile) {
42-
imageMapping = await flySystem.loadJsonFile<Record<string, string>>(imageMappingFile);
30+
command.action(
31+
async (
32+
tenantIdentifier: string,
33+
inputFile: string,
34+
imageMappingFile: string,
35+
fileMappingFile: string,
36+
flags,
37+
) => {
38+
if (!(await flySystem.isFileExists(inputFile))) {
39+
throw new Error(`File ${inputFile} was not found.`);
4340
}
4441

45-
const intent = commandBus.createCommand('ExecuteMutations', {
46-
filePath: inputFile,
47-
tenant: {
48-
identifier: tenantIdentifier,
49-
},
50-
credentials,
51-
placeholderMap: {
52-
images: imageMapping,
53-
},
54-
});
55-
const { result } = await commandBus.dispatch(intent);
56-
if (!result) {
57-
throw new Error('Failed to execute the Mutations file.');
58-
}
59-
// console.dir({ result }, { depth: null });
60-
logger.success(`Mutations executed.`);
61-
} catch (error) {
62-
if (error instanceof ZodError) {
63-
for (const issue of error.issues) {
64-
logger.error(`[${issue.path.join('.')}]: ${issue.message}`);
42+
try {
43+
const { credentials } = await getAuthenticatedUser({
44+
isInteractive: !flags.noInteractive,
45+
token_id: flags.token_id,
46+
token_secret: flags.token_secret,
47+
});
48+
49+
let imageMapping: Record<string, string> = {};
50+
if (imageMappingFile && imageMappingFile !== '-') {
51+
imageMapping = await flySystem.loadJsonFile<Record<string, string>>(imageMappingFile);
52+
}
53+
54+
let fileMapping: Record<string, string> = {};
55+
if (fileMappingFile && fileMappingFile !== '-') {
56+
fileMapping = await flySystem.loadJsonFile<Record<string, string>>(fileMappingFile);
57+
}
58+
59+
const intent = commandBus.createCommand('ExecuteMutations', {
60+
filePath: inputFile,
61+
tenant: {
62+
identifier: tenantIdentifier,
63+
},
64+
credentials,
65+
placeholderMap: {
66+
images: imageMapping,
67+
files: fileMapping,
68+
},
69+
});
70+
const { result } = await commandBus.dispatch(intent);
71+
if (!result) {
72+
throw new Error('Failed to execute the Mutations file.');
73+
}
74+
// console.dir({ result }, { depth: null });
75+
logger.success(`Mutations executed.`);
76+
} catch (error) {
77+
if (error instanceof ZodError) {
78+
for (const issue of error.issues) {
79+
logger.error(`[${issue.path.join('.')}]: ${issue.message}`);
80+
}
6581
}
82+
throw error;
6683
}
67-
throw error;
68-
}
69-
});
84+
},
85+
);
7086
return command;
7187
};

components/cli/src/command/mass-operation/run.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import { ZodError } from 'zod';
88
import type { GetAuthenticatedUser } from '../../domain/contracts/get-authenticated-user';
99
import { addInteractiveAndTokenOption } from '../../core/helpers/add-iteractive-and-token-option';
1010

11+
type MassOperationBulkTaskError = { error: string };
12+
type MassOperationBulkTaskSuccess = { id: string; status: string };
13+
export type MassOperationBulkTaskResponse = {
14+
bulkTask: MassOperationBulkTaskError | MassOperationBulkTaskSuccess;
15+
};
16+
1117
type Deps = {
1218
logger: Logger;
1319
commandBus: CommandBus;
@@ -72,7 +78,7 @@ export const createRunMassOperationCommand = ({
7278
});
7379
const { result } = await commandBus.dispatch(intent);
7480

75-
let startedTask = result?.task;
81+
let startedTask: MassOperationBulkTaskSuccess | undefined = result?.task;
7682
if (!startedTask) {
7783
throw new Error('Task not started. Please check the logs for more information.');
7884
}
@@ -85,14 +91,18 @@ export const createRunMassOperationCommand = ({
8591
});
8692

8793
logger.info(`Now, Waiting for task ${pc.yellow(startedTask.id)} to complete...`);
88-
while (startedTask?.status !== 'complete') {
89-
logger.info(`Task status: ${pc.yellow(startedTask?.status)}`);
94+
while (startedTask.status !== 'complete') {
95+
logger.info(`Task status: ${pc.yellow(startedTask.status)}`);
9096
await new Promise((resolve) => setTimeout(resolve, 1000));
91-
const get = await crystallizeClient.nextPimApi(getMassOperationBulkTask, { id: startedTask?.id });
92-
if (get.bulkTask.error) {
93-
throw new Error(get.data.bulkTask.error);
97+
const res: MassOperationBulkTaskResponse =
98+
await crystallizeClient.nextPimApi<MassOperationBulkTaskResponse>(getMassOperationBulkTask, {
99+
id: startedTask.id,
100+
});
101+
const { bulkTask } = res;
102+
if ('error' in bulkTask) {
103+
throw new Error(bulkTask.error);
94104
}
95-
startedTask = get.bulkTask;
105+
startedTask = bulkTask;
96106
}
97107
logger.success(`Task completed successfully. Task ID: ${pc.yellow(startedTask.id)}`);
98108
} catch (error) {

components/cli/src/content/completion_file.bash

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ _crystallize_completions() {
44
local subcmd="${COMP_WORDS[2]}"
55
local subsubcmd="${COMP_WORDS[3]}"
66

7-
local commands="help changelog doc boilerplate tenant login whoami mass-operation image"
7+
local commands="help changelog doc boilerplate tenant login whoami mass-operation token image file"
88
local program_options="--version"
99
local default_options="--help"
1010
local i_login_options="--no-interactive --token_id= --token_secret="
@@ -122,6 +122,20 @@ _crystallize_completions() {
122122
;;
123123
esac
124124
;;
125+
file)
126+
if [[ "${COMP_CWORD}" -eq 2 ]]; then
127+
local options="upload ${default_options}"
128+
COMPREPLY=($(compgen -W "${options}" -- "${cur}"))
129+
return 0
130+
fi
131+
case "${subcmd}" in
132+
upload)
133+
local options="${i_login_options} --force ${default_options}"
134+
COMPREPLY=($(compgen -W "${options}" -- "${cur}"))
135+
return 0
136+
;;
137+
esac
138+
;;
125139
esac
126140
}
127141

components/cli/src/core/create-s3-uploader.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.

0 commit comments

Comments
 (0)