@nocobase/cli 2.1.0-beta.33 → 2.1.0-beta.34

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.
Files changed (52) hide show
  1. package/dist/commands/app/down.js +10 -13
  2. package/dist/commands/app/logs.js +0 -1
  3. package/dist/commands/app/restart.js +63 -2
  4. package/dist/commands/app/start.js +41 -17
  5. package/dist/commands/app/stop.js +0 -1
  6. package/dist/commands/app/upgrade.js +9 -4
  7. package/dist/commands/env/add.js +3 -4
  8. package/dist/commands/env/auth.js +3 -2
  9. package/dist/commands/env/remove.js +38 -13
  10. package/dist/commands/env/update.js +9 -2
  11. package/dist/commands/examples/prompts-stages.js +4 -4
  12. package/dist/commands/examples/prompts-test.js +4 -4
  13. package/dist/commands/init.js +38 -31
  14. package/dist/commands/install.js +100 -63
  15. package/dist/commands/license/activate.js +66 -64
  16. package/dist/commands/license/id.js +0 -1
  17. package/dist/commands/license/plugins/clean.js +0 -1
  18. package/dist/commands/license/plugins/list.js +0 -1
  19. package/dist/commands/license/plugins/sync.js +0 -1
  20. package/dist/commands/license/shared.js +3 -3
  21. package/dist/commands/license/status.js +0 -1
  22. package/dist/commands/plugin/disable.js +0 -1
  23. package/dist/commands/plugin/enable.js +0 -1
  24. package/dist/commands/plugin/list.js +0 -1
  25. package/dist/commands/self/update.js +12 -3
  26. package/dist/commands/skills/install.js +12 -3
  27. package/dist/commands/skills/remove.js +12 -3
  28. package/dist/commands/skills/update.js +12 -3
  29. package/dist/commands/source/dev.js +0 -1
  30. package/dist/commands/source/download.js +29 -17
  31. package/dist/lib/app-managed-resources.js +8 -2
  32. package/dist/lib/bootstrap.js +12 -3
  33. package/dist/lib/db-connection-check.js +3 -23
  34. package/dist/lib/docker-env-file.js +52 -0
  35. package/dist/lib/env-auth.js +4 -3
  36. package/dist/lib/env-config.js +1 -0
  37. package/dist/lib/env-guard.js +8 -7
  38. package/dist/lib/generated-command.js +0 -1
  39. package/dist/lib/inquirer-theme.js +17 -0
  40. package/dist/lib/inquirer.js +244 -0
  41. package/dist/lib/object-utils.js +76 -0
  42. package/dist/lib/prompt-catalog-core.js +185 -0
  43. package/dist/lib/prompt-catalog-terminal.js +375 -0
  44. package/dist/lib/prompt-catalog.js +2 -573
  45. package/dist/lib/prompt-validators.js +56 -1
  46. package/dist/lib/resource-command.js +0 -1
  47. package/dist/lib/skills-manager.js +75 -11
  48. package/dist/lib/startup-update.js +12 -8
  49. package/dist/lib/ui.js +28 -51
  50. package/dist/locale/en-US.json +8 -3
  51. package/dist/locale/zh-CN.json +8 -3
  52. package/package.json +7 -5
@@ -7,7 +7,6 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
- import * as p from '@clack/prompts';
11
10
  import pc from 'picocolors';
12
11
  import { existsSync } from 'node:fs';
13
12
  import path from 'node:path';
@@ -19,10 +18,11 @@ import { resolveDefaultConfigScope } from '../lib/cli-home.js';
19
18
  import { runPromptCatalogWebUI, } from "../lib/prompt-web-ui.js";
20
19
  import { validateApiBaseUrl, validateEnvKey } from "../lib/prompt-validators.js";
21
20
  import { inspectSkillsStatus, installNocoBaseSkills, updateNocoBaseSkills, } from '../lib/skills-manager.js';
21
+ import { omitKeys, pickKeys } from "../lib/object-utils.js";
22
+ import { printInfo, printStage, printVerbose, printWarning } from '../lib/ui.js';
22
23
  import Download from "./download.js";
23
24
  import EnvAdd from "./env/add.js";
24
25
  import Install, { defaultDbPortForDialect } from "./install.js";
25
- import _ from 'lodash';
26
26
  const DEFAULT_INIT_API_BASE_URL = 'http://localhost:13000/api';
27
27
  const DEFAULT_INIT_APP_NAME = 'local';
28
28
  const DOWNLOAD_OUTPUT_DIR_PROMPT = Download.prompts.outputDir;
@@ -129,15 +129,18 @@ function shellQuoteArg(value) {
129
129
  : `'${value.replace(/'/g, `'\\''`)}'`;
130
130
  }
131
131
  function initTitle() {
132
- return translateCli('commands.init.messages.title');
132
+ return 'Set up NocoBase';
133
+ }
134
+ function logInitStage(title) {
135
+ printStage(title);
133
136
  }
134
137
  function logInitUiReady(command, url) {
135
- p.log.step(translateCli('commands.init.messages.uiReady'));
136
- p.log.info(translateCli('commands.init.messages.uiReadyHelp'));
138
+ command.log(translateCli('commands.init.messages.uiReady'));
139
+ command.log(translateCli('commands.init.messages.uiReadyHelp'));
137
140
  command.log(`URL: ${url}`);
138
141
  }
139
142
  function logInitUiBrowserOpenFallback() {
140
- p.log.warn(translateCli('commands.init.messages.uiOpenBrowserFallback'));
143
+ printWarning(translateCli('commands.init.messages.uiOpenBrowserFallback'));
141
144
  }
142
145
  function formatBrowserOpenError(error) {
143
146
  if (error instanceof Error) {
@@ -313,8 +316,8 @@ Prompt modes:
313
316
  min: 0,
314
317
  max: 65535,
315
318
  }),
316
- ..._.pick(EnvAdd.flags, INIT_ENV_ADD_FLAG_NAMES),
317
- ..._.omit(Install.flags, ['yes', 'env']),
319
+ ...pickKeys(EnvAdd.flags, INIT_ENV_ADD_FLAG_NAMES),
320
+ ...omitKeys(Install.flags, ['yes', 'env']),
318
321
  };
319
322
  async run() {
320
323
  const parsedResult = await this.parse(Init);
@@ -337,10 +340,10 @@ Prompt modes:
337
340
  if (normalizedFlags.resume) {
338
341
  const envName = String(normalizedFlags.env ?? '').trim();
339
342
  if (!envName) {
340
- p.log.error(formatResumeEnvRequiredMessage());
343
+ this.error(formatResumeEnvRequiredMessage());
341
344
  this.exit(1);
342
345
  }
343
- p.intro(initTitle());
346
+ logInitStage(initTitle());
344
347
  await this.syncNocoBaseSkills({
345
348
  skip: Boolean(normalizedFlags['skip-skills']),
346
349
  });
@@ -351,7 +354,6 @@ Prompt modes:
351
354
  const message = error instanceof Error ? error.message : String(error);
352
355
  this.error(message);
353
356
  }
354
- p.outro('Workspace init finished.');
355
357
  return;
356
358
  }
357
359
  const interactive = Boolean(stdinStream.isTTY && stdoutStream.isTTY);
@@ -377,21 +379,21 @@ Prompt modes:
377
379
  }
378
380
  if (normalizedFlags.yes && !String(presetValues.appName ?? '').trim()) {
379
381
  const formatted = formatSkippedAppNameRequiredMessage();
380
- p.log.error(highlightInitValidationMessage(formatted));
382
+ this.error(highlightInitValidationMessage(formatted));
381
383
  this.exit(1);
382
384
  }
383
385
  const appName = String(presetValues.appName ?? '').trim();
384
386
  if (useBrowserUi) {
385
- p.intro(initTitle());
386
- p.log.info(translateCli('commands.init.messages.uiOpening'));
387
+ logInitStage(initTitle());
388
+ this.log(translateCli('commands.init.messages.uiOpening'));
387
389
  }
388
390
  else {
389
- p.intro(initTitle());
391
+ logInitStage(initTitle());
390
392
  if (normalizedFlags.yes) {
391
- p.log.info(`Prompts skipped (--yes). NocoBase will be installed for env "${appName}" using the provided flags and safe defaults.`);
393
+ printInfo(`Non-interactive setup for env "${appName}" (--yes).`);
392
394
  }
393
395
  else if (!interactive) {
394
- p.log.warn('No interactive terminal detected. NocoBase will be installed using the provided flags and safe defaults.');
396
+ printWarning('No interactive terminal detected. NocoBase will be installed using the provided flags and safe defaults.');
395
397
  }
396
398
  }
397
399
  const dynamicInitialValues = await Init.buildDynamicInitialValuesForInstall(normalizedFlags, presetValues);
@@ -412,7 +414,7 @@ Prompt modes:
412
414
  },
413
415
  onOpenBrowserError: (_url, err) => {
414
416
  logInitUiBrowserOpenFallback();
415
- p.log.info(`Browser open error: ${formatBrowserOpenError(err)}`);
417
+ this.log(`Browser open error: ${formatBrowserOpenError(err)}`);
416
418
  },
417
419
  });
418
420
  }
@@ -422,12 +424,11 @@ Prompt modes:
422
424
  yes: normalizedFlags.yes || useBrowserUi || !interactive,
423
425
  hooks: {
424
426
  onCancel: () => {
425
- p.cancel('Init cancelled.');
426
427
  this.exit(0);
427
428
  },
428
429
  onMissingNonInteractive: (message) => {
429
430
  const formatted = formatInitValidationMessage(message);
430
- p.log.error(highlightInitValidationMessage(formatted));
431
+ this.error(highlightInitValidationMessage(formatted));
431
432
  this.exit(1);
432
433
  },
433
434
  },
@@ -438,7 +439,7 @@ Prompt modes:
438
439
  ? await getEnv(String(results.appName ?? '').trim(), { scope: resolveDefaultConfigScope() })
439
440
  : undefined;
440
441
  if (existingEnv && Boolean(normalizedFlags.force)) {
441
- p.log.warn(`Reconfiguring existing env ${pc.cyan(pc.bold(`"${existingEnv.name}"`))} from the global config because ${pc.bold('--force')} was set. The env config will be updated before install starts, then refreshed again after install succeeds.`);
442
+ printWarning(`Reconfiguring existing env ${pc.cyan(pc.bold(`"${existingEnv.name}"`))} from the global config because ${pc.bold('--force')} was set. The env config will be updated before install starts, then refreshed again after install succeeds.`);
442
443
  }
443
444
  await this.syncNocoBaseSkills({
444
445
  skip: Boolean(normalizedFlags['skip-skills']),
@@ -447,14 +448,16 @@ Prompt modes:
447
448
  try {
448
449
  // oclif explicit registry keys use `:` (e.g. `env:add`); users still type `nb env add`.
449
450
  if (hasNocobase) {
450
- p.log.step('Running nb env add');
451
+ logInitStage('Connecting to the env');
452
+ printVerbose('Running nb env add');
451
453
  await this.config.runCommand('env:add', this.buildEnvAddArgv(results));
452
454
  }
453
455
  else {
454
- p.log.step('Saving the local env config');
456
+ logInitStage('Saving env config');
455
457
  await this.persistManagedEnvConfig(results, normalizedFlags);
456
458
  managedInstallResults = results;
457
- p.log.step('Running nb init');
459
+ printInfo(`Saved env config for "${String(results.appName ?? DEFAULT_INIT_APP_NAME).trim() || DEFAULT_INIT_APP_NAME}".`);
460
+ printVerbose('Running nb init');
458
461
  await this.config.runCommand('install', this.buildInstallArgv(results, normalizedFlags));
459
462
  }
460
463
  }
@@ -463,10 +466,9 @@ Prompt modes:
463
466
  const formatted = managedInstallResults
464
467
  ? this.formatManagedInstallFailureMessage(message, managedInstallResults, normalizedFlags)
465
468
  : message;
466
- p.outro(pc.red(formatted));
469
+ this.error(pc.red(formatted));
467
470
  this.exit(1);
468
471
  }
469
- p.outro('Workspace init finished.');
470
472
  }
471
473
  static async buildDynamicInitialValuesForInstall(flags, presetValues) {
472
474
  const out = {};
@@ -706,22 +708,25 @@ Prompt modes:
706
708
  }
707
709
  async syncNocoBaseSkills(options) {
708
710
  if (options?.skip) {
709
- p.log.step('Skipped NocoBase agent skills sync.');
711
+ printVerbose('Skipped agent skills sync.');
710
712
  return;
711
713
  }
712
714
  try {
715
+ logInitStage('Syncing agent skills');
713
716
  const status = await inspectSkillsStatus();
714
717
  if (!status.installed) {
715
- p.log.step('Installing NocoBase agent skills (nb skills install)');
718
+ printVerbose('Installing NocoBase agent skills (nb skills install)');
716
719
  await installNocoBaseSkills();
720
+ printInfo('Agent skills ready.');
717
721
  return;
718
722
  }
719
- p.log.step('Updating NocoBase agent skills (nb skills update)');
723
+ printVerbose('Updating NocoBase agent skills (nb skills update)');
720
724
  await updateNocoBaseSkills();
725
+ printInfo('Agent skills ready.');
721
726
  }
722
727
  catch (error) {
723
728
  const message = error instanceof Error ? error.message : String(error);
724
- p.outro(pc.red(`Skills sync failed: ${message}`));
729
+ this.error(pc.red(`Skills sync failed: ${message}`));
725
730
  this.exit(1);
726
731
  }
727
732
  }
@@ -798,9 +803,11 @@ Prompt modes:
798
803
  if (options?.nonInteractive ?? true) {
799
804
  argv.unshift('-y');
800
805
  }
806
+ argv.push('--skip-save-env-log');
801
807
  const processArgv = process.argv.slice(2);
802
808
  const envName = String(results.appName ?? DEFAULT_INIT_APP_NAME).trim() || DEFAULT_INIT_APP_NAME;
803
809
  const source = String(results.source ?? '').trim();
810
+ const hasNocobase = String(results.hasNocobase ?? '').trim() === 'yes';
804
811
  const apiBaseUrl = String(results.apiBaseUrl ?? '').trim();
805
812
  const authType = String(results.authType ?? '').trim();
806
813
  const accessToken = String(results.accessToken ?? '');
@@ -811,7 +818,7 @@ Prompt modes:
811
818
  if (Boolean(flags.verbose)) {
812
819
  argv.push('--verbose');
813
820
  }
814
- if (apiBaseUrl) {
821
+ if (hasNocobase && apiBaseUrl) {
815
822
  argv.push('--api-base-url', apiBaseUrl);
816
823
  }
817
824
  if (authType) {
@@ -7,8 +7,6 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
- import * as p from '@clack/prompts';
11
- import _ from 'lodash';
12
10
  import { spawn } from 'node:child_process';
13
11
  import crypto from 'node:crypto';
14
12
  import { mkdir } from 'node:fs/promises';
@@ -24,9 +22,11 @@ import { findAvailableTcpPort, validateAvailableTcpPort, validateTcpPort, valida
24
22
  import { validateExternalDbConfig } from "../lib/db-connection-check.js";
25
23
  import { formatMissingManagedAppEnvMessage } from '../lib/app-runtime.js';
26
24
  import { run, runNocoBaseCommand } from '../lib/run-npm.js';
27
- import { startTask, stopTask, updateTask } from '../lib/ui.js';
25
+ import { printInfo, printStage, printVerbose, printWarning, setVerboseMode, } from '../lib/ui.js';
26
+ import { omitKeys, upperFirst } from "../lib/object-utils.js";
28
27
  import { getEnv, setCurrentEnv, upsertEnv } from '../lib/auth-store.js';
29
28
  import { buildStoredEnvConfig } from '../lib/env-config.js';
29
+ import { resolveDockerEnvFileArg, } from "../lib/docker-env-file.js";
30
30
  import Download, { defaultDockerRegistryForLang, } from './download.js';
31
31
  import EnvAdd from "./env/add.js";
32
32
  const DEFAULT_INSTALL_ENV_NAME = 'local';
@@ -260,6 +260,13 @@ async function commandOutput(command, args, options) {
260
260
  });
261
261
  }
262
262
  export default class Install extends Command {
263
+ ensuredDockerNetworks = new Set();
264
+ logStage(title) {
265
+ printStage(title);
266
+ }
267
+ logDetail(message) {
268
+ printVerbose(message);
269
+ }
263
270
  static hidden = true;
264
271
  static description = 'Install NocoBase: database, storage, admin user, and `nocobase-v1 install`. Optionally run `nb source download` first; distribution and image details are configured on `nb source download`, not here. Use `--resume` to continue an interrupted setup from the saved workspace env config.';
265
272
  static examples = [
@@ -288,6 +295,10 @@ export default class Install extends Command {
288
295
  description: 'Show detailed command output',
289
296
  default: false,
290
297
  }),
298
+ 'skip-save-env-log': Flags.boolean({
299
+ hidden: true,
300
+ default: false,
301
+ }),
291
302
  env: Flags.string({
292
303
  char: 'e',
293
304
  description: 'App/env name to create or update. Defaults app paths to ./<envName>/source/ and ./<envName>/storage/.',
@@ -373,7 +384,7 @@ export default class Install extends Command {
373
384
  description: 'Download NocoBase app files or pull a Docker image before installing',
374
385
  default: false,
375
386
  }),
376
- ..._.omit(Download.flags, ['yes']),
387
+ ...omitKeys(Download.flags, ['yes']),
377
388
  };
378
389
  /** Environment name only: run before {@link Install.prompts} (see `run`). */
379
390
  static envPrompts = {
@@ -797,7 +808,7 @@ export default class Install extends Command {
797
808
  const database = String(dbResults.dbDatabase ?? '').trim();
798
809
  const address = host && port ? `${host}:${port}` : host || port || '(unknown address)';
799
810
  const target = database ? `${address}/${database}` : address;
800
- p.log.step(`Checking external ${dialect} database: ${target}`);
811
+ printVerbose(`Checking external ${dialect} database: ${target}`);
801
812
  const validationError = await validateExternalDbConfig(dbResults);
802
813
  if (validationError) {
803
814
  throw new Error(validationError);
@@ -1026,7 +1037,7 @@ export default class Install extends Command {
1026
1037
  }
1027
1038
  const nextPort = await findAvailableTcpPort();
1028
1039
  if (options?.warn) {
1029
- p.log.warn(`${options.label ?? 'Default port'} ${normalized} is already in use. Using available port ${nextPort} for this setup.`);
1040
+ printWarning(`${options.label ?? 'Default port'} ${normalized} is already in use. Using available port ${nextPort} for this setup.`);
1030
1041
  }
1031
1042
  return nextPort;
1032
1043
  }
@@ -1093,7 +1104,6 @@ export default class Install extends Command {
1093
1104
  yes: false,
1094
1105
  hooks: {
1095
1106
  onCancel: () => {
1096
- p.cancel('Download cancelled.');
1097
1107
  exit(0);
1098
1108
  },
1099
1109
  onMissingNonInteractive: (message) => {
@@ -1443,18 +1453,23 @@ export default class Install extends Command {
1443
1453
  throw new Error(`Built-in database does not support "${dbDialect}" yet. Please choose PostgreSQL, MySQL, MariaDB, or KingbaseES.`);
1444
1454
  }
1445
1455
  async ensureDockerNetwork(name) {
1446
- p.log.step(`Checking Docker network: ${name}`);
1456
+ if (this.ensuredDockerNetworks.has(name)) {
1457
+ return;
1458
+ }
1459
+ printVerbose(`Checking Docker network: ${name}`);
1447
1460
  const exists = await commandSucceeds('docker', ['network', 'inspect', name]);
1448
1461
  if (exists) {
1449
- p.log.info(`Docker network already exists: ${name}`);
1462
+ printVerbose(`Docker network already exists: ${name}`);
1463
+ this.ensuredDockerNetworks.add(name);
1450
1464
  return;
1451
1465
  }
1452
- p.log.step(`Creating Docker network: ${name}`);
1466
+ printVerbose(`Creating Docker network: ${name}`);
1453
1467
  try {
1454
1468
  await run('docker', ['network', 'create', name], {
1455
1469
  errorName: 'docker network create',
1456
1470
  });
1457
- p.log.info(`Docker network is ready: ${name}`);
1471
+ printVerbose(`Docker network is ready: ${name}`);
1472
+ this.ensuredDockerNetworks.add(name);
1458
1473
  }
1459
1474
  catch (error) {
1460
1475
  const message = error instanceof Error ? error.message : String(error);
@@ -1489,7 +1504,7 @@ export default class Install extends Command {
1489
1504
  if (!params.force) {
1490
1505
  return true;
1491
1506
  }
1492
- p.log.info(`Removing existing ${params.displayName}: ${params.containerName}`);
1507
+ printVerbose(`Removing existing ${params.displayName}: ${params.containerName}`);
1493
1508
  await this.removeDockerContainer(params.containerName);
1494
1509
  return false;
1495
1510
  }
@@ -1513,7 +1528,7 @@ export default class Install extends Command {
1513
1528
  async ensureBuiltinDbContainer(plan, options) {
1514
1529
  const exists = await this.dockerContainerExists(plan.containerName);
1515
1530
  if (exists) {
1516
- p.log.info(`Built-in ${plan.dbDialect} container already exists: ${plan.containerName}`);
1531
+ printVerbose(`Built-in ${plan.dbDialect} container already exists: ${plan.containerName}`);
1517
1532
  return;
1518
1533
  }
1519
1534
  await mkdir(plan.dataDir, { recursive: true });
@@ -1540,7 +1555,8 @@ export default class Install extends Command {
1540
1555
  dbPassword: params.dbResults.dbPassword,
1541
1556
  builtinDbImage: params.dbResults.builtinDbImage,
1542
1557
  });
1543
- p.log.step(`Preparing built-in ${plan.dbDialect} database`);
1558
+ this.logStage('Preparing database');
1559
+ printInfo(`Using built-in ${plan.dbDialect} database.`);
1544
1560
  await this.ensureDockerNetwork(plan.networkName);
1545
1561
  const existingContainerKept = await this.removeDockerContainerIfForced({
1546
1562
  containerName: plan.containerName,
@@ -1556,10 +1572,11 @@ export default class Install extends Command {
1556
1572
  await this.ensureBuiltinDbContainer(plan, {
1557
1573
  stdio: params.commandStdio ?? 'ignore',
1558
1574
  });
1559
- p.log.info(`Built-in ${plan.dbDialect} database is ready at ${plan.dbHost}:${plan.dbPort}`);
1575
+ printInfo(`${upperFirst(plan.dbDialect)} database ready.`);
1576
+ printVerbose(`Built-in ${plan.dbDialect} database ready at ${plan.dbHost}:${plan.dbPort}`);
1560
1577
  return plan;
1561
1578
  }
1562
- static buildDockerAppPlan(params) {
1579
+ static async buildDockerAppPlan(params) {
1563
1580
  const dockerRegistry = String(downloadResultsValue(params.downloadResults, 'dockerRegistry') ?? '').trim()
1564
1581
  || defaultDockerRegistryForLang(process.env.NB_LOCALE);
1565
1582
  const version = String(downloadResultsValue(params.downloadResults, 'version') ?? '').trim() || DEFAULT_DOCKER_VERSION;
@@ -1582,6 +1599,8 @@ export default class Install extends Command {
1582
1599
  const appKey = crypto.randomBytes(32).toString('hex');
1583
1600
  const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
1584
1601
  const containerName = Install.buildDockerAppContainerName(params.envName, params.dockerContainerPrefix ?? params.workspaceName);
1602
+ const configuredEnvFile = String(params.appResults.envFile ?? '').trim();
1603
+ const envFile = await resolveDockerEnvFileArg(params.envName, configuredEnvFile ? { envFile: configuredEnvFile } : undefined);
1585
1604
  const initEnvVars = Install.buildInitAppEnvVars({
1586
1605
  appResults: params.appResults,
1587
1606
  rootResults: params.rootResults,
@@ -1598,6 +1617,9 @@ export default class Install extends Command {
1598
1617
  '-p',
1599
1618
  `${appPort}:80`,
1600
1619
  ];
1620
+ if (envFile) {
1621
+ args.push('--env-file', envFile);
1622
+ }
1601
1623
  for (const [key, value] of Object.entries(initEnvVars)) {
1602
1624
  args.push('-e', `${key}=${value}`);
1603
1625
  }
@@ -1609,6 +1631,7 @@ export default class Install extends Command {
1609
1631
  imageRef,
1610
1632
  appPort,
1611
1633
  storagePath,
1634
+ envFile,
1612
1635
  appKey,
1613
1636
  timeZone,
1614
1637
  args,
@@ -1617,7 +1640,7 @@ export default class Install extends Command {
1617
1640
  async ensureDockerAppContainer(plan, options) {
1618
1641
  const exists = await this.dockerContainerExists(plan.containerName);
1619
1642
  if (exists) {
1620
- p.log.info(`App container already exists: ${plan.containerName}`);
1643
+ printVerbose(`App container already exists: ${plan.containerName}`);
1621
1644
  return 'existing';
1622
1645
  }
1623
1646
  await mkdir(plan.storagePath, { recursive: true });
@@ -1631,7 +1654,7 @@ export default class Install extends Command {
1631
1654
  const networkName = params.builtinDbPlan?.networkName
1632
1655
  ?? Install.buildBuiltinDbNetworkName(params.envName, params.dockerNetworkName ?? params.workspaceName);
1633
1656
  await this.ensureDockerNetwork(networkName);
1634
- const plan = Install.buildDockerAppPlan({
1657
+ const plan = await Install.buildDockerAppPlan({
1635
1658
  envName: params.envName,
1636
1659
  workspaceName: params.workspaceName,
1637
1660
  dockerContainerPrefix: params.dockerContainerPrefix,
@@ -1641,7 +1664,7 @@ export default class Install extends Command {
1641
1664
  rootResults: params.rootResults,
1642
1665
  networkName,
1643
1666
  });
1644
- p.log.step(`Starting Docker app ${plan.imageRef}`);
1667
+ printVerbose('Starting NocoBase app (Docker)');
1645
1668
  await this.removeDockerContainerIfForced({
1646
1669
  containerName: plan.containerName,
1647
1670
  displayName: 'app container',
@@ -1655,7 +1678,7 @@ export default class Install extends Command {
1655
1678
  plan.appKey = env.APP_KEY || plan.appKey;
1656
1679
  plan.timeZone = env.TZ || plan.timeZone;
1657
1680
  }
1658
- p.log.info(`App container is ready at http://127.0.0.1:${plan.appPort}`);
1681
+ printVerbose(`NocoBase app is starting at http://127.0.0.1:${plan.appPort}`);
1659
1682
  return plan;
1660
1683
  }
1661
1684
  static pushDownloadArgIfValue(argv, flag, value) {
@@ -1666,6 +1689,9 @@ export default class Install extends Command {
1666
1689
  }
1667
1690
  static buildDownloadArgvFromResults(results, options) {
1668
1691
  const argv = ['-y', '--no-intro'];
1692
+ if (options?.compactLog) {
1693
+ argv.push('--compact-log');
1694
+ }
1669
1695
  const source = String(results.source ?? '').trim();
1670
1696
  if (options?.verbose) {
1671
1697
  argv.push('--verbose');
@@ -1718,11 +1744,8 @@ export default class Install extends Command {
1718
1744
  async downloadManagedSource(params) {
1719
1745
  const argv = Install.buildDownloadArgvFromResults(params.downloadResults, {
1720
1746
  verbose: params.verbose,
1747
+ compactLog: true,
1721
1748
  });
1722
- const source = String(params.downloadResults.source ?? '').trim();
1723
- p.log.step(source === 'docker'
1724
- ? 'Downloading Docker image'
1725
- : 'Downloading local NocoBase app files');
1726
1749
  return await this.config.runCommand('source:download', argv);
1727
1750
  }
1728
1751
  async downloadLocalApp(params) {
@@ -1784,7 +1807,7 @@ export default class Install extends Command {
1784
1807
  rootResults: params.rootResults,
1785
1808
  });
1786
1809
  const args = ['start', '--quickstart', '--daemon'];
1787
- p.log.step(`Stopping any existing local NocoBase process in ${params.projectRoot}`);
1810
+ this.logDetail(`Stopping any existing local NocoBase process in ${params.projectRoot}`);
1788
1811
  try {
1789
1812
  await runNocoBaseCommand(['pm2', 'kill'], {
1790
1813
  cwd: params.projectRoot,
@@ -1794,15 +1817,15 @@ export default class Install extends Command {
1794
1817
  }
1795
1818
  catch (error) {
1796
1819
  const message = error instanceof Error ? error.message : String(error);
1797
- p.log.info(`Skipped local process cleanup before start: ${message}`);
1820
+ this.logDetail(`Skipped local process cleanup before start: ${message}`);
1798
1821
  }
1799
- p.log.step(`Starting local NocoBase app from ${params.projectRoot}`);
1822
+ this.logDetail(`Starting local NocoBase app from ${params.projectRoot}`);
1800
1823
  await runNocoBaseCommand(args, {
1801
1824
  cwd: params.projectRoot,
1802
1825
  env,
1803
1826
  stdio: params.commandStdio ?? 'ignore',
1804
1827
  });
1805
- p.log.info(`Local app is starting at http://127.0.0.1:${env.APP_PORT}`);
1828
+ this.logDetail(`Local app is starting at http://127.0.0.1:${env.APP_PORT}`);
1806
1829
  return {
1807
1830
  source: params.source,
1808
1831
  projectRoot: params.projectRoot,
@@ -1876,35 +1899,29 @@ export default class Install extends Command {
1876
1899
  const fetchImpl = options?.fetchImpl ?? fetch;
1877
1900
  const startedAt = Date.now();
1878
1901
  let lastMessage = 'No response yet';
1879
- let taskActive = true;
1880
- startTask(`Waiting for application health check: ${healthCheckUrl}. NocoBase has started and is still booting...`);
1881
- try {
1882
- while (Date.now() - startedAt < timeoutMs) {
1883
- const result = await Install.requestAppHealthCheck({
1884
- healthCheckUrl,
1885
- fetchImpl,
1886
- requestTimeoutMs,
1887
- });
1888
- if (result.ok) {
1889
- stopTask();
1890
- taskActive = false;
1891
- p.log.info(`Application health check passed: ${healthCheckUrl}`);
1892
- return;
1893
- }
1894
- lastMessage = result.message;
1895
- const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
1896
- updateTask(`Waiting for application health check: ${healthCheckUrl}. Still starting... (${elapsedSeconds}s elapsed, last status: ${Install.formatHealthCheckMessage(lastMessage)})`);
1897
- const remainingMs = timeoutMs - (Date.now() - startedAt);
1898
- if (remainingMs <= 0) {
1899
- break;
1900
- }
1901
- await Install.sleep(Math.min(intervalMs, remainingMs));
1902
+ let lastLoggedStatus = '';
1903
+ printInfo('Waiting for NocoBase to become ready...');
1904
+ while (Date.now() - startedAt < timeoutMs) {
1905
+ const result = await Install.requestAppHealthCheck({
1906
+ healthCheckUrl,
1907
+ fetchImpl,
1908
+ requestTimeoutMs,
1909
+ });
1910
+ if (result.ok) {
1911
+ return;
1902
1912
  }
1903
- }
1904
- finally {
1905
- if (taskActive) {
1906
- stopTask();
1913
+ lastMessage = result.message;
1914
+ const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
1915
+ const statusLine = `Waiting for NocoBase to become ready... (${elapsedSeconds}s elapsed, last status: ${Install.formatHealthCheckMessage(lastMessage)})`;
1916
+ if (statusLine !== lastLoggedStatus) {
1917
+ printInfo(statusLine);
1918
+ lastLoggedStatus = statusLine;
1919
+ }
1920
+ const remainingMs = timeoutMs - (Date.now() - startedAt);
1921
+ if (remainingMs <= 0) {
1922
+ break;
1907
1923
  }
1924
+ await Install.sleep(Math.min(intervalMs, remainingMs));
1908
1925
  }
1909
1926
  const logHint = options?.containerName
1910
1927
  ? ` You can inspect startup logs with: docker logs ${options.containerName}`
@@ -1931,6 +1948,7 @@ export default class Install extends Command {
1931
1948
  || DEFAULT_INSTALL_APP_PORT;
1932
1949
  const storagePath = String(params.appResults.storagePath ?? '').trim()
1933
1950
  || defaultInstallStoragePath(params.envName);
1951
+ const envFile = String(params.appResults.envFile ?? '').trim() || undefined;
1934
1952
  const apiBaseUrl = Install.resolveApiBaseUrl({
1935
1953
  appResults: params.appResults,
1936
1954
  envAddResults: params.envAddResults,
@@ -1953,6 +1971,7 @@ export default class Install extends Command {
1953
1971
  appRootPath: params.appResults.appRootPath,
1954
1972
  appPort,
1955
1973
  storagePath,
1974
+ ...(envFile ? { envFile } : {}),
1956
1975
  appKey: params.appResults.appKey,
1957
1976
  timezone: params.appResults.timeZone,
1958
1977
  builtinDb: params.dbResults.builtinDb,
@@ -2049,7 +2068,14 @@ export default class Install extends Command {
2049
2068
  },
2050
2069
  yes,
2051
2070
  });
2052
- const envAddResults = await runPromptCatalog(EnvAdd.prompts, {
2071
+ const envAddPromptsForInstall = {
2072
+ ...EnvAdd.prompts,
2073
+ apiBaseUrl: {
2074
+ ...EnvAdd.prompts.apiBaseUrl,
2075
+ validate: undefined,
2076
+ },
2077
+ };
2078
+ const envAddResults = await runPromptCatalog(envAddPromptsForInstall, {
2053
2079
  initialValues: {
2054
2080
  apiBaseUrl: `http://127.0.0.1:${appResults.appPort ?? DEFAULT_INSTALL_APP_PORT}/api`,
2055
2081
  },
@@ -2077,13 +2103,14 @@ export default class Install extends Command {
2077
2103
  const parsed = {
2078
2104
  ...flags,
2079
2105
  };
2106
+ setVerboseMode(Boolean(parsed.verbose));
2080
2107
  const commandStdio = this.commandStdio(parsed.verbose);
2081
2108
  if (!parsed['no-intro']) {
2082
- p.intro('Set Up NocoBase');
2109
+ this.logStage('Set up NocoBase');
2083
2110
  }
2084
2111
  if (parsed.resume) {
2085
2112
  const envLabel = Install.toOptionalPromptString(parsed.env);
2086
- p.log.step(envLabel
2113
+ printInfo(envLabel
2087
2114
  ? `Resuming setup for env "${envLabel}" from the saved workspace config`
2088
2115
  : 'Resuming setup from the saved workspace config');
2089
2116
  }
@@ -2100,6 +2127,9 @@ export default class Install extends Command {
2100
2127
  : undefined;
2101
2128
  await Install.ensureExternalDbReadyForInstall(dbResults);
2102
2129
  if (!parsed.resume) {
2130
+ if (!parsed['skip-save-env-log']) {
2131
+ this.logStage('Saving env config');
2132
+ }
2103
2133
  await this.saveInstalledEnv({
2104
2134
  envName,
2105
2135
  appResults,
@@ -2108,7 +2138,9 @@ export default class Install extends Command {
2108
2138
  rootResults,
2109
2139
  envAddResults,
2110
2140
  });
2111
- p.log.info(`Saved install config for env "${envName}"`);
2141
+ if (!parsed['skip-save-env-log']) {
2142
+ printInfo(`Saved env config for "${envName}".`);
2143
+ }
2112
2144
  }
2113
2145
  let builtinDbPlan;
2114
2146
  if (Boolean(dbResults.builtinDb)) {
@@ -2132,11 +2164,13 @@ export default class Install extends Command {
2132
2164
  let dockerAppPlan;
2133
2165
  let localAppPlan;
2134
2166
  if (Boolean(appResults.fetchSource)) {
2167
+ this.logStage('Preparing application');
2135
2168
  if (source === 'docker') {
2136
2169
  await this.downloadManagedSource({
2137
2170
  downloadResults,
2138
2171
  verbose: parsed.verbose,
2139
2172
  });
2173
+ printInfo('Application image ready.');
2140
2174
  dockerAppPlan = await this.installDockerApp({
2141
2175
  envName,
2142
2176
  dockerNetworkName,
@@ -2160,6 +2194,7 @@ export default class Install extends Command {
2160
2194
  downloadResults,
2161
2195
  verbose: parsed.verbose,
2162
2196
  });
2197
+ printInfo('Application files ready.');
2163
2198
  localAppPlan = await this.startLocalApp({
2164
2199
  envName,
2165
2200
  source: localSource,
@@ -2174,15 +2209,17 @@ export default class Install extends Command {
2174
2209
  }
2175
2210
  }
2176
2211
  else {
2177
- p.log.info('Skipped app download and install.');
2212
+ this.logDetail('Skipped app download and install.');
2178
2213
  }
2179
2214
  if (dockerAppPlan || localAppPlan) {
2215
+ this.logStage('Starting NocoBase');
2180
2216
  await this.waitForAppHealthCheck(Install.resolveApiBaseUrl({
2181
2217
  appResults,
2182
2218
  envAddResults,
2183
2219
  }), {
2184
2220
  containerName: dockerAppPlan?.containerName,
2185
2221
  });
2222
+ printInfo(`NocoBase is ready at http://127.0.0.1:${dockerAppPlan?.appPort ?? localAppPlan?.appPort}`);
2186
2223
  }
2187
2224
  if (dockerAppPlan || localAppPlan || builtinDbPlan) {
2188
2225
  await this.saveInstalledEnv({
@@ -2199,9 +2236,9 @@ export default class Install extends Command {
2199
2236
  envAddResults,
2200
2237
  appReady: Boolean(dockerAppPlan || localAppPlan),
2201
2238
  });
2202
- p.outro(dockerAppPlan || localAppPlan
2203
- ? `NocoBase is ready at http://127.0.0.1:${dockerAppPlan?.appPort ?? localAppPlan?.appPort}`
2204
- : `Install config for "${envName}" has been saved.`);
2239
+ if (!dockerAppPlan && !localAppPlan) {
2240
+ printInfo(`Install config for "${envName}" has been saved.`);
2241
+ }
2205
2242
  }
2206
2243
  }
2207
2244
  function downloadResultsValue(downloadResults, key) {