@nocobase/cli 2.1.0-beta.42 → 2.1.0-beta.42-test.2

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
@@ -69,49 +69,20 @@ const { ensureRuntimeFromArgv } = await import(pathToFileURL(bootstrapPath).href
69
69
  const startupUpdatePath = isDev
70
70
  ? path.join(root, 'src/lib/startup-update.ts')
71
71
  : path.join(root, 'dist/lib/startup-update.js');
72
- const { maybeRunStartupUpdatePrompt } = await import(pathToFileURL(startupUpdatePath).href);
72
+ const { maybeRunStartupUpdate } = await import(pathToFileURL(startupUpdatePath).href);
73
+ const cliEntryErrorPath = isDev
74
+ ? path.join(root, 'src/lib/cli-entry-error.ts')
75
+ : path.join(root, 'dist/lib/cli-entry-error.js');
76
+ const { formatCliEntryError } = await import(pathToFileURL(cliEntryErrorPath).href);
73
77
  const { flush, run, settings } = await import('@oclif/core');
74
78
 
75
79
  if (isDev) {
76
80
  settings.debug = true;
77
81
  }
78
82
 
79
- function getCommandToken(argv) {
80
- const tokens = [];
81
-
82
- for (const token of argv) {
83
- if (!token || token.startsWith('-')) {
84
- continue;
85
- }
86
-
87
- tokens.push(token);
88
- }
89
-
90
- if (tokens[0] === 'api') {
91
- return tokens[1] ?? tokens[0];
92
- }
93
-
94
- return tokens[0];
95
- }
96
-
97
- function formatCliEntryError(error, argv) {
98
- const message = error instanceof Error ? error.message : String(error);
99
- const missingCommandMatch = message.match(/^Command (.+) not found\.$/);
100
- if (missingCommandMatch) {
101
- const commandToken = getCommandToken(argv) ?? missingCommandMatch[1];
102
- return [
103
- `Unknown command: \`${commandToken}\`.`,
104
- 'If this is a built-in command or a typo, run `nb --help` to inspect available commands.',
105
- `If \`${commandToken}\` should be a runtime command from your NocoBase app, run \`nb env update\` and try again.`,
106
- ].join('\n');
107
- }
108
-
109
- return message;
110
- }
111
-
112
83
  try {
113
84
  const argv = process.argv.slice(2);
114
- const startupUpdate = await maybeRunStartupUpdatePrompt(argv);
85
+ const startupUpdate = await maybeRunStartupUpdate(argv);
115
86
  if (startupUpdate.kind === 'updated') {
116
87
  const result = spawnSync(process.execPath, process.argv.slice(1), {
117
88
  stdio: 'inherit',
@@ -55,7 +55,11 @@ export default class AppRestart extends Command {
55
55
  default: false,
56
56
  }),
57
57
  quickstart: Flags.boolean({ description: 'Quickstart the application after stopping it', required: false }),
58
- port: Flags.string({ description: 'Port (overrides appPort from env config when set)', char: 'p', required: false }),
58
+ port: Flags.string({
59
+ description: 'Port (overrides appPort from env config when set)',
60
+ char: 'p',
61
+ required: false,
62
+ }),
59
63
  daemon: Flags.boolean({
60
64
  description: 'Run the application as a daemon after stopping it (default: true; use --no-daemon to stay in the foreground)',
61
65
  char: 'd',
@@ -63,7 +67,11 @@ export default class AppRestart extends Command {
63
67
  default: true,
64
68
  allowNo: true,
65
69
  }),
66
- instances: Flags.integer({ description: 'Number of instances to run after stopping it', char: 'i', required: false }),
70
+ instances: Flags.integer({
71
+ description: 'Number of instances to run after stopping it',
72
+ char: 'i',
73
+ required: false,
74
+ }),
67
75
  'launch-mode': Flags.string({ description: 'Launch Mode', required: false, options: ['pm2', 'node'] }),
68
76
  verbose: Flags.boolean({
69
77
  description: 'Show raw shutdown/startup output from the underlying local or Docker command',
@@ -121,14 +129,13 @@ export default class AppRestart extends Command {
121
129
  failTask(`Failed to recreate NocoBase for "${runtime.envName}".`);
122
130
  this.error(formatDockerRestartFailure(runtime.envName, message));
123
131
  }
124
- const appUrl = formatAppUrl(runtime.env.appPort === undefined || runtime.env.appPort === null
125
- ? undefined
126
- : String(runtime.env.appPort));
132
+ const appUrl = formatAppUrl(runtime.env.appPort === undefined || runtime.env.appPort === null ? undefined : String(runtime.env.appPort));
127
133
  await waitForAppReady({
128
134
  envName: runtime.envName,
129
135
  apiBaseUrl: resolveManagedAppApiBaseUrl(runtime),
130
136
  containerName: runtime.containerName,
131
137
  logHint: `You can inspect startup logs with \`nb app logs --env ${runtime.envName}\`.`,
138
+ ...(flags.verbose ? { verbose: true } : {}),
132
139
  });
133
140
  succeedTask(`NocoBase is running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
134
141
  return;
@@ -175,6 +175,7 @@ export default class AppStart extends Command {
175
175
  apiBaseUrl,
176
176
  containerName: runtime.containerName,
177
177
  logHint: `You can inspect startup logs with \`nb app logs --env ${runtime.envName}\`.`,
178
+ ...(flags.verbose ? { verbose: true } : {}),
178
179
  });
179
180
  if (shouldPrintStartSuccess()) {
180
181
  succeedTask(`NocoBase is running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
@@ -312,7 +312,7 @@ export default class AppUpgrade extends Command {
312
312
  throw new Error([
313
313
  `Env "${runtime.envName}" does not have a saved \`downloadVersion\`.`,
314
314
  'This env cannot be upgraded until a source version is explicit.',
315
- 'Re-run `nb init` or `nb env add` for this env, or pass `--version` to `nb app upgrade`.',
315
+ `Re-run \`nb init --ui --env ${runtime.envName}\` for this env, or pass \`--version\` to \`nb app upgrade\`.`,
316
316
  ].join('\n'));
317
317
  }
318
318
  return {
@@ -383,7 +383,7 @@ export default class AppUpgrade extends Command {
383
383
  this.error([
384
384
  `Can't upgrade "${runtime.envName}" from this machine.`,
385
385
  'This env only has an API connection, so there is no saved local app or Docker runtime to upgrade here.',
386
- 'If you want a local NocoBase AI environment that the CLI can upgrade, run `nb init` first.',
386
+ 'If you want a local NocoBase AI environment that the CLI can upgrade, run `nb init --ui` first.',
387
387
  ].join('\n'));
388
388
  }
389
389
  if (runtime.kind === 'ssh') {
@@ -8,10 +8,12 @@
8
8
  */
9
9
  import { Args, Command } from '@oclif/core';
10
10
  import { assertSupportedCliConfigKey, deleteCliConfigValue } from '../../lib/cli-config.js';
11
+ import { clearLegacyStartupUpdatePolicyForCurrentInstall } from '../../lib/startup-update.js';
11
12
  export default class ConfigDelete extends Command {
12
13
  static summary = 'Delete an explicitly configured CLI setting';
13
14
  static examples = [
14
15
  '<%= config.bin %> <%= command.id %> locale',
16
+ '<%= config.bin %> <%= command.id %> update.policy',
15
17
  '<%= config.bin %> <%= command.id %> license.pkg-url',
16
18
  '<%= config.bin %> <%= command.id %> docker.network',
17
19
  '<%= config.bin %> <%= command.id %> docker.container-prefix',
@@ -29,6 +31,7 @@ export default class ConfigDelete extends Command {
29
31
  const { args } = await this.parse(ConfigDelete);
30
32
  const key = assertSupportedCliConfigKey(args.key);
31
33
  const removed = await deleteCliConfigValue(key);
32
- this.log(removed ? `Deleted ${key}` : `${key} was not set`);
34
+ const clearedLegacy = key === 'update.policy' ? await clearLegacyStartupUpdatePolicyForCurrentInstall() : false;
35
+ this.log(removed || clearedLegacy ? `Deleted ${key}` : `${key} was not set`);
33
36
  }
34
37
  }
@@ -12,6 +12,7 @@ export default class ConfigGet extends Command {
12
12
  static summary = 'Get the effective CLI configuration value for a key';
13
13
  static examples = [
14
14
  '<%= config.bin %> <%= command.id %> locale',
15
+ '<%= config.bin %> <%= command.id %> update.policy',
15
16
  '<%= config.bin %> <%= command.id %> license.pkg-url',
16
17
  '<%= config.bin %> <%= command.id %> docker.network',
17
18
  '<%= config.bin %> <%= command.id %> docker.container-prefix',
@@ -8,11 +8,13 @@
8
8
  */
9
9
  import { Args, Command } from '@oclif/core';
10
10
  import { assertSupportedCliConfigKey, setCliConfigValue } from '../../lib/cli-config.js';
11
+ import { clearLegacyStartupUpdatePolicyForCurrentInstall } from '../../lib/startup-update.js';
11
12
  export default class ConfigSet extends Command {
12
13
  static summary = 'Set a CLI configuration value';
13
- static description = 'Set a supported CLI configuration key. Supported keys: locale, license.pkg-url, docker.network, docker.container-prefix, bin.docker, bin.git, bin.yarn.';
14
+ static description = 'Set a supported CLI configuration key. Supported keys: locale, update.policy, license.pkg-url, docker.network, docker.container-prefix, bin.docker, bin.git, bin.yarn.';
14
15
  static examples = [
15
16
  '<%= config.bin %> <%= command.id %> locale zh-CN',
17
+ '<%= config.bin %> <%= command.id %> update.policy prompt',
16
18
  '<%= config.bin %> <%= command.id %> license.pkg-url https://pkg.nocobase.com/',
17
19
  '<%= config.bin %> <%= command.id %> docker.network nocobase',
18
20
  '<%= config.bin %> <%= command.id %> docker.container-prefix nb',
@@ -34,6 +36,9 @@ export default class ConfigSet extends Command {
34
36
  const { args } = await this.parse(ConfigSet);
35
37
  const key = assertSupportedCliConfigKey(args.key);
36
38
  const value = await setCliConfigValue(key, args.value);
39
+ if (key === 'update.policy') {
40
+ await clearLegacyStartupUpdatePolicyForCurrentInstall();
41
+ }
37
42
  this.log(`${key}=${value}`);
38
43
  }
39
44
  }
@@ -13,10 +13,7 @@ import { renderTable } from '../../lib/ui.js';
13
13
  import { builtinDbStatus, resolveDbRuntime } from './shared.js';
14
14
  export default class DbPs extends Command {
15
15
  static description = 'Show built-in database runtime status for configured envs without starting or stopping anything.';
16
- static examples = [
17
- '<%= config.bin %> <%= command.id %>',
18
- '<%= config.bin %> <%= command.id %> --env app1',
19
- ];
16
+ static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --env app1'];
20
17
  static flags = {
21
18
  env: Flags.string({
22
19
  char: 'e',
@@ -26,11 +23,9 @@ export default class DbPs extends Command {
26
23
  async run() {
27
24
  const { flags } = await this.parse(DbPs);
28
25
  const requestedEnv = flags.env?.trim() || undefined;
29
- const envNames = requestedEnv
30
- ? [requestedEnv]
31
- : Object.keys((await listEnvs()).envs).sort();
26
+ const envNames = requestedEnv ? [requestedEnv] : Object.keys((await listEnvs()).envs).sort();
32
27
  if (!envNames.length) {
33
- this.log('No NocoBase env is configured yet. Run `nb init` to create one first.');
28
+ this.log('No NocoBase env is configured yet. Run `nb init --ui` to create one first.');
34
29
  return;
35
30
  }
36
31
  const rows = [];
@@ -44,16 +39,8 @@ export default class DbPs extends Command {
44
39
  continue;
45
40
  }
46
41
  const type = runtime.kind === 'builtin' ? 'builtin' : runtime.status;
47
- const status = runtime.kind === 'builtin'
48
- ? await builtinDbStatus(runtime.containerName)
49
- : runtime.status;
50
- rows.push([
51
- runtime.envName,
52
- type,
53
- runtime.dbDialect,
54
- status,
55
- runtime.address,
56
- ]);
42
+ const status = runtime.kind === 'builtin' ? await builtinDbStatus(runtime.containerName) : runtime.status;
43
+ rows.push([runtime.envName, type, runtime.dbDialect, status, runtime.address]);
57
44
  }
58
45
  this.log(renderTable(['Env', 'Type', 'Dialect', 'Status', 'Address'], rows));
59
46
  }
@@ -23,7 +23,7 @@ function resolveExplicitAuthType(value) {
23
23
  return value === 'basic' || value === 'token' || value === 'oauth' ? value : undefined;
24
24
  }
25
25
  function formatMissingEnvMessage(envName) {
26
- return [`Env "${envName}" is not configured.`, `Run \`nb env add ${envName} --api-base-url <url>\` first.`].join('\n');
26
+ return [`Env "${envName}" is not configured.`, `Run \`nb init --ui --env ${envName}\` first.`].join('\n');
27
27
  }
28
28
  export default class EnvAuth extends Command {
29
29
  static summary = 'Authenticate a saved NocoBase environment with basic login, a token, or OAuth';
@@ -9,13 +9,12 @@
9
9
  import { Command } from '@oclif/core';
10
10
  import { getCurrentEnvName, listEnvs, resolveConfiguredAuthType } from '../../lib/auth-store.js';
11
11
  import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
+ import { translateCli } from '../../lib/cli-locale.js';
12
13
  import { renderTable } from '../../lib/ui.js';
13
14
  import { resolveApiBaseUrl } from './shared.js';
14
15
  export default class EnvList extends Command {
15
16
  static summary = 'List configured environments';
16
- static examples = [
17
- '<%= config.bin %> <%= command.id %>',
18
- ];
17
+ static examples = ['<%= config.bin %> <%= command.id %>'];
19
18
  async run() {
20
19
  await this.parse(EnvList);
21
20
  const scope = resolveDefaultConfigScope();
@@ -23,8 +22,8 @@ export default class EnvList extends Command {
23
22
  const currentEnv = await getCurrentEnvName({ scope });
24
23
  const names = Object.keys(envs).sort();
25
24
  if (!names.length) {
26
- this.log('No envs configured.');
27
- this.log('Run `nb env add <name> --api-base-url <url>` to add one.');
25
+ this.log(translateCli('commands.env.messages.noEnvsConfigured'));
26
+ this.log(translateCli('commands.env.messages.noEnvsConfiguredHelp'));
28
27
  return;
29
28
  }
30
29
  const rows = [];
@@ -10,6 +10,7 @@ import { Args, Command, Flags } from '@oclif/core';
10
10
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime } from '../../lib/app-runtime.js';
11
11
  import { getCurrentEnvName, listEnvs } from '../../lib/auth-store.js';
12
12
  import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
13
+ import { translateCli } from '../../lib/cli-locale.js';
13
14
  import { renderTable } from '../../lib/ui.js';
14
15
  import { apiStatus, runtimeStatus } from './shared.js';
15
16
  export default class EnvStatus extends Command {
@@ -46,13 +47,11 @@ export default class EnvStatus extends Command {
46
47
  const { envs } = await listEnvs({ scope });
47
48
  const configuredEnvNames = Object.keys(envs).sort();
48
49
  if (!configuredEnvNames.length) {
49
- this.log('No envs configured.');
50
- this.log('Run `nb env add <name> --api-base-url <url>` to add one.');
50
+ this.log(translateCli('commands.env.messages.noEnvsConfigured'));
51
+ this.log(translateCli('commands.env.messages.noEnvsConfiguredHelp'));
51
52
  return;
52
53
  }
53
- const envNames = flags.all
54
- ? configuredEnvNames
55
- : [requestedEnv || (await getCurrentEnvName({ scope }))];
54
+ const envNames = flags.all ? configuredEnvNames : [requestedEnv || (await getCurrentEnvName({ scope }))];
56
55
  const rows = [];
57
56
  for (const envName of envNames) {
58
57
  const runtime = await resolveManagedAppRuntime(envName);
@@ -76,8 +75,12 @@ export default class EnvStatus extends Command {
76
75
  rows.push({
77
76
  env: runtime.envName,
78
77
  status,
79
- apiBaseUrl: runtime.env.apiBaseUrl
80
- || String(runtime.env.config.apiBaseUrl ?? runtime.env.config.baseUrl ?? envs[envName]?.apiBaseUrl ?? envs[envName]?.baseUrl ?? '').trim(),
78
+ apiBaseUrl: runtime.env.apiBaseUrl ||
79
+ String(runtime.env.config.apiBaseUrl ??
80
+ runtime.env.config.baseUrl ??
81
+ envs[envName]?.apiBaseUrl ??
82
+ envs[envName]?.baseUrl ??
83
+ '').trim(),
81
84
  });
82
85
  }
83
86
  if (flags['json-output']) {
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
10
  import pc from 'picocolors';
11
+ import crypto from 'node:crypto';
11
12
  import { existsSync } from 'node:fs';
12
13
  import path from 'node:path';
13
14
  import { stdin as stdinStream, stdout as stdoutStream } from 'node:process';
@@ -84,6 +85,16 @@ function explicitApiBaseUrlFlag(flags) {
84
85
  function explicitDbHostFlag(flags) {
85
86
  return String(flags['db-host'] ?? '').trim();
86
87
  }
88
+ function optionalInitString(value) {
89
+ const text = String(value ?? '').trim();
90
+ return text || undefined;
91
+ }
92
+ function resolveManagedAppKey(value) {
93
+ return optionalInitString(value) ?? crypto.randomBytes(32).toString('hex');
94
+ }
95
+ function resolveManagedTimeZone(value) {
96
+ return optionalInitString(value) ?? (Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC');
97
+ }
87
98
  function shouldAllowExistingInitEnv() {
88
99
  return argvHasToken(process.argv.slice(2), ['--force', '-f']);
89
100
  }
@@ -803,6 +814,7 @@ Prompt modes:
803
814
  }
804
815
  async persistManagedEnvConfig(results, flags = {}) {
805
816
  const envName = String(results.appName ?? DEFAULT_INIT_APP_NAME).trim() || DEFAULT_INIT_APP_NAME;
817
+ const existingEnv = await getEnv(envName, { scope: resolveDefaultConfigScope() });
806
818
  const appPort = String(results.appPort ?? '').trim();
807
819
  const source = String(results.source ?? '').trim();
808
820
  const version = resolveInitDownloadVersion(results);
@@ -826,11 +838,15 @@ Prompt modes:
826
838
  const authUsername = authType === 'basic' ? String(results.username ?? results.rootUsername ?? '').trim() : '';
827
839
  const accessToken = String(results.accessToken ?? '');
828
840
  const skipDownload = results.skipDownload === true;
841
+ const appKey = resolveManagedAppKey(results.appKey ?? existingEnv?.config.appKey);
842
+ const timeZone = resolveManagedTimeZone(results.timeZone ?? existingEnv?.config.timezone);
829
843
  const builtinDb = explicitDbHostFlag(flags)
830
844
  ? false
831
845
  : results.builtinDb === undefined
832
846
  ? undefined
833
847
  : Boolean(results.builtinDb);
848
+ results.appKey = appKey;
849
+ results.timeZone = timeZone;
834
850
  await upsertEnv(envName, {
835
851
  ...(source === 'docker'
836
852
  ? { kind: 'docker' }
@@ -852,6 +868,8 @@ Prompt modes:
852
868
  ...(appRootPath ? { appRootPath } : {}),
853
869
  ...(storagePath ? { storagePath } : {}),
854
870
  ...(appPort ? { appPort } : {}),
871
+ ...(appKey ? { appKey } : {}),
872
+ ...(timeZone ? { timezone: timeZone } : {}),
855
873
  ...(!skipDownload && results.devDependencies !== undefined
856
874
  ? { devDependencies: Boolean(results.devDependencies) }
857
875
  : {}),
@@ -26,6 +26,7 @@ import { omitKeys, upperFirst } from "../lib/object-utils.js";
26
26
  import { getEnv, setCurrentEnv, upsertEnv } from '../lib/auth-store.js';
27
27
  import { buildStoredEnvConfig } from '../lib/env-config.js';
28
28
  import { resolveDockerEnvFileArg } from "../lib/docker-env-file.js";
29
+ import { startDockerLogFollower } from '../lib/docker-log-stream.js';
29
30
  import Download, { defaultDockerRegistryForLang } from './download.js';
30
31
  import EnvAdd from "./env/add.js";
31
32
  const DEFAULT_INSTALL_ENV_NAME = 'local';
@@ -842,6 +843,18 @@ export default class Install extends Command {
842
843
  const text = String(value).trim();
843
844
  return text || undefined;
844
845
  }
846
+ static resolveManagedAppKey(value) {
847
+ return Install.toOptionalPromptString(value) ?? crypto.randomBytes(32).toString('hex');
848
+ }
849
+ static resolveManagedTimeZone(value) {
850
+ return Install.toOptionalPromptString(value) ?? (Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC');
851
+ }
852
+ async ensureManagedAppRuntimeConfig(params) {
853
+ const savedEnv = await getEnv(params.envName, { scope: resolveDefaultConfigScope() });
854
+ const savedConfig = savedEnv?.config;
855
+ params.appResults.appKey = Install.resolveManagedAppKey(params.appResults.appKey ?? savedConfig?.appKey);
856
+ params.appResults.timeZone = Install.resolveManagedTimeZone(params.appResults.timeZone ?? savedConfig?.timezone);
857
+ }
845
858
  static async validateAppPort(value, values) {
846
859
  const formatError = validateTcpPort(value);
847
860
  if (formatError) {
@@ -1112,7 +1125,7 @@ export default class Install extends Command {
1112
1125
  throw new Error([
1113
1126
  `Cannot continue setup for "${env.name}" in non-interactive resume mode yet.`,
1114
1127
  `These setup-only flags are not saved in the env config: ${missingFlags.join(', ')}`,
1115
- `Run \`nb init --env ${env.name} --resume\` without \`--yes\`, or pass those flags again.`,
1128
+ `Run \`nb init --ui --env ${env.name} --resume\` without \`--yes\`, or pass those flags again.`,
1116
1129
  ].join('\n'));
1117
1130
  }
1118
1131
  }
@@ -1664,8 +1677,8 @@ export default class Install extends Command {
1664
1677
  const dbSchema = optionalEnvString(params.dbResults.dbSchema);
1665
1678
  const dbTablePrefix = optionalEnvString(params.dbResults.dbTablePrefix);
1666
1679
  const dbUnderscored = optionalEnvBoolean(params.dbResults.dbUnderscored);
1667
- const appKey = crypto.randomBytes(32).toString('hex');
1668
- const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
1680
+ const appKey = Install.resolveManagedAppKey(params.appResults.appKey);
1681
+ const timeZone = Install.resolveManagedTimeZone(params.appResults.timeZone);
1669
1682
  const containerName = Install.buildDockerAppContainerName(params.envName, params.dockerContainerPrefix ?? params.workspaceName);
1670
1683
  const configuredEnvFile = String(params.appResults.envFile ?? '').trim();
1671
1684
  const envFile = await resolveDockerEnvFileArg(params.envName, configuredEnvFile ? { envFile: configuredEnvFile } : undefined);
@@ -1888,8 +1901,8 @@ export default class Install extends Command {
1888
1901
  const storagePath = resolveConfiguredEnvPath(configuredStoragePath) ??
1889
1902
  resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1890
1903
  const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
1891
- const appKey = crypto.randomBytes(32).toString('hex');
1892
- const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
1904
+ const appKey = Install.resolveManagedAppKey(params.appResults.appKey);
1905
+ const timeZone = Install.resolveManagedTimeZone(params.appResults.timeZone);
1893
1906
  const env = {
1894
1907
  STORAGE_PATH: storagePath,
1895
1908
  APP_PORT: String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT,
@@ -2010,32 +2023,38 @@ export default class Install extends Command {
2010
2023
  let lastMessage = 'No response yet';
2011
2024
  let lastLoggedStatus = '';
2012
2025
  printInfo('Waiting for NocoBase to become ready...');
2013
- while (Date.now() - startedAt < timeoutMs) {
2014
- const result = await Install.requestAppHealthCheck({
2015
- healthCheckUrl,
2016
- fetchImpl,
2017
- requestTimeoutMs,
2018
- });
2019
- if (result.ok) {
2020
- return;
2021
- }
2022
- lastMessage = result.message;
2023
- const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
2024
- const statusLine = `Waiting for NocoBase to become ready... (${elapsedSeconds}s elapsed, last status: ${Install.formatHealthCheckMessage(lastMessage)})`;
2025
- if (statusLine !== lastLoggedStatus) {
2026
- printInfo(statusLine);
2027
- lastLoggedStatus = statusLine;
2028
- }
2029
- const remainingMs = timeoutMs - (Date.now() - startedAt);
2030
- if (remainingMs <= 0) {
2031
- break;
2026
+ const dockerLogFollower = options?.verbose && options.containerName ? startDockerLogFollower(options.containerName) : undefined;
2027
+ try {
2028
+ while (Date.now() - startedAt < timeoutMs) {
2029
+ const result = await Install.requestAppHealthCheck({
2030
+ healthCheckUrl,
2031
+ fetchImpl,
2032
+ requestTimeoutMs,
2033
+ });
2034
+ if (result.ok) {
2035
+ return;
2036
+ }
2037
+ lastMessage = result.message;
2038
+ const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
2039
+ const statusLine = `Waiting for NocoBase to become ready... (${elapsedSeconds}s elapsed, last status: ${Install.formatHealthCheckMessage(lastMessage)})`;
2040
+ if (statusLine !== lastLoggedStatus) {
2041
+ printInfo(statusLine);
2042
+ lastLoggedStatus = statusLine;
2043
+ }
2044
+ const remainingMs = timeoutMs - (Date.now() - startedAt);
2045
+ if (remainingMs <= 0) {
2046
+ break;
2047
+ }
2048
+ await Install.sleep(Math.min(intervalMs, remainingMs));
2032
2049
  }
2033
- await Install.sleep(Math.min(intervalMs, remainingMs));
2050
+ const logHint = options?.containerName
2051
+ ? ` You can inspect startup logs with: docker logs ${options.containerName}`
2052
+ : '';
2053
+ throw new Error(`The application did not become ready in time. Expected \`${healthCheckUrl}\` to respond with \`ok\`, but the last status was: ${Install.formatHealthCheckMessage(lastMessage)}.${logHint}`);
2054
+ }
2055
+ finally {
2056
+ await dockerLogFollower?.stop();
2034
2057
  }
2035
- const logHint = options?.containerName
2036
- ? ` You can inspect startup logs with: docker logs ${options.containerName}`
2037
- : '';
2038
- throw new Error(`The application did not become ready in time. Expected \`${healthCheckUrl}\` to respond with \`ok\`, but the last status was: ${Install.formatHealthCheckMessage(lastMessage)}.${logHint}`);
2039
2058
  }
2040
2059
  async saveInstalledEnv(params) {
2041
2060
  await upsertEnv(params.envName, Install.buildSavedEnvConfig(params), { scope: resolveDefaultConfigScope() });
@@ -2273,6 +2292,10 @@ export default class Install extends Command {
2273
2292
  }
2274
2293
  const promptResults = await this.collectPromptResults(parsed, flags.yes);
2275
2294
  const { envName, appResults, downloadResults, dbResults, rootResults, envAddResults } = promptResults;
2295
+ await this.ensureManagedAppRuntimeConfig({
2296
+ envName,
2297
+ appResults,
2298
+ });
2276
2299
  const source = String(downloadResultsValue(downloadResults, 'source') ?? '').trim();
2277
2300
  const usesDockerResources = Boolean(dbResults.builtinDb) || source === 'docker';
2278
2301
  const dockerNetworkName = usesDockerResources
@@ -2392,6 +2415,7 @@ export default class Install extends Command {
2392
2415
  envAddResults,
2393
2416
  }), {
2394
2417
  containerName: dockerAppPlan?.containerName,
2418
+ verbose: parsed.verbose,
2395
2419
  });
2396
2420
  printInfo(`NocoBase is ready at http://127.0.0.1:${dockerAppPlan?.appPort ?? localAppPlan?.appPort}`);
2397
2421
  }
@@ -8,14 +8,29 @@
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
10
  import { confirm } from "../../lib/inquirer.js";
11
+ import { updateNocoBaseSkills } from '../../lib/skills-manager.js';
11
12
  import { setVerboseMode } from '../../lib/ui.js';
12
13
  import { formatSelfUpdateUnavailableMessage, formatUnsupportedSelfUpdateMessage, inspectSelfStatus, updateSelf, } from '../../lib/self-manager.js';
14
+ function formatSkillsUpdateMessage(result, verbose) {
15
+ if (result.action === 'noop') {
16
+ if (result.reason === 'not-installed') {
17
+ return verbose
18
+ ? 'NocoBase AI coding skills are not installed globally. Run `nb skills install` first.'
19
+ : 'Skipped skills update because NocoBase AI coding skills are not installed.';
20
+ }
21
+ return verbose
22
+ ? 'NocoBase AI coding skills are already up to date globally.'
23
+ : 'NocoBase AI coding skills are up to date.';
24
+ }
25
+ return verbose ? 'Updated the global NocoBase AI coding skills.' : 'Updated NocoBase AI coding skills globally.';
26
+ }
13
27
  export default class SelfUpdate extends Command {
14
28
  static summary = 'Update the globally installed NocoBase CLI';
15
29
  static description = 'Update the current NocoBase CLI install when it is managed by a standard global npm install.';
16
30
  static examples = [
17
31
  '<%= config.bin %> <%= command.id %>',
18
32
  '<%= config.bin %> <%= command.id %> --yes',
33
+ '<%= config.bin %> <%= command.id %> --skills',
19
34
  '<%= config.bin %> <%= command.id %> --channel alpha --json',
20
35
  ];
21
36
  static flags = {
@@ -33,6 +48,10 @@ export default class SelfUpdate extends Command {
33
48
  description: 'Output the result as JSON',
34
49
  default: false,
35
50
  }),
51
+ skills: Flags.boolean({
52
+ description: 'Also update the globally installed NocoBase AI coding skills',
53
+ default: false,
54
+ }),
36
55
  verbose: Flags.boolean({
37
56
  description: 'Show detailed update output',
38
57
  default: false,
@@ -50,11 +69,14 @@ export default class SelfUpdate extends Command {
50
69
  if (!status.latestVersion && status.registryError) {
51
70
  this.error(formatSelfUpdateUnavailableMessage(status));
52
71
  }
72
+ let shouldUpdateSkills = flags.skills;
53
73
  if (!flags.yes && status.updateAvailable) {
54
74
  let confirmed = false;
55
75
  try {
56
76
  confirmed = await confirm({
57
- message: `Update ${status.packageName} from ${status.currentVersion} to ${status.latestVersion}?`,
77
+ message: flags.skills
78
+ ? `Update ${status.packageName} from ${status.currentVersion} to ${status.latestVersion} and refresh the globally installed NocoBase AI coding skills?`
79
+ : `Update ${status.packageName} from ${status.currentVersion} to ${status.latestVersion}?`,
58
80
  default: false,
59
81
  });
60
82
  }
@@ -69,6 +91,24 @@ export default class SelfUpdate extends Command {
69
91
  channel: flags.channel,
70
92
  verbose: flags.verbose,
71
93
  });
94
+ if (flags.skills && !flags.yes && !status.updateAvailable) {
95
+ let confirmed = false;
96
+ try {
97
+ confirmed = await confirm({
98
+ message: 'Update the globally installed NocoBase AI coding skills?',
99
+ default: true,
100
+ });
101
+ }
102
+ catch {
103
+ return;
104
+ }
105
+ shouldUpdateSkills = confirmed;
106
+ }
107
+ const skillsResult = shouldUpdateSkills
108
+ ? await updateNocoBaseSkills({
109
+ verbose: flags.verbose,
110
+ })
111
+ : undefined;
72
112
  if (flags.json) {
73
113
  this.log(JSON.stringify({
74
114
  ok: true,
@@ -79,6 +119,17 @@ export default class SelfUpdate extends Command {
79
119
  channel: result.status.channel,
80
120
  fromVersion: result.status.currentVersion,
81
121
  toVersion: result.targetVersion,
122
+ skills: skillsResult
123
+ ? {
124
+ action: skillsResult.action,
125
+ reason: skillsResult.action === 'noop' ? skillsResult.reason : undefined,
126
+ globalRoot: skillsResult.status.globalRoot,
127
+ workspaceRoot: skillsResult.status.workspaceRoot,
128
+ installedSkillNames: skillsResult.status.installedSkillNames,
129
+ installedVersion: skillsResult.status.installedVersion,
130
+ installedRef: skillsResult.status.installedRef,
131
+ }
132
+ : undefined,
82
133
  }, null, 2));
83
134
  return;
84
135
  }
@@ -86,10 +137,16 @@ export default class SelfUpdate extends Command {
86
137
  this.log(flags.verbose
87
138
  ? `NocoBase CLI is already up to date at ${result.status.currentVersion}.`
88
139
  : `NocoBase CLI is up to date: ${result.status.currentVersion}.`);
140
+ if (skillsResult) {
141
+ this.log(formatSkillsUpdateMessage(skillsResult, flags.verbose));
142
+ }
89
143
  return;
90
144
  }
91
145
  this.log(flags.verbose
92
146
  ? `Updated NocoBase CLI from ${result.status.currentVersion} using ${result.packageSpec}${result.targetVersion ? ` (latest ${result.status.channel} resolves to ${result.targetVersion})` : ''}.`
93
147
  : `Updated NocoBase CLI: ${result.status.currentVersion} -> ${result.targetVersion}.`);
148
+ if (skillsResult) {
149
+ this.log(formatSkillsUpdateMessage(skillsResult, flags.verbose));
150
+ }
94
151
  }
95
152
  }