@nocobase/cli 2.1.0-beta.34 → 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.
package/bin/run.js CHANGED
@@ -5,7 +5,7 @@ import fs from 'node:fs';
5
5
  import { createRequire } from 'node:module';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath, pathToFileURL } from 'node:url';
8
- import { normalizeSessionEnv } from './session-env.js';
8
+ import { normalizeNodeOptions, normalizeSessionEnv } from './session-env.js';
9
9
 
10
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
11
  const requireFromCli = createRequire(import.meta.url);
@@ -18,6 +18,7 @@ if (process.env.NB_CLI_USE_DIST === '1') {
18
18
  }
19
19
 
20
20
  normalizeSessionEnv();
21
+ normalizeNodeOptions();
21
22
 
22
23
  /**
23
24
  * In the monorepo, plain `node` cannot load `.ts`. Re-exec once with `--import <tsx>`
@@ -4,6 +4,7 @@ const SESSION_ENV_SOURCES = [
4
4
  'COPILOT_AGENT_SESSION_ID',
5
5
  'CLAUDE_CODE_SESSION_ID',
6
6
  ];
7
+ const PRESERVE_SYMLINKS_FLAG = '--preserve-symlinks';
7
8
 
8
9
  export function resolveNormalizedSessionId(env = process.env) {
9
10
  for (const key of SESSION_ENV_SOURCES) {
@@ -25,3 +26,14 @@ export function normalizeSessionEnv(env = process.env) {
25
26
  env.NB_SESSION_ID = sessionId;
26
27
  return sessionId;
27
28
  }
29
+
30
+ export function normalizeNodeOptions(env = process.env) {
31
+ const currentNodeOptions = String(env.NODE_OPTIONS ?? '').trim();
32
+ const flags = currentNodeOptions ? currentNodeOptions.split(/\s+/) : [];
33
+
34
+ if (!flags.includes(PRESERVE_SYMLINKS_FLAG)) {
35
+ env.NODE_OPTIONS = [...flags, PRESERVE_SYMLINKS_FLAG].join(' ');
36
+ }
37
+
38
+ return env.NODE_OPTIONS;
39
+ }
@@ -10,7 +10,7 @@ import { Command, Flags } from '@oclif/core';
10
10
  import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
11
11
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, startDockerContainer, } from '../../lib/app-runtime.js';
12
12
  import { AppHealthCheckError, formatAppUrl, isAppReady, resolveManagedAppApiBaseUrl, waitForAppReady, } from '../../lib/app-health.js';
13
- import { ensureBuiltinDbReady, ensureSavedLocalSource, recreateSavedDockerApp, } from '../../lib/app-managed-resources.js';
13
+ import { ensureBuiltinDbReady, ensureLocalPostinstall, ensureSavedLocalSource, recreateSavedDockerApp, } from '../../lib/app-managed-resources.js';
14
14
  import { run } from '../../lib/run-npm.js';
15
15
  import { announceTargetEnv, failTask, printInfo, startTask, succeedTask } from '../../lib/ui.js';
16
16
  function argvHasToken(argv, tokens) {
@@ -258,6 +258,17 @@ export default class AppStart extends Command {
258
258
  }
259
259
  return;
260
260
  }
261
+ try {
262
+ await ensureLocalPostinstall(runtime, {
263
+ verbose: flags.verbose,
264
+ onStartTask: startTask,
265
+ onSucceedTask: succeedTask,
266
+ onFailTask: failTask,
267
+ });
268
+ }
269
+ catch (error) {
270
+ this.error(error instanceof Error ? error.message : String(error));
271
+ }
261
272
  if (flags.daemon === false) {
262
273
  printInfo(`Starting NocoBase for "${runtime.envName}" in the foreground${appUrl ? ` at ${appUrl}` : ''}. Press Ctrl+C to stop.`);
263
274
  }
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
10
  import { upsertEnv } from '../../lib/auth-store.js';
11
+ import { ensureLocalPostinstall } from '../../lib/app-managed-resources.js';
11
12
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, startDockerContainer, stopDockerContainer, } from '../../lib/app-runtime.js';
12
13
  import { resolveConfiguredEnvPath } from '../../lib/cli-home.js';
13
14
  import { deriveBuiltinDbConnection } from '../../lib/builtin-db.js';
@@ -23,6 +24,18 @@ const APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS = 5_000;
23
24
  function trimValue(value) {
24
25
  return String(value ?? '').trim();
25
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
+ }
26
39
  function formatAppUrl(port) {
27
40
  const value = trimValue(port);
28
41
  return value ? `http://127.0.0.1:${value}` : undefined;
@@ -261,10 +274,13 @@ export default class AppUpgrade extends Command {
261
274
  persistDownloadVersion: requestedVersion || undefined,
262
275
  };
263
276
  }
264
- static buildLocalDownloadArgv(runtime, downloadVersion) {
277
+ static buildLocalDownloadArgv(runtime, downloadVersion, options) {
265
278
  const argv = ['-y', '--no-intro', '--source', runtime.source, '--replace'];
266
279
  const gitUrl = readEnvValue(runtime.env, 'gitUrl');
267
280
  const npmRegistry = readEnvValue(runtime.env, 'npmRegistry');
281
+ if (options?.verbose) {
282
+ argv.push('--verbose');
283
+ }
268
284
  argv.push('--version', downloadVersion, '--output-dir', runtime.projectRoot);
269
285
  if (gitUrl) {
270
286
  argv.push('--git-url', gitUrl);
@@ -283,18 +299,12 @@ export default class AppUpgrade extends Command {
283
299
  }
284
300
  return argv;
285
301
  }
286
- static buildDockerDownloadArgv(runtime, plan) {
287
- const argv = [
288
- '-y',
289
- '--no-intro',
290
- '--source',
291
- 'docker',
292
- '--replace',
293
- '--docker-registry',
294
- plan.dockerRegistry,
295
- '--version',
296
- plan.downloadVersion,
297
- ];
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);
298
308
  const dockerPlatform = normalizeDockerPlatform(runtime.env.config.dockerPlatform);
299
309
  if (dockerPlatform) {
300
310
  argv.push('--docker-platform', dockerPlatform);
@@ -328,6 +338,11 @@ export default class AppUpgrade extends Command {
328
338
  const dbDatabase = readEnvValue(runtime.env, 'dbDatabase');
329
339
  const dbUser = readEnvValue(runtime.env, 'dbUser');
330
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;
331
346
  const networkName = trimValue(runtime.dockerNetworkName || runtime.workspaceName);
332
347
  const missing = [];
333
348
  if (!networkName) {
@@ -383,7 +398,11 @@ export default class AppUpgrade extends Command {
383
398
  if (envFile) {
384
399
  args.push('--env-file', envFile);
385
400
  }
386
- 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);
387
406
  return {
388
407
  containerName: runtime.containerName,
389
408
  networkName,
@@ -421,7 +440,9 @@ export default class AppUpgrade extends Command {
421
440
  if (!flags['skip-code-update'] && (runtime.source === 'npm' || runtime.source === 'git')) {
422
441
  startTask(`Refreshing NocoBase files for "${runtime.envName}" from the saved ${runtime.source} source...`);
423
442
  try {
424
- await runCommand('source:download', AppUpgrade.buildLocalDownloadArgv(runtime, downloadVersion));
443
+ await runCommand('source:download', AppUpgrade.buildLocalDownloadArgv(runtime, downloadVersion, {
444
+ verbose: flags.verbose,
445
+ }));
425
446
  succeedTask(`NocoBase files are up to date for "${runtime.envName}".`);
426
447
  }
427
448
  catch (error) {
@@ -436,6 +457,12 @@ export default class AppUpgrade extends Command {
436
457
  else {
437
458
  printInfo(`Skipping code download for "${runtime.envName}" because this env is managed from an existing local app path.`);
438
459
  }
460
+ await ensureLocalPostinstall(runtime, {
461
+ verbose: flags.verbose,
462
+ onStartTask: startTask,
463
+ onSucceedTask: succeedTask,
464
+ onFailTask: failTask,
465
+ });
439
466
  startTask(`Starting upgraded NocoBase for "${runtime.envName}"...`);
440
467
  try {
441
468
  await runLocalNocoBaseCommand(runtime, AppUpgrade.buildLocalStartArgv(runtime), {
@@ -461,7 +488,9 @@ export default class AppUpgrade extends Command {
461
488
  const plan = await AppUpgrade.buildDockerUpgradePlan(runtime, downloadVersion);
462
489
  startTask(`Refreshing the Docker image for "${runtime.envName}"...`);
463
490
  try {
464
- await runCommand('source:download', AppUpgrade.buildDockerDownloadArgv(runtime, plan));
491
+ await runCommand('source:download', AppUpgrade.buildDockerDownloadArgv(runtime, plan, {
492
+ verbose: flags.verbose,
493
+ }));
465
494
  succeedTask(`Docker image is ready for "${runtime.envName}".`);
466
495
  }
467
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
  }