@nocobase/cli 2.1.0-beta.35 → 2.1.0-beta.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,6 +24,18 @@ const APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS = 5_000;
24
24
  function trimValue(value) {
25
25
  return String(value ?? '').trim();
26
26
  }
27
+ function pushOptionalEnvArg(args, key, value) {
28
+ if (typeof value === 'string') {
29
+ if (!value) {
30
+ return;
31
+ }
32
+ args.push('-e', `${key}=${value}`);
33
+ return;
34
+ }
35
+ if (typeof value === 'boolean') {
36
+ args.push('-e', `${key}=${String(value)}`);
37
+ }
38
+ }
27
39
  function formatAppUrl(port) {
28
40
  const value = trimValue(port);
29
41
  return value ? `http://127.0.0.1:${value}` : undefined;
@@ -262,10 +274,13 @@ export default class AppUpgrade extends Command {
262
274
  persistDownloadVersion: requestedVersion || undefined,
263
275
  };
264
276
  }
265
- static buildLocalDownloadArgv(runtime, downloadVersion) {
277
+ static buildLocalDownloadArgv(runtime, downloadVersion, options) {
266
278
  const argv = ['-y', '--no-intro', '--source', runtime.source, '--replace'];
267
279
  const gitUrl = readEnvValue(runtime.env, 'gitUrl');
268
280
  const npmRegistry = readEnvValue(runtime.env, 'npmRegistry');
281
+ if (options?.verbose) {
282
+ argv.push('--verbose');
283
+ }
269
284
  argv.push('--version', downloadVersion, '--output-dir', runtime.projectRoot);
270
285
  if (gitUrl) {
271
286
  argv.push('--git-url', gitUrl);
@@ -284,18 +299,12 @@ export default class AppUpgrade extends Command {
284
299
  }
285
300
  return argv;
286
301
  }
287
- static buildDockerDownloadArgv(runtime, plan) {
288
- const argv = [
289
- '-y',
290
- '--no-intro',
291
- '--source',
292
- 'docker',
293
- '--replace',
294
- '--docker-registry',
295
- plan.dockerRegistry,
296
- '--version',
297
- plan.downloadVersion,
298
- ];
302
+ static buildDockerDownloadArgv(runtime, plan, options) {
303
+ const argv = ['-y', '--no-intro'];
304
+ if (options?.verbose) {
305
+ argv.push('--verbose');
306
+ }
307
+ argv.push('--source', 'docker', '--replace', '--docker-registry', plan.dockerRegistry, '--version', plan.downloadVersion);
299
308
  const dockerPlatform = normalizeDockerPlatform(runtime.env.config.dockerPlatform);
300
309
  if (dockerPlatform) {
301
310
  argv.push('--docker-platform', dockerPlatform);
@@ -329,6 +338,11 @@ export default class AppUpgrade extends Command {
329
338
  const dbDatabase = readEnvValue(runtime.env, 'dbDatabase');
330
339
  const dbUser = readEnvValue(runtime.env, 'dbUser');
331
340
  const dbPassword = readEnvValue(runtime.env, 'dbPassword');
341
+ const dbSchema = readEnvValue(runtime.env, 'dbSchema');
342
+ const dbTablePrefix = readEnvValue(runtime.env, 'dbTablePrefix');
343
+ const dbUnderscored = typeof runtime.env.config.dbUnderscored === 'boolean'
344
+ ? runtime.env.config.dbUnderscored
345
+ : undefined;
332
346
  const networkName = trimValue(runtime.dockerNetworkName || runtime.workspaceName);
333
347
  const missing = [];
334
348
  if (!networkName) {
@@ -384,7 +398,11 @@ export default class AppUpgrade extends Command {
384
398
  if (envFile) {
385
399
  args.push('--env-file', envFile);
386
400
  }
387
- args.push('-e', `APP_KEY=${appKey}`, '-e', `DB_DIALECT=${dbDialect}`, '-e', `DB_HOST=${dbHost}`, '-e', `DB_PORT=${dbPort}`, '-e', `DB_DATABASE=${dbDatabase}`, '-e', `DB_USER=${dbUser}`, '-e', `DB_PASSWORD=${dbPassword}`, '-e', `TZ=${timeZone}`, '-v', `${storagePath}:${DOCKER_APP_STORAGE_DESTINATION}`, imageRef);
401
+ args.push('-e', `APP_KEY=${appKey}`, '-e', `DB_DIALECT=${dbDialect}`, '-e', `DB_HOST=${dbHost}`, '-e', `DB_PORT=${dbPort}`, '-e', `DB_DATABASE=${dbDatabase}`, '-e', `DB_USER=${dbUser}`, '-e', `DB_PASSWORD=${dbPassword}`, '-e', `TZ=${timeZone}`, '-v', `${storagePath}:${DOCKER_APP_STORAGE_DESTINATION}`);
402
+ pushOptionalEnvArg(args, 'DB_SCHEMA', dbSchema || undefined);
403
+ pushOptionalEnvArg(args, 'DB_TABLE_PREFIX', dbTablePrefix || undefined);
404
+ pushOptionalEnvArg(args, 'DB_UNDERSCORED', dbUnderscored);
405
+ args.push(imageRef);
388
406
  return {
389
407
  containerName: runtime.containerName,
390
408
  networkName,
@@ -422,7 +440,9 @@ export default class AppUpgrade extends Command {
422
440
  if (!flags['skip-code-update'] && (runtime.source === 'npm' || runtime.source === 'git')) {
423
441
  startTask(`Refreshing NocoBase files for "${runtime.envName}" from the saved ${runtime.source} source...`);
424
442
  try {
425
- await runCommand('source:download', AppUpgrade.buildLocalDownloadArgv(runtime, downloadVersion));
443
+ await runCommand('source:download', AppUpgrade.buildLocalDownloadArgv(runtime, downloadVersion, {
444
+ verbose: flags.verbose,
445
+ }));
426
446
  succeedTask(`NocoBase files are up to date for "${runtime.envName}".`);
427
447
  }
428
448
  catch (error) {
@@ -468,7 +488,9 @@ export default class AppUpgrade extends Command {
468
488
  const plan = await AppUpgrade.buildDockerUpgradePlan(runtime, downloadVersion);
469
489
  startTask(`Refreshing the Docker image for "${runtime.envName}"...`);
470
490
  try {
471
- await runCommand('source:download', AppUpgrade.buildDockerDownloadArgv(runtime, plan));
491
+ await runCommand('source:download', AppUpgrade.buildDockerDownloadArgv(runtime, plan, {
492
+ verbose: flags.verbose,
493
+ }));
472
494
  succeedTask(`Docker image is ready for "${runtime.envName}".`);
473
495
  }
474
496
  catch (error) {
@@ -0,0 +1,147 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { Command, Flags } from '@oclif/core';
10
+ import { BACKUP_CREATE_TIMEOUT_MS, BACKUP_POLL_INTERVAL_MS, BACKUP_RUNTIME_COMMANDS, buildBackupEnvArgv, ensureBackupRuntimeCommands, resolveBackupCreateOutputPath, resolveBackupTargetEnv, runBackupCliJsonCommand, sleep, } from '../../lib/backup.js';
11
+ import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
12
+ import { announceTargetEnv, failTask, startTask, succeedTask, updateTask } from '../../lib/ui.js';
13
+ function formatBackupCreateTimeoutError(envName, name) {
14
+ return [
15
+ `Backup "${name}" did not finish in time for "${envName}".`,
16
+ `Waited ${Math.floor(BACKUP_CREATE_TIMEOUT_MS / 1000)}s but it still reports \`inProgress: true\`.`,
17
+ ].join(' ');
18
+ }
19
+ function readBackupCreateResult(response) {
20
+ const name = String(response.data?.name ?? '').trim();
21
+ if (!name) {
22
+ throw new Error('Backup creation did not return a backup name.');
23
+ }
24
+ return {
25
+ name,
26
+ inProgress: Boolean(response.data?.inProgress),
27
+ };
28
+ }
29
+ function readBackupInProgress(response, name) {
30
+ const status = response.data?.[name];
31
+ if (!status || typeof status !== 'object') {
32
+ throw new Error(`Backup status did not include "${name}".`);
33
+ }
34
+ return Boolean(status.inProgress);
35
+ }
36
+ function readDownloadOutput(response) {
37
+ const output = String(response.data?.output ?? '').trim();
38
+ return output || undefined;
39
+ }
40
+ export default class BackupCreate extends Command {
41
+ static summary = 'Create a backup through the selected env and download it locally';
42
+ static examples = [
43
+ '<%= config.bin %> <%= command.id %>',
44
+ '<%= config.bin %> <%= command.id %> --output ./fixtures/base.nbdump',
45
+ '<%= config.bin %> <%= command.id %> --env e2e --output ./fixtures',
46
+ ];
47
+ static flags = {
48
+ env: Flags.string({
49
+ char: 'e',
50
+ description: 'CLI env name to back up. Defaults to the current env when omitted',
51
+ }),
52
+ yes: Flags.boolean({
53
+ char: 'y',
54
+ description: 'Confirm using --env when it targets a different env than the current env',
55
+ default: false,
56
+ }),
57
+ output: Flags.string({
58
+ char: 'o',
59
+ description: 'Download path. When omitted, save to the current directory using the remote backup filename',
60
+ }),
61
+ 'json-output': Flags.boolean({
62
+ char: 'j',
63
+ description: 'Print the final backup result as JSON',
64
+ default: false,
65
+ }),
66
+ };
67
+ async run() {
68
+ const { flags } = await this.parse(BackupCreate);
69
+ const requestedEnv = flags.env?.trim() || undefined;
70
+ const explicitEnvSelection = Boolean(requestedEnv && hasExplicitEnvSelection(this.argv));
71
+ const jsonOutput = Boolean(flags['json-output']);
72
+ if (explicitEnvSelection) {
73
+ const confirmed = await ensureCrossEnvConfirmed({
74
+ command: this,
75
+ requestedEnv,
76
+ yes: flags.yes,
77
+ });
78
+ if (!confirmed) {
79
+ return;
80
+ }
81
+ }
82
+ const { envName, env } = await resolveBackupTargetEnv(requestedEnv);
83
+ const envArgv = buildBackupEnvArgv({
84
+ requestedEnv,
85
+ explicitEnvSelection,
86
+ yes: flags.yes,
87
+ });
88
+ if (!jsonOutput) {
89
+ announceTargetEnv(envName);
90
+ }
91
+ await ensureBackupRuntimeCommands({
92
+ envName,
93
+ env,
94
+ commandIds: [
95
+ BACKUP_RUNTIME_COMMANDS.create,
96
+ BACKUP_RUNTIME_COMMANDS.status,
97
+ BACKUP_RUNTIME_COMMANDS.download,
98
+ ],
99
+ quiet: jsonOutput,
100
+ });
101
+ try {
102
+ if (!jsonOutput) {
103
+ startTask(`Creating backup for "${envName}"...`);
104
+ }
105
+ const createResponse = await runBackupCliJsonCommand(['api', 'backup', 'create', ...envArgv], { errorName: 'nb api backup create' });
106
+ const { name, inProgress } = readBackupCreateResult(createResponse);
107
+ const outputPath = await resolveBackupCreateOutputPath(flags.output, name);
108
+ const startedAt = Date.now();
109
+ let pending = inProgress;
110
+ while (pending) {
111
+ const now = Date.now();
112
+ const elapsedMs = now - startedAt;
113
+ if (elapsedMs >= BACKUP_CREATE_TIMEOUT_MS) {
114
+ throw new Error(formatBackupCreateTimeoutError(envName, name));
115
+ }
116
+ const elapsedSeconds = Math.max(1, Math.floor(elapsedMs / 1000));
117
+ if (!jsonOutput) {
118
+ updateTask(`Waiting for backup "${name}" to finish for "${envName}"... (${elapsedSeconds}s elapsed)`);
119
+ }
120
+ await sleep(BACKUP_POLL_INTERVAL_MS);
121
+ const statusResponse = await runBackupCliJsonCommand(['api', 'backup', 'status', '--name', name, ...envArgv], { errorName: 'nb api backup status' });
122
+ pending = readBackupInProgress(statusResponse, name);
123
+ }
124
+ if (!jsonOutput) {
125
+ updateTask(`Downloading backup "${name}" for "${envName}"...`);
126
+ }
127
+ const downloadResponse = await runBackupCliJsonCommand(['api', 'backup', 'download', '--name', name, '--output', outputPath, ...envArgv], { errorName: 'nb api backup download' });
128
+ const savedPath = readDownloadOutput(downloadResponse) ?? outputPath;
129
+ const result = {
130
+ env: envName,
131
+ name,
132
+ output: savedPath,
133
+ };
134
+ if (jsonOutput) {
135
+ this.log(JSON.stringify(result, null, 2));
136
+ return;
137
+ }
138
+ succeedTask(`Backup saved to ${savedPath}`);
139
+ }
140
+ catch (error) {
141
+ if (!jsonOutput) {
142
+ failTask(`Failed to create backup for "${envName}".`);
143
+ }
144
+ throw error;
145
+ }
146
+ }
147
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { Command, loadHelpClass } from '@oclif/core';
10
+ export default class Backup extends Command {
11
+ static summary = 'Create or restore NocoBase backups';
12
+ async run() {
13
+ await this.parse(Backup);
14
+ const Help = await loadHelpClass(this.config);
15
+ await new Help(this.config, this.config.pjson.oclif.helpOptions ?? this.config.pjson.helpOptions).showHelp([
16
+ this.id ?? 'backup',
17
+ ...this.argv,
18
+ ]);
19
+ }
20
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { Command, Flags } from '@oclif/core';
10
+ import { BACKUP_RUNTIME_COMMANDS, buildBackupEnvArgv, ensureBackupRuntimeCommands, resolveBackupRestoreFilePath, resolveBackupTargetEnv, resolveBackupWaitApiBaseUrl, runBackupCliCommand, } from '../../lib/backup.js';
11
+ import { waitForAppReady } from '../../lib/app-health.js';
12
+ import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
13
+ import { confirm } from "../../lib/inquirer.js";
14
+ import { announceTargetEnv, failTask, isInteractiveTerminal, startTask, stopTask, succeedTask } from '../../lib/ui.js';
15
+ async function confirmBackupRestore(envName, filePath, force) {
16
+ if (force) {
17
+ return true;
18
+ }
19
+ if (!isInteractiveTerminal()) {
20
+ throw new Error(`\`nb backup restore\` needs confirmation. Re-run with \`--force\` to restore ${filePath} into "${envName}" in non-interactive mode.`);
21
+ }
22
+ try {
23
+ return await confirm({
24
+ message: `Restore backup "${filePath}" into "${envName}"? This will overwrite application data.`,
25
+ default: false,
26
+ });
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ export default class BackupRestore extends Command {
33
+ static summary = 'Restore a backup file into the selected env';
34
+ static examples = [
35
+ '<%= config.bin %> <%= command.id %> --file ./fixtures/base.nbdump --force',
36
+ '<%= config.bin %> <%= command.id %> --env e2e --file ./fixtures/base.nbdump --yes --force',
37
+ ];
38
+ static flags = {
39
+ env: Flags.string({
40
+ char: 'e',
41
+ description: 'CLI env name to restore into. Defaults to the current env when omitted',
42
+ }),
43
+ yes: Flags.boolean({
44
+ char: 'y',
45
+ description: 'Confirm using --env when it targets a different env than the current env',
46
+ default: false,
47
+ }),
48
+ file: Flags.string({
49
+ char: 'f',
50
+ description: 'Local backup file to upload and restore',
51
+ required: true,
52
+ }),
53
+ force: Flags.boolean({
54
+ description: 'Confirm overwriting application data during restore',
55
+ default: false,
56
+ }),
57
+ };
58
+ async run() {
59
+ const { flags } = await this.parse(BackupRestore);
60
+ const requestedEnv = flags.env?.trim() || undefined;
61
+ const explicitEnvSelection = Boolean(requestedEnv && hasExplicitEnvSelection(this.argv));
62
+ if (explicitEnvSelection) {
63
+ const confirmed = await ensureCrossEnvConfirmed({
64
+ command: this,
65
+ requestedEnv,
66
+ yes: flags.yes,
67
+ });
68
+ if (!confirmed) {
69
+ return;
70
+ }
71
+ }
72
+ const filePath = await resolveBackupRestoreFilePath(flags.file);
73
+ const { envName, env } = await resolveBackupTargetEnv(requestedEnv);
74
+ const restoreConfirmed = await confirmBackupRestore(envName, filePath, flags.force);
75
+ if (!restoreConfirmed) {
76
+ return;
77
+ }
78
+ const envArgv = buildBackupEnvArgv({
79
+ requestedEnv,
80
+ explicitEnvSelection,
81
+ yes: flags.yes,
82
+ });
83
+ announceTargetEnv(envName);
84
+ await ensureBackupRuntimeCommands({
85
+ envName,
86
+ env,
87
+ commandIds: [BACKUP_RUNTIME_COMMANDS.restoreUpload],
88
+ });
89
+ startTask(`Restoring backup for "${envName}" from ${filePath}...`);
90
+ try {
91
+ await runBackupCliCommand(['api', 'backup', 'restore-upload', '--file', filePath, '--force', ...envArgv], { errorName: 'nb api backup restore-upload' });
92
+ stopTask();
93
+ await waitForAppReady({
94
+ envName,
95
+ apiBaseUrl: resolveBackupWaitApiBaseUrl(env),
96
+ });
97
+ succeedTask(`Backup restored for "${envName}" from ${filePath}`);
98
+ }
99
+ catch (error) {
100
+ stopTask();
101
+ failTask(`Failed to restore backup for "${envName}".`);
102
+ throw error;
103
+ }
104
+ }
105
+ }
@@ -13,7 +13,7 @@ import { buildStoredEnvConfig, } from '../../lib/env-config.js';
13
13
  import { runPromptCatalog, } from '../../lib/prompt-catalog.js';
14
14
  import { applyCliLocale, CLI_LOCALE_FLAG_DESCRIPTION, CLI_LOCALE_FLAG_OPTIONS, localeText, } from '../../lib/cli-locale.js';
15
15
  import { validateApiBaseUrl } from '../../lib/prompt-validators.js';
16
- import { printStage, printSuccess, printVerbose, setVerboseMode } from '../../lib/ui.js';
16
+ import { printInfo, printStage, printSuccess, printVerbose, setVerboseMode } from '../../lib/ui.js';
17
17
  const ENV_RUNTIME_FLAG_MAP = {
18
18
  source: 'source',
19
19
  'download-version': 'downloadVersion',
@@ -33,6 +33,8 @@ const ENV_RUNTIME_FLAG_MAP = {
33
33
  'db-database': 'dbDatabase',
34
34
  'db-user': 'dbUser',
35
35
  'db-password': 'dbPassword',
36
+ 'db-schema': 'dbSchema',
37
+ 'db-table-prefix': 'dbTablePrefix',
36
38
  'root-username': 'rootUsername',
37
39
  'root-email': 'rootEmail',
38
40
  'root-password': 'rootPassword',
@@ -43,8 +45,26 @@ const ENV_BOOLEAN_RUNTIME_FLAG_MAP = {
43
45
  'dev-dependencies': 'devDependencies',
44
46
  build: 'build',
45
47
  'build-dts': 'buildDts',
48
+ 'db-underscored': 'dbUnderscored',
46
49
  };
47
50
  const envAddText = (key, values) => localeText(`commands.envAdd.${key}`, values);
51
+ const envAddAccessTokenPrompt = {
52
+ type: 'text',
53
+ message: envAddText('prompts.accessToken.message'),
54
+ required: true,
55
+ hidden: (values) => values.authType !== 'token' || values.skipAuth === true,
56
+ };
57
+ function formatDeferredAuthMessage(envName, authType) {
58
+ const normalizedAuthType = String(authType ?? '').trim();
59
+ const nextStep = `Authentication was skipped for env "${envName}". Run \`nb env auth ${envName}\` to finish setup.`;
60
+ if (normalizedAuthType === 'token') {
61
+ return `${nextStep} You will be prompted for an access token.`;
62
+ }
63
+ if (normalizedAuthType === 'oauth') {
64
+ return `${nextStep} A browser sign-in flow will be started.`;
65
+ }
66
+ return nextStep;
67
+ }
48
68
  export default class EnvAdd extends Command {
49
69
  static summary = 'Save a named NocoBase API endpoint (token or OAuth), then switch the CLI to use it';
50
70
  static examples = [
@@ -97,6 +117,10 @@ export default class EnvAdd extends Command {
97
117
  aliases: ['token'],
98
118
  description: 'API key or access token when using --auth-type token (prompted in a TTY when omitted)',
99
119
  }),
120
+ 'skip-auth': Flags.boolean({
121
+ description: 'Save the env now and finish authentication later with `nb env auth`',
122
+ default: false,
123
+ }),
100
124
  source: Flags.string({
101
125
  hidden: true,
102
126
  description: 'Application source saved with this env',
@@ -189,6 +213,19 @@ export default class EnvAdd extends Command {
189
213
  hidden: true,
190
214
  description: 'Database password saved with this env',
191
215
  }),
216
+ 'db-schema': Flags.string({
217
+ hidden: true,
218
+ description: 'Database schema saved with this env',
219
+ }),
220
+ 'db-table-prefix': Flags.string({
221
+ hidden: true,
222
+ description: 'Database table prefix saved with this env',
223
+ }),
224
+ 'db-underscored': Flags.boolean({
225
+ allowNo: true,
226
+ hidden: true,
227
+ description: 'Whether this env uses underscored database naming',
228
+ }),
192
229
  'root-username': Flags.string({
193
230
  hidden: true,
194
231
  description: 'Initial root username saved with this env',
@@ -234,13 +271,7 @@ export default class EnvAdd extends Command {
234
271
  initialValue: 'oauth',
235
272
  required: true,
236
273
  },
237
- accessToken: {
238
- type: 'text',
239
- message: envAddText('prompts.accessToken.message'),
240
- placeholder: envAddText('prompts.accessToken.placeholder'),
241
- required: true,
242
- hidden: (values) => values.authType !== 'token',
243
- },
274
+ accessToken: envAddAccessTokenPrompt,
244
275
  };
245
276
  buildPromptValues(nameArg, flags) {
246
277
  const values = {};
@@ -255,6 +286,9 @@ export default class EnvAdd extends Command {
255
286
  if (flags['auth-type']) {
256
287
  values.authType = flags['auth-type'];
257
288
  }
289
+ if (flags['skip-auth']) {
290
+ values.skipAuth = true;
291
+ }
258
292
  const token = flags['access-token'] ?? flags.token;
259
293
  if (typeof token === 'string' && token !== '') {
260
294
  values.accessToken = token;
@@ -269,6 +303,18 @@ export default class EnvAdd extends Command {
269
303
  }
270
304
  return initialValues;
271
305
  }
306
+ buildPromptCatalog(flags) {
307
+ if (!flags['skip-auth']) {
308
+ return EnvAdd.prompts;
309
+ }
310
+ return {
311
+ ...EnvAdd.prompts,
312
+ accessToken: {
313
+ ...envAddAccessTokenPrompt,
314
+ hidden: () => true,
315
+ },
316
+ };
317
+ }
272
318
  buildEnvConfig(results, flags) {
273
319
  const envConfigInput = {
274
320
  apiBaseUrl: results.apiBaseUrl,
@@ -288,12 +334,15 @@ export default class EnvAdd extends Command {
288
334
  async run() {
289
335
  const { args, flags } = await this.parse(EnvAdd);
290
336
  const parsedFlags = flags;
337
+ if (parsedFlags['skip-auth'] && (parsedFlags['access-token'] !== undefined || parsedFlags.token !== undefined)) {
338
+ this.error('--skip-auth cannot be used with --access-token or --token.');
339
+ }
291
340
  applyCliLocale(parsedFlags.locale);
292
341
  setVerboseMode(parsedFlags.verbose);
293
342
  if (!parsedFlags['no-intro']) {
294
343
  printStage('Connect to NocoBase');
295
344
  }
296
- const results = await runPromptCatalog(EnvAdd.prompts, {
345
+ const results = await runPromptCatalog(this.buildPromptCatalog(parsedFlags), {
297
346
  values: this.buildPromptValues(args.name, parsedFlags),
298
347
  initialValues: this.buildPromptInitialValues(parsedFlags),
299
348
  command: this,
@@ -303,6 +352,11 @@ export default class EnvAdd extends Command {
303
352
  printVerbose(`Saving env "${envName}" globally.`);
304
353
  await upsertEnv(envName, envConfig, { scope: resolveDefaultConfigScope() });
305
354
  await setCurrentEnv(envName, { scope: resolveDefaultConfigScope() });
355
+ if (parsedFlags['skip-auth']) {
356
+ printSuccess(`✔ Env "${envName}" was saved.`);
357
+ printInfo(formatDeferredAuthMessage(envName, results.authType));
358
+ return;
359
+ }
306
360
  if (results.authType === 'oauth') {
307
361
  await this.config.runCommand('env:auth', [envName]);
308
362
  }
@@ -7,15 +7,31 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { Args, Command, Flags } from '@oclif/core';
10
- import { getCurrentEnvName } from '../../lib/auth-store.js';
10
+ import { getCurrentEnvName, getEnv, resolveConfiguredAuthType, updateEnvConnection, } from '../../lib/auth-store.js';
11
11
  import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
12
  import { authenticateEnvWithOauth } from '../../lib/env-auth.js';
13
- import { failTask, printStage, startTask, succeedTask } from '../../lib/ui.js';
13
+ import { runPromptCatalog } from '../../lib/prompt-catalog.js';
14
+ import { failTask, printStage, startTask, stopTask, succeedTask } from '../../lib/ui.js';
15
+ import EnvAdd from "./add.js";
16
+ const envAuthPrompts = {
17
+ authType: EnvAdd.prompts.authType,
18
+ accessToken: EnvAdd.prompts.accessToken,
19
+ };
20
+ function resolveExplicitAuthType(value) {
21
+ return value === 'token' || value === 'oauth' ? value : undefined;
22
+ }
23
+ function formatMissingEnvMessage(envName) {
24
+ return [
25
+ `Env "${envName}" is not configured.`,
26
+ `Run \`nb env add ${envName} --api-base-url <url>\` first.`,
27
+ ].join('\n');
28
+ }
14
29
  export default class EnvAuth extends Command {
15
- static summary = 'Sign in to a saved NocoBase environment with OAuth';
30
+ static summary = 'Authenticate a saved NocoBase environment with a token or OAuth';
16
31
  static examples = [
17
32
  '<%= config.bin %> <%= command.id %>',
18
33
  '<%= config.bin %> <%= command.id %> prod',
34
+ '<%= config.bin %> <%= command.id %> prod --auth-type token --access-token <api-key>',
19
35
  ];
20
36
  static args = {
21
37
  name: Args.string({
@@ -30,6 +46,15 @@ export default class EnvAuth extends Command {
30
46
  deprecated: true,
31
47
  description: 'Environment name (same as the optional positional argument; for compatibility with -e/--env on other commands)',
32
48
  }),
49
+ 'auth-type': Flags.string({
50
+ char: 'a',
51
+ description: 'Authentication: token (API key) or oauth (browser login)',
52
+ options: ['token', 'oauth'],
53
+ }),
54
+ 'access-token': Flags.string({
55
+ char: 't',
56
+ description: 'API key or access token when using token authentication',
57
+ }),
33
58
  };
34
59
  async run() {
35
60
  const { args, flags } = await this.parse(EnvAuth);
@@ -38,18 +63,67 @@ export default class EnvAuth extends Command {
38
63
  if (nameArg && nameFlag && nameArg !== nameFlag) {
39
64
  this.error(`Environment name was provided both as the argument ("${nameArg}") and as --env ("${nameFlag}"). Please use only one.`);
40
65
  }
66
+ if (flags['auth-type'] === 'oauth' && flags['access-token'] !== undefined) {
67
+ this.error('--access-token cannot be used with --auth-type oauth.');
68
+ }
41
69
  const envName = nameArg || nameFlag || (await getCurrentEnvName({ scope: resolveDefaultConfigScope() }));
42
- printStage('Signing in');
43
- startTask(`Starting browser sign-in for "${envName}"...`);
70
+ const env = await getEnv(envName, { scope: resolveDefaultConfigScope() });
71
+ if (!env) {
72
+ this.error(formatMissingEnvMessage(envName));
73
+ }
74
+ const tokenFromFlags = flags['access-token'];
75
+ const tokenFlagProvided = tokenFromFlags !== undefined;
76
+ const tokenProvided = typeof tokenFromFlags === 'string' && tokenFromFlags !== '';
77
+ if (tokenFlagProvided && !tokenProvided) {
78
+ this.error('--access-token cannot be empty.');
79
+ }
80
+ const explicitAuthType = resolveExplicitAuthType(flags['auth-type']);
81
+ const savedAuthType = resolveConfiguredAuthType(env.config);
82
+ const resolvedAuthType = explicitAuthType ?? (tokenProvided ? 'token' : savedAuthType);
83
+ const prompted = (resolvedAuthType === 'oauth'
84
+ ? { authType: 'oauth' }
85
+ : resolvedAuthType === 'token' && tokenProvided
86
+ ? { authType: 'token', accessToken: tokenFromFlags }
87
+ : await runPromptCatalog(envAuthPrompts, {
88
+ values: {
89
+ ...(resolvedAuthType ? { authType: resolvedAuthType } : {}),
90
+ },
91
+ command: this,
92
+ })) ?? {};
93
+ const authType = resolveExplicitAuthType(prompted.authType ?? resolvedAuthType);
94
+ if (!authType) {
95
+ this.error('Choose an authentication type before continuing.');
96
+ }
97
+ printStage('Authenticating');
44
98
  try {
45
- await authenticateEnvWithOauth({
46
- envName,
47
- scope: resolveDefaultConfigScope(),
48
- });
49
- succeedTask(`✔ Signed in to "${envName}".`);
99
+ if (authType === 'token') {
100
+ const accessToken = String(prompted.accessToken ?? tokenFromFlags ?? '');
101
+ if (accessToken.trim() === '') {
102
+ this.error('--access-token cannot be empty.');
103
+ }
104
+ startTask(`Saving access token for "${envName}"...`);
105
+ await updateEnvConnection(envName, {
106
+ authType: 'token',
107
+ accessToken,
108
+ }, { scope: resolveDefaultConfigScope() });
109
+ stopTask();
110
+ }
111
+ else {
112
+ startTask(`Starting browser sign-in for "${envName}"...`);
113
+ await updateEnvConnection(envName, {
114
+ authType: 'oauth',
115
+ }, { scope: resolveDefaultConfigScope() });
116
+ await authenticateEnvWithOauth({
117
+ envName,
118
+ scope: resolveDefaultConfigScope(),
119
+ });
120
+ stopTask();
121
+ }
122
+ await this.config.runCommand('env:update', [envName]);
123
+ succeedTask(`✔ Authenticated "${envName}".`);
50
124
  }
51
125
  catch (error) {
52
- failTask(`Sign-in failed for "${envName}".`);
126
+ failTask(`Authentication failed for "${envName}".`);
53
127
  throw error;
54
128
  }
55
129
  }