@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.
@@ -110,10 +110,10 @@ export function formatMissingManagedAppEnvMessage(envName) {
110
110
  if (requested) {
111
111
  return [
112
112
  `Env "${requested}" is not configured in this workspace.`,
113
- `If you want to create a new NocoBase AI environment, run \`nb init --env ${requested}\` first.`,
113
+ `If you want to create a new NocoBase AI environment, run \`nb init --ui --env ${requested}\` first.`,
114
114
  ].join('\n');
115
115
  }
116
- return 'No NocoBase env is configured yet. Run `nb init` to create one first.';
116
+ return 'No NocoBase env is configured yet. Run `nb init --ui` to create one first.';
117
117
  }
118
118
  export async function runLocalNocoBaseCommand(runtime, args, options) {
119
119
  const envVars = await buildRuntimeEnvVars(runtime);
@@ -32,6 +32,13 @@ function normalizeOptionalCliLocale(value) {
32
32
  }
33
33
  return normalizeCliLocale(normalized);
34
34
  }
35
+ function normalizeOptionalCliUpdatePolicy(value) {
36
+ const normalized = normalizeOptionalString(value);
37
+ if (normalized === 'prompt' || normalized === 'auto' || normalized === 'off') {
38
+ return normalized;
39
+ }
40
+ return undefined;
41
+ }
35
42
  export function readEnvApiBaseUrl(config) {
36
43
  if (!config) {
37
44
  return undefined;
@@ -79,10 +86,12 @@ function normalizeEnvConfigEntry(entry) {
79
86
  function normalizeAuthConfig(config) {
80
87
  const settings = config.settings ?? {};
81
88
  const locale = normalizeOptionalCliLocale(settings.locale);
89
+ const updatePolicy = normalizeOptionalCliUpdatePolicy(settings.update?.policy);
82
90
  return {
83
91
  name: config.name || config.dockerResourcePrefix,
84
92
  settings: {
85
93
  ...(locale ? { locale } : {}),
94
+ ...(updatePolicy ? { update: { policy: updatePolicy } } : {}),
86
95
  ...(settings.license?.pkgUrl ? { license: { pkgUrl: normalizeOptionalString(settings.license.pkgUrl) } } : {}),
87
96
  ...(settings.docker?.network || settings.docker?.containerPrefix
88
97
  ? {
@@ -62,14 +62,14 @@ export async function resolveBackupTargetEnv(requestedEnv) {
62
62
  const { envs } = await listEnvs({ scope });
63
63
  const configuredEnvNames = Object.keys(envs);
64
64
  if (!configuredEnvNames.length) {
65
- throw new Error('No env is configured. Run `nb env add <name> --api-base-url <url>` first.');
65
+ throw new Error('No env is configured. Run `nb init --ui` first.');
66
66
  }
67
67
  if (requestedEnv?.trim()) {
68
- throw new Error(`Env "${envName}" is not configured. Run \`nb env add ${envName} --api-base-url <url>\` first.`);
68
+ throw new Error(`Env "${envName}" is not configured. Run \`nb init --ui --env ${envName}\` first.`);
69
69
  }
70
70
  throw new Error([
71
71
  `Current env "${envName}" is not configured.`,
72
- 'Switch to an existing env with `nb env use <name>`, or add one with `nb env add <name> --api-base-url <url>`.',
72
+ `Switch to an existing env with \`nb env use <name>\`, or run \`nb init --ui --env ${envName}\` to create or connect it.`,
73
73
  ].join('\n'));
74
74
  }
75
75
  export async function ensureBackupRuntimeCommands(params) {
@@ -254,25 +254,31 @@ export function formatSwaggerSchemaError(response, context) {
254
254
  })
255
255
  .join('\n');
256
256
  const envLabel = context.envName ? ` for env "${context.envName}"` : '';
257
+ const tokenHint = context.envName
258
+ ? `Update the API key with \`nb env update ${context.envName} --token <api-key>\`, log in with \`nb env auth ${context.envName}\`, or rerun the command with \`--access-token <api-key>\`.`
259
+ : 'Update the API key with `nb env update <name> --token <api-key>`, log in with `nb env auth <name>`, or rerun the command with `--access-token <api-key>`.';
257
260
  const commandHint = context.commandToken
258
- ? `If \`${context.commandToken}\` is a runtime command, refresh the runtime after updating the token with \`nb env update\`. If it is a typo, run \`nb --help\` to inspect available commands.`
259
- : 'Run `nb --help` to inspect built-in commands, then refresh runtime commands with `nb env update` after updating the token.';
261
+ ? `If \`${context.commandToken}\` is a runtime command, retry it after updating the token. If it is a typo, run \`nb --help\` to inspect available commands.`
262
+ : 'Run `nb --help` to inspect built-in commands, then retry the original command after updating the token.';
260
263
  return [
261
264
  `Authentication failed while loading the command runtime from \`swagger:get\`${envLabel}.`,
262
265
  `Base URL: ${context.baseUrl}`,
263
266
  details,
264
- 'Update the API key with `nb env add <name> --api-base-url <url> --auth-type token --access-token <api-key>`, log in with `nb env auth <name>`, or rerun the command with `--access-token <api-key>`.',
267
+ tokenHint,
265
268
  commandHint,
266
269
  ].join('\n');
267
270
  }
268
271
  if (isNetworkFetchFailure(response)) {
269
272
  const rawMessage = response.data?.error?.message || 'fetch failed';
273
+ const updateConnectionHint = context.envName
274
+ ? `If you recently changed the server address, update env "${context.envName}" with \`nb env update ${context.envName} --api-base-url <url>\` and retry the original command.`
275
+ : 'If you recently changed the server address, update the saved env connection with `nb env update <name> --api-base-url <url>` and retry the original command.';
270
276
  return [
271
277
  'Failed to reach the NocoBase server while loading the command runtime from `swagger:get`.',
272
278
  `Base URL: ${context.baseUrl}`,
273
279
  `Network error: ${rawMessage}`,
274
280
  'Check that the NocoBase app is running, the base URL is correct, and the server is reachable from this machine.',
275
- 'If you recently changed the server address, update it with `nb env add <name> --api-base-url <url>` and retry `nb env update`.',
281
+ updateConnectionHint,
276
282
  'Use `nb env list` to inspect the current env configuration.',
277
283
  ].join('\n');
278
284
  }
@@ -282,7 +288,7 @@ export function formatMissingRuntimeEnvError(commandToken) {
282
288
  if (!commandToken) {
283
289
  return [
284
290
  'No env is configured for runtime commands.',
285
- 'Run `nb env add <name> --api-base-url <url>` first.',
291
+ 'Run `nb init --ui` first.',
286
292
  'If you configure multiple environments later, switch with `nb env use <name>`.',
287
293
  ].join('\n');
288
294
  }
@@ -290,7 +296,7 @@ export function formatMissingRuntimeEnvError(commandToken) {
290
296
  `Unable to resolve runtime command \`${commandToken}\`.`,
291
297
  'No env is configured, so the CLI cannot load runtime commands from `swagger:get`.',
292
298
  'If this is a built-in command or a typo, run `nb --help` to inspect available commands.',
293
- 'If this should be an application runtime command, run `nb env add <name> --api-base-url <url>` and then `nb env update`.',
299
+ 'If this should be an application runtime command, run `nb init --ui` to create or connect a NocoBase app first.',
294
300
  ].join('\n');
295
301
  }
296
302
  export async function ensureRuntimeFromArgv(argv, options) {
@@ -366,8 +372,8 @@ export async function updateEnvRuntime(options) {
366
372
  throw new Error([
367
373
  env
368
374
  ? `Env "${envName}" is missing a base URL.`
369
- : `Env "${envName}" is not configured. Run \`nb env add ${envName}\` first.`,
370
- env ? 'Update it with `nb env add <name> --api-base-url <url>` first.' : '',
375
+ : `Env "${envName}" is not configured. Run \`nb init --ui --env ${envName}\` first.`,
376
+ env ? `Update env "${envName}" with \`nb env update ${envName} --api-base-url <url>\` first.` : '',
371
377
  ]
372
378
  .filter(Boolean)
373
379
  .join('\n'));
@@ -15,8 +15,11 @@ export const DEFAULT_DOCKER_CONTAINER_PREFIX = 'nb';
15
15
  export const DEFAULT_DOCKER_BIN = 'docker';
16
16
  export const DEFAULT_GIT_BIN = 'git';
17
17
  export const DEFAULT_YARN_BIN = 'yarn';
18
+ export const CLI_UPDATE_POLICY_OPTIONS = ['prompt', 'auto', 'off'];
19
+ export const DEFAULT_UPDATE_POLICY = 'prompt';
18
20
  export const SUPPORTED_CLI_CONFIG_KEYS = [
19
21
  'locale',
22
+ 'update.policy',
20
23
  'license.pkg-url',
21
24
  'docker.network',
22
25
  'docker.container-prefix',
@@ -42,9 +45,17 @@ export function assertSupportedCliConfigKey(value) {
42
45
  }
43
46
  return value;
44
47
  }
48
+ export function normalizeCliUpdatePolicy(value) {
49
+ const normalized = trimValue(value);
50
+ if (!normalized) {
51
+ return undefined;
52
+ }
53
+ return CLI_UPDATE_POLICY_OPTIONS.includes(normalized) ? normalized : undefined;
54
+ }
45
55
  function cloneSettings(config) {
46
56
  return {
47
57
  ...(config.settings?.locale ? { locale: trimValue(config.settings.locale) } : {}),
58
+ update: config.settings?.update ? { ...config.settings.update } : undefined,
48
59
  license: config.settings?.license ? { ...config.settings.license } : undefined,
49
60
  docker: config.settings?.docker ? { ...config.settings.docker } : undefined,
50
61
  bin: config.settings?.bin ? { ...config.settings.bin } : undefined,
@@ -54,6 +65,10 @@ function pruneSettings(config) {
54
65
  if (config.settings && !trimValue(config.settings.locale)) {
55
66
  delete config.settings.locale;
56
67
  }
68
+ const update = config.settings?.update;
69
+ if (update && !normalizeCliUpdatePolicy(update.policy)) {
70
+ delete config.settings?.update;
71
+ }
57
72
  const license = config.settings?.license;
58
73
  if (license && !trimValue(license.pkgUrl)) {
59
74
  delete config.settings?.license;
@@ -68,6 +83,7 @@ function pruneSettings(config) {
68
83
  }
69
84
  if (config.settings &&
70
85
  !config.settings.locale &&
86
+ !config.settings.update &&
71
87
  !config.settings.license &&
72
88
  !config.settings.docker &&
73
89
  !config.settings.bin) {
@@ -78,6 +94,8 @@ export function getExplicitCliConfigValue(config, key) {
78
94
  switch (key) {
79
95
  case 'locale':
80
96
  return trimValue(config.settings?.locale);
97
+ case 'update.policy':
98
+ return normalizeCliUpdatePolicy(config.settings?.update?.policy);
81
99
  case 'license.pkg-url':
82
100
  return trimValue(config.settings?.license?.pkgUrl);
83
101
  case 'docker.network':
@@ -100,6 +118,8 @@ export function getEffectiveCliConfigValue(config, key) {
100
118
  switch (key) {
101
119
  case 'locale':
102
120
  return resolveCliLocale(undefined, { configuredLocale: trimValue(config.settings?.locale) });
121
+ case 'update.policy':
122
+ return explicit ?? DEFAULT_UPDATE_POLICY;
103
123
  case 'license.pkg-url':
104
124
  return DEFAULT_LICENSE_PKG_URL;
105
125
  case 'docker.network':
@@ -129,6 +149,13 @@ export function normalizeCliConfigValue(key, value) {
129
149
  }
130
150
  return locale;
131
151
  }
152
+ if (key === 'update.policy') {
153
+ const policy = normalizeCliUpdatePolicy(normalized);
154
+ if (!policy) {
155
+ throw new Error(`Config key "${key}" must be one of: ${CLI_UPDATE_POLICY_OPTIONS.join(', ')}`);
156
+ }
157
+ return policy;
158
+ }
132
159
  return normalized;
133
160
  }
134
161
  export async function loadCliConfig(options = {}) {
@@ -158,6 +185,12 @@ export async function setCliConfigValue(key, value, options = {}) {
158
185
  case 'locale':
159
186
  config.settings.locale = normalized;
160
187
  break;
188
+ case 'update.policy':
189
+ config.settings.update = {
190
+ ...(config.settings.update ?? {}),
191
+ policy: normalized,
192
+ };
193
+ break;
161
194
  case 'license.pkg-url':
162
195
  config.settings.license = {
163
196
  ...(config.settings.license ?? {}),
@@ -211,6 +244,11 @@ export async function deleteCliConfigValue(key, options = {}) {
211
244
  case 'locale':
212
245
  delete config.settings.locale;
213
246
  break;
247
+ case 'update.policy':
248
+ if (config.settings.update) {
249
+ delete config.settings.update.policy;
250
+ }
251
+ break;
214
252
  case 'license.pkg-url':
215
253
  if (config.settings.license) {
216
254
  delete config.settings.license.pkgUrl;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ export function getCommandPathTokens(argv) {
10
+ const tokens = [];
11
+ for (const token of argv) {
12
+ if (!token) {
13
+ continue;
14
+ }
15
+ if (token.startsWith('-')) {
16
+ break;
17
+ }
18
+ tokens.push(token);
19
+ }
20
+ return tokens;
21
+ }
22
+ export function formatCliEntryError(error, argv) {
23
+ const message = error instanceof Error ? error.message : String(error);
24
+ const missingCommandMatch = message.match(/^Command (.+) not found\.$/);
25
+ if (!missingCommandMatch) {
26
+ return message;
27
+ }
28
+ const commandPathTokens = getCommandPathTokens(argv);
29
+ const attemptedCommand = commandPathTokens.join(' ') || missingCommandMatch[1];
30
+ const isApiCommand = commandPathTokens[0] === 'api';
31
+ if (isApiCommand) {
32
+ const helpCommandTokens = commandPathTokens.length > 2 ? commandPathTokens.slice(0, -1) : ['api'];
33
+ const helpCommand = `nb ${helpCommandTokens.join(' ')} --help`;
34
+ return [
35
+ `Unknown command: \`${attemptedCommand}\`.`,
36
+ `If this is a built-in command or a typo, run \`${helpCommand}\` to inspect the commands available under that API group.`,
37
+ ].join('\n');
38
+ }
39
+ return [
40
+ `Unknown command: \`${attemptedCommand}\`.`,
41
+ 'If this is a built-in command or a typo, run `nb --help` to inspect available commands.',
42
+ `If \`${attemptedCommand}\` should be a runtime command from your NocoBase app, check whether the connected app exposes it, then retry the command.`,
43
+ ].join('\n');
44
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { spawn } from 'node:child_process';
10
+ const DEFAULT_DOCKER_LOG_TAIL = 50;
11
+ export function startDockerLogFollower(containerName, options) {
12
+ const tail = Math.max(0, options?.tail ?? DEFAULT_DOCKER_LOG_TAIL);
13
+ const child = spawn('docker', ['logs', '--tail', String(tail), '--follow', containerName], {
14
+ stdio: 'inherit',
15
+ });
16
+ let settled = false;
17
+ let resolveClosed;
18
+ const closed = new Promise((resolve) => {
19
+ resolveClosed = resolve;
20
+ });
21
+ const settle = () => {
22
+ if (!settled) {
23
+ settled = true;
24
+ resolveClosed();
25
+ }
26
+ };
27
+ child.once('error', settle);
28
+ child.once('close', settle);
29
+ return {
30
+ stop: async () => {
31
+ if (settled) {
32
+ return;
33
+ }
34
+ try {
35
+ if (!child.kill()) {
36
+ settle();
37
+ }
38
+ }
39
+ catch {
40
+ settle();
41
+ }
42
+ await closed;
43
+ },
44
+ };
45
+ }
@@ -629,19 +629,19 @@ async function createLoopbackServer(state) {
629
629
  });
630
630
  let resolveWaiter;
631
631
  let rejectWaiter;
632
- const waitForCode = () => new Promise((resolveCode, rejectCode) => {
632
+ const waitForCode = () => new Promise((resolve, reject) => {
633
633
  resolveWaiter = (code) => {
634
634
  void close();
635
- resolveCode(code);
635
+ resolve(code);
636
636
  };
637
637
  rejectWaiter = (error) => {
638
638
  void close();
639
- rejectCode(error);
639
+ reject(error);
640
640
  };
641
641
  });
642
642
  const close = async () => {
643
- await new Promise((resolveClose) => {
644
- server.close(() => resolveClose());
643
+ await new Promise((resolve) => {
644
+ server.close(() => resolve());
645
645
  });
646
646
  };
647
647
  server.on('error', (error) => {
@@ -772,7 +772,7 @@ export async function resolveAccessToken(options) {
772
772
  }
773
773
  const baseUrl = options.baseUrl ?? env.baseUrl;
774
774
  if (!baseUrl) {
775
- throw new Error(`Env "${envName}" is missing a base URL. Run \`nb env add ${envName} --api-base-url <url>\`.`);
775
+ throw new Error(`Env "${envName}" is missing a base URL. Update it with \`nb env update ${envName} --api-base-url <url>\` first.`);
776
776
  }
777
777
  printVerbose(`Refreshing OAuth session for env "${envName}"`);
778
778
  return refreshOauthAccessToken({
@@ -793,7 +793,12 @@ export async function resolveServerRequestTarget(options) {
793
793
  scope: options.scope,
794
794
  });
795
795
  if (!baseUrl) {
796
- throw new Error('Missing base URL. Use --api-base-url or configure one with `nb env add`.');
796
+ throw new Error([
797
+ env ? `Env "${envName}" is missing a base URL.` : `Env "${envName}" is not configured.`,
798
+ env
799
+ ? `Use --api-base-url or update env "${envName}" with \`nb env update ${envName} --api-base-url <url>\`.`
800
+ : `Use --api-base-url or run \`nb init --ui --env ${envName}\` first.`,
801
+ ].join('\n'));
797
802
  }
798
803
  return { baseUrl, token };
799
804
  }
@@ -807,8 +812,8 @@ export async function authenticateEnvWithBasic(options) {
807
812
  ? `Environment "${envName}" does not have an API base URL yet.`
808
813
  : `Environment "${envName}" has not been set up yet.`,
809
814
  env
810
- ? `Run \`nb env add ${envName} --api-base-url <url>\` to finish setting it up.`
811
- : `Run \`nb env add ${envName}\` first.`,
815
+ ? `Run \`nb env update ${envName} --api-base-url <url>\` to finish setting it up.`
816
+ : `Run \`nb init --ui --env ${envName}\` first.`,
812
817
  ]
813
818
  .filter(Boolean)
814
819
  .join('\n'));
@@ -864,8 +869,8 @@ export async function authenticateEnvWithOauth(options) {
864
869
  ? `Environment "${envName}" does not have an API base URL yet.`
865
870
  : `Environment "${envName}" has not been set up yet.`,
866
871
  env
867
- ? `Run \`nb env add ${envName} --api-base-url <url>\` to finish setting it up.`
868
- : `Run \`nb env add ${envName}\` first.`,
872
+ ? `Run \`nb env update ${envName} --api-base-url <url>\` to finish setting it up.`
873
+ : `Run \`nb init --ui --env ${envName}\` first.`,
869
874
  ]
870
875
  .filter(Boolean)
871
876
  .join('\n'));
@@ -912,10 +917,13 @@ export async function authenticateEnvWithOauth(options) {
912
917
  const code = await new Promise((resolve, reject) => {
913
918
  const timeout = setTimeout(() => reject(new Error(`OAuth sign-in timed out after 5 minutes. Run \`nb env auth ${envName}\` to try again.`)), OAUTH_LOGIN_TIMEOUT_MS);
914
919
  timeout.unref?.();
915
- callback.waitForCode().then((value) => {
920
+ callback
921
+ .waitForCode()
922
+ .then((value) => {
916
923
  clearTimeout(timeout);
917
924
  resolve(value);
918
- }, (error) => {
925
+ })
926
+ .catch((error) => {
919
927
  clearTimeout(timeout);
920
928
  reject(error);
921
929
  });
@@ -292,6 +292,9 @@ export async function publishSourceSnapshot(params) {
292
292
  if (publishError) {
293
293
  throw publishError;
294
294
  }
295
+ if (!result) {
296
+ throw new Error('Source snapshot publishing finished without a result.');
297
+ }
295
298
  return result;
296
299
  }
297
300
  export function buildSuggestedInitCommand(result) {
@@ -299,7 +302,7 @@ export function buildSuggestedInitCommand(result) {
299
302
  const normalizedRegistry = result.npmRegistry || `http://${host}:${port || DEFAULT_SOURCE_REGISTRY_PORT}`;
300
303
  const suggestedEnv = ['snapshot', sanitizeEnvSegment(result.gitSha)].filter(Boolean).join('');
301
304
  return [
302
- `nb init --env ${suggestedEnv} --yes --source npm`,
305
+ `nb init --ui --env ${suggestedEnv} --yes --source npm`,
303
306
  `--version ${result.version}`,
304
307
  `--npm-registry=${normalizedRegistry}`,
305
308
  ].join(' ');
@@ -10,6 +10,7 @@ import fs from 'node:fs/promises';
10
10
  import path from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { confirm } from "./inquirer.js";
13
+ import { DEFAULT_UPDATE_POLICY, getExplicitCliConfigValue, loadCliConfig, } from './cli-config.js';
13
14
  import { inspectSelfInstall, inspectSelfStatus, } from './self-manager.js';
14
15
  import { inspectSkillsStatus } from './skills-manager.js';
15
16
  import { resolveCliHomeDir } from './cli-home.js';
@@ -83,39 +84,53 @@ async function writeCurrentInstallEntry(updater) {
83
84
  const state = await readState();
84
85
  const installBinPath = getCurrentInstallBinPath();
85
86
  const nextEntry = updater(getCurrentInstallEntry(state), state);
87
+ const entries = {
88
+ ...(state.entries ?? {}),
89
+ };
90
+ if (nextEntry?.policy || nextEntry?.lastCheckedDate) {
91
+ entries[installBinPath] = nextEntry;
92
+ }
93
+ else {
94
+ delete entries[installBinPath];
95
+ }
86
96
  await writeState({
87
97
  ...state,
88
- entries: {
89
- ...(state.entries ?? {}),
90
- [installBinPath]: nextEntry,
91
- },
98
+ entries: Object.keys(entries).length ? entries : undefined,
92
99
  });
93
100
  }
94
101
  async function markChecked(now = new Date()) {
95
- await writeCurrentInstallEntry((current, state) => {
102
+ await writeCurrentInstallEntry((current) => {
96
103
  return {
97
- policy: current?.policy ?? 'daily',
104
+ ...current,
98
105
  lastCheckedDate: todayStamp(now),
99
106
  };
100
107
  });
101
108
  }
102
- async function disableStartupUpdateForCurrentInstall() {
103
- await writeCurrentInstallEntry((current, state) => {
104
- return {
105
- policy: 'disabled',
106
- lastCheckedDate: current?.lastCheckedDate
107
- ?? state.lastCheckedDate,
108
- };
109
- });
109
+ function resolveLegacyStartupUpdatePolicy(state) {
110
+ const policy = getCurrentInstallEntry(state)?.policy;
111
+ if (policy === 'disabled') {
112
+ return 'off';
113
+ }
114
+ if (policy === 'daily') {
115
+ return 'prompt';
116
+ }
117
+ return undefined;
110
118
  }
111
- async function enableDailyStartupUpdateForCurrentInstall() {
112
- await writeCurrentInstallEntry((current, state) => {
113
- return {
114
- policy: 'daily',
115
- lastCheckedDate: current?.lastCheckedDate
116
- ?? state.lastCheckedDate,
117
- };
118
- });
119
+ async function resolveStartupUpdatePolicy(state) {
120
+ const config = await loadCliConfig({ scope: 'global' });
121
+ const explicit = getExplicitCliConfigValue(config, 'update.policy');
122
+ return explicit ?? resolveLegacyStartupUpdatePolicy(state) ?? DEFAULT_UPDATE_POLICY;
123
+ }
124
+ export async function clearLegacyStartupUpdatePolicyForCurrentInstall() {
125
+ const state = await readState();
126
+ const current = getCurrentInstallEntry(state);
127
+ if (!current?.policy) {
128
+ return false;
129
+ }
130
+ await writeCurrentInstallEntry(() => ({
131
+ lastCheckedDate: current.lastCheckedDate,
132
+ }));
133
+ return true;
119
134
  }
120
135
  export async function shouldRunStartupUpdateCheck(argv, now = new Date()) {
121
136
  if (process.env[NB_SKIP_STARTUP_UPDATE_ENV] === '1') {
@@ -125,19 +140,14 @@ export async function shouldRunStartupUpdateCheck(argv, now = new Date()) {
125
140
  return false;
126
141
  }
127
142
  const state = await readState();
128
- const currentEntry = getCurrentInstallEntry(state);
129
- if (currentEntry?.policy === 'disabled') {
143
+ const policy = await resolveStartupUpdatePolicy(state);
144
+ if (policy === 'off') {
130
145
  return false;
131
146
  }
132
- if (currentEntry?.policy === 'daily') {
133
- return readCurrentInstallLastCheckedDate(state) !== todayStamp(now);
134
- }
135
147
  const selfInstall = await inspectSelfInstall();
136
148
  if (!shouldEnableStartupUpdateForInstallMethod(selfInstall.installMethod)) {
137
- await disableStartupUpdateForCurrentInstall();
138
149
  return false;
139
150
  }
140
- await enableDailyStartupUpdateForCurrentInstall();
141
151
  return readCurrentInstallLastCheckedDate(state) !== todayStamp(now);
142
152
  }
143
153
  export function shouldEnableStartupUpdateForInstallMethod(installMethod) {
@@ -189,14 +199,13 @@ function buildPromptMessage(selfStatus, skillsStatus) {
189
199
  return lines.join('\n');
190
200
  }
191
201
  function buildUpdateCommands(selfStatus, skillsStatus) {
192
- const commands = [];
193
202
  if (selfStatus.updateAvailable && selfStatus.updatable) {
194
- commands.push('nb self update --yes');
203
+ return [skillsStatus.updateAvailable === true ? 'nb self update --yes --skills' : 'nb self update --yes'];
195
204
  }
196
205
  if (skillsStatus.updateAvailable === true) {
197
- commands.push('nb skills update --yes');
206
+ return ['nb skills update --yes'];
198
207
  }
199
- return commands;
208
+ return [];
200
209
  }
201
210
  function buildNonInteractiveWarning(selfStatus, skillsStatus) {
202
211
  const commands = buildUpdateCommands(selfStatus, skillsStatus);
@@ -233,26 +242,36 @@ function buildDeclinedWarning(selfStatus, skillsStatus) {
233
242
  'You may run into compatibility issues until you update.',
234
243
  ].join(' ');
235
244
  }
236
- async function runStartupUpdates() {
237
- await run('nb', ['self', 'update', '--yes'], {
238
- stdio: 'inherit',
239
- env: {
240
- [NB_SKIP_STARTUP_UPDATE_ENV]: '1',
241
- },
242
- errorName: 'nb self update',
243
- });
244
- await run('nb', ['skills', 'update', '--yes'], {
245
- stdio: 'inherit',
246
- env: {
247
- [NB_SKIP_STARTUP_UPDATE_ENV]: '1',
248
- },
249
- errorName: 'nb skills update',
250
- });
245
+ async function runStartupUpdates(selfStatus, skillsStatus) {
246
+ if (selfStatus.updateAvailable && selfStatus.updatable) {
247
+ const args = ['self', 'update', '--yes'];
248
+ if (skillsStatus.updateAvailable === true) {
249
+ args.push('--skills');
250
+ }
251
+ await run('nb', args, {
252
+ stdio: 'inherit',
253
+ env: {
254
+ [NB_SKIP_STARTUP_UPDATE_ENV]: '1',
255
+ },
256
+ errorName: 'nb self update',
257
+ });
258
+ return;
259
+ }
260
+ if (skillsStatus.updateAvailable === true) {
261
+ await run('nb', ['skills', 'update', '--yes'], {
262
+ stdio: 'inherit',
263
+ env: {
264
+ [NB_SKIP_STARTUP_UPDATE_ENV]: '1',
265
+ },
266
+ errorName: 'nb skills update',
267
+ });
268
+ }
251
269
  }
252
- export async function maybeRunStartupUpdatePrompt(argv) {
270
+ export async function maybeRunStartupUpdate(argv) {
253
271
  if (!(await shouldRunStartupUpdateCheck(argv))) {
254
272
  return { kind: 'skipped' };
255
273
  }
274
+ const policy = await resolveStartupUpdatePolicy(await readState());
256
275
  const selfStatus = await inspectSelfStatus();
257
276
  const skillsStatus = await inspectSkillsStatus();
258
277
  if (!hasPendingUpdates(selfStatus, skillsStatus)) {
@@ -264,6 +283,11 @@ export async function maybeRunStartupUpdatePrompt(argv) {
264
283
  await markChecked();
265
284
  return { kind: 'warned' };
266
285
  }
286
+ if (policy === 'auto') {
287
+ await runStartupUpdates(selfStatus, skillsStatus);
288
+ await markChecked();
289
+ return { kind: 'updated' };
290
+ }
267
291
  let answer = false;
268
292
  try {
269
293
  answer = await confirm({
@@ -279,7 +303,7 @@ export async function maybeRunStartupUpdatePrompt(argv) {
279
303
  await markChecked();
280
304
  return { kind: 'declined' };
281
305
  }
282
- await runStartupUpdates();
306
+ await runStartupUpdates(selfStatus, skillsStatus);
283
307
  await markChecked();
284
308
  return { kind: 'updated' };
285
309
  }
@@ -370,6 +370,12 @@
370
370
  "description": "Set up the first admin."
371
371
  }
372
372
  }
373
+ },
374
+ "env": {
375
+ "messages": {
376
+ "noEnvsConfigured": "No envs configured.",
377
+ "noEnvsConfiguredHelp": "Run `nb init --ui` to create one first."
378
+ }
373
379
  }
374
380
  },
375
381
  "apiCommandCompat": {
@@ -370,6 +370,12 @@
370
370
  "description": "设置第一个管理员账号。"
371
371
  }
372
372
  }
373
+ },
374
+ "env": {
375
+ "messages": {
376
+ "noEnvsConfigured": "尚未配置任何 env。",
377
+ "noEnvsConfiguredHelp": "请先运行 `nb init --ui` 创建一个。"
378
+ }
373
379
  }
374
380
  },
375
381
  "apiCommandCompat": {