@nocobase/cli 2.1.0-beta.41 → 2.1.0-beta.42-test.1

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']) {
@@ -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';
@@ -1112,7 +1113,7 @@ export default class Install extends Command {
1112
1113
  throw new Error([
1113
1114
  `Cannot continue setup for "${env.name}" in non-interactive resume mode yet.`,
1114
1115
  `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.`,
1116
+ `Run \`nb init --ui --env ${env.name} --resume\` without \`--yes\`, or pass those flags again.`,
1116
1117
  ].join('\n'));
1117
1118
  }
1118
1119
  }
@@ -2010,32 +2011,38 @@ export default class Install extends Command {
2010
2011
  let lastMessage = 'No response yet';
2011
2012
  let lastLoggedStatus = '';
2012
2013
  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;
2014
+ const dockerLogFollower = options?.verbose && options.containerName ? startDockerLogFollower(options.containerName) : undefined;
2015
+ try {
2016
+ while (Date.now() - startedAt < timeoutMs) {
2017
+ const result = await Install.requestAppHealthCheck({
2018
+ healthCheckUrl,
2019
+ fetchImpl,
2020
+ requestTimeoutMs,
2021
+ });
2022
+ if (result.ok) {
2023
+ return;
2024
+ }
2025
+ lastMessage = result.message;
2026
+ const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
2027
+ const statusLine = `Waiting for NocoBase to become ready... (${elapsedSeconds}s elapsed, last status: ${Install.formatHealthCheckMessage(lastMessage)})`;
2028
+ if (statusLine !== lastLoggedStatus) {
2029
+ printInfo(statusLine);
2030
+ lastLoggedStatus = statusLine;
2031
+ }
2032
+ const remainingMs = timeoutMs - (Date.now() - startedAt);
2033
+ if (remainingMs <= 0) {
2034
+ break;
2035
+ }
2036
+ await Install.sleep(Math.min(intervalMs, remainingMs));
2032
2037
  }
2033
- await Install.sleep(Math.min(intervalMs, remainingMs));
2038
+ const logHint = options?.containerName
2039
+ ? ` You can inspect startup logs with: docker logs ${options.containerName}`
2040
+ : '';
2041
+ 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}`);
2042
+ }
2043
+ finally {
2044
+ await dockerLogFollower?.stop();
2034
2045
  }
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
2046
  }
2040
2047
  async saveInstalledEnv(params) {
2041
2048
  await upsertEnv(params.envName, Install.buildSavedEnvConfig(params), { scope: resolveDefaultConfigScope() });
@@ -2392,6 +2399,7 @@ export default class Install extends Command {
2392
2399
  envAddResults,
2393
2400
  }), {
2394
2401
  containerName: dockerAppPlan?.containerName,
2402
+ verbose: parsed.verbose,
2395
2403
  });
2396
2404
  printInfo(`NocoBase is ready at http://127.0.0.1:${dockerAppPlan?.appPort ?? localAppPlan?.appPort}`);
2397
2405
  }
@@ -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
  }
@@ -16,20 +16,20 @@ function formatUnsupportedRuntimeMessage(kind, envName) {
16
16
  return [
17
17
  `Can't run dev mode for "${envName}".`,
18
18
  'This env is managed by Docker, but `nb source dev` requires a local npm or Git source directory.',
19
- `Use \`nb app logs --env ${envName}\` to inspect the Docker app, or create a source-based env with \`nb init --env ${envName} --source git\`.`,
19
+ `Use \`nb app logs --env ${envName}\` to inspect the Docker app, or create a source-based env with \`nb init --ui --env ${envName} --source git\`.`,
20
20
  ].join('\n');
21
21
  }
22
22
  if (kind === 'ssh') {
23
23
  return [
24
24
  `Can't run dev mode for "${envName}" yet.`,
25
25
  'SSH env support is reserved but not implemented yet.',
26
- `Create a source-based env with \`nb init --env ${envName} --source git\` if you want local development mode right now.`,
26
+ `Create a source-based env with \`nb init --ui --env ${envName} --source git\` if you want local development mode right now.`,
27
27
  ].join('\n');
28
28
  }
29
29
  return [
30
30
  `Can't run dev mode for "${envName}".`,
31
31
  'This env only has an API connection, but `nb source dev` requires a local npm or Git source directory.',
32
- `Create a source-based env with \`nb init --env ${envName} --source git\` if you want local development mode.`,
32
+ `Create a source-based env with \`nb init --ui --env ${envName} --source git\` if you want local development mode.`,
33
33
  ].join('\n');
34
34
  }
35
35
  function appUrlForPort(port) {
@@ -129,8 +129,8 @@ export default class SourceDev extends Command {
129
129
  this.error(formatUnsupportedRuntimeMessage(runtime.kind, runtime.envName));
130
130
  }
131
131
  announceTargetEnv(runtime.envName);
132
- const devPort = flags.port
133
- || (runtime.env.appPort !== undefined && runtime.env.appPort !== null
132
+ const devPort = flags.port ||
133
+ (runtime.env.appPort !== undefined && runtime.env.appPort !== null
134
134
  ? String(runtime.env.appPort).trim()
135
135
  : undefined);
136
136
  const appUrl = appUrlForPort(devPort);
@@ -6,6 +6,7 @@
6
6
  * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
+ import { startDockerLogFollower } from './docker-log-stream.js';
9
10
  import { printInfo } from './ui.js';
10
11
  const APP_HEALTH_CHECK_INTERVAL_MS = 2_000;
11
12
  const APP_HEALTH_CHECK_TIMEOUT_MS = 600_000;
@@ -99,34 +100,40 @@ export async function waitForAppReady(params) {
99
100
  let lastMessage = 'No response yet';
100
101
  let nextProgressLogAt = startedAt + progressLogIntervalMs;
101
102
  printInfo(`Waiting for NocoBase to become ready for "${params.envName}"...`);
102
- while (Date.now() - startedAt < timeoutMs) {
103
- const result = await requestAppHealthCheck({
104
- healthCheckUrl,
105
- fetchImpl: params.fetchImpl,
106
- requestTimeoutMs: params.requestTimeoutMs,
107
- });
108
- if (result.ok) {
109
- return;
110
- }
111
- lastMessage = result.message;
112
- const now = Date.now();
113
- if (now >= nextProgressLogAt) {
114
- const elapsedSeconds = Math.max(1, Math.floor((now - startedAt) / 1000));
115
- printInfo(`Still waiting for "${params.envName}"... (${elapsedSeconds}s elapsed)`);
116
- while (nextProgressLogAt <= now) {
117
- nextProgressLogAt += progressLogIntervalMs;
103
+ const dockerLogFollower = params.verbose && params.containerName ? startDockerLogFollower(params.containerName) : undefined;
104
+ try {
105
+ while (Date.now() - startedAt < timeoutMs) {
106
+ const result = await requestAppHealthCheck({
107
+ healthCheckUrl,
108
+ fetchImpl: params.fetchImpl,
109
+ requestTimeoutMs: params.requestTimeoutMs,
110
+ });
111
+ if (result.ok) {
112
+ return;
118
113
  }
114
+ lastMessage = result.message;
115
+ const now = Date.now();
116
+ if (now >= nextProgressLogAt) {
117
+ const elapsedSeconds = Math.max(1, Math.floor((now - startedAt) / 1000));
118
+ printInfo(`Still waiting for "${params.envName}"... (${elapsedSeconds}s elapsed)`);
119
+ while (nextProgressLogAt <= now) {
120
+ nextProgressLogAt += progressLogIntervalMs;
121
+ }
122
+ }
123
+ const remainingMs = timeoutMs - (Date.now() - startedAt);
124
+ if (remainingMs <= 0) {
125
+ break;
126
+ }
127
+ await sleep(Math.min(intervalMs, remainingMs));
119
128
  }
120
- const remainingMs = timeoutMs - (Date.now() - startedAt);
121
- if (remainingMs <= 0) {
122
- break;
123
- }
124
- await sleep(Math.min(intervalMs, remainingMs));
129
+ const hints = [
130
+ params.logHint,
131
+ params.containerName ? `docker logs ${params.containerName}` : undefined,
132
+ ].filter(Boolean);
133
+ const hintText = hints.length > 0 ? ` ${hints.join(' ')}` : '';
134
+ throw new AppHealthCheckError(`NocoBase did not become ready in time for "${params.envName}". Expected \`${healthCheckUrl}\` to respond with \`ok\`, but the last status was: ${lastMessage}.${hintText}`);
135
+ }
136
+ finally {
137
+ await dockerLogFollower?.stop();
125
138
  }
126
- const hints = [
127
- params.logHint,
128
- params.containerName ? `docker logs ${params.containerName}` : undefined,
129
- ].filter(Boolean);
130
- const hintText = hints.length > 0 ? ` ${hints.join(' ')}` : '';
131
- throw new AppHealthCheckError(`NocoBase did not become ready in time for "${params.envName}". Expected \`${healthCheckUrl}\` to respond with \`ok\`, but the last status was: ${lastMessage}.${hintText}`);
132
139
  }
@@ -82,7 +82,7 @@ function formatSavedDockerSettingsIncomplete(envName, missing) {
82
82
  return [
83
83
  `Can't start NocoBase for "${envName}" yet.`,
84
84
  `The saved Docker settings for this env are incomplete. Missing: ${missing.join(', ')}.`,
85
- 'Re-run `nb init` or `nb env add` to refresh this env config, then try again.',
85
+ `Re-run \`nb init --ui --env ${envName}\` to refresh this env config, then try again.`,
86
86
  ].join('\n');
87
87
  }
88
88
  function formatDockerAppRecreateFailure(envName, message) {