@nocobase/cli 2.1.0-alpha.40 → 2.1.0-alpha.46

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 (56) hide show
  1. package/README.md +20 -8
  2. package/README.zh-CN.md +20 -8
  3. package/bin/run.js +9 -37
  4. package/dist/commands/app/destroy.js +225 -0
  5. package/dist/commands/app/down.js +24 -254
  6. package/dist/commands/app/restart.js +12 -5
  7. package/dist/commands/app/shared.js +122 -0
  8. package/dist/commands/app/start.js +41 -59
  9. package/dist/commands/app/stop.js +72 -26
  10. package/dist/commands/app/upgrade.js +312 -429
  11. package/dist/commands/config/delete.js +8 -1
  12. package/dist/commands/config/get.js +5 -0
  13. package/dist/commands/config/set.js +10 -1
  14. package/dist/commands/db/ps.js +5 -18
  15. package/dist/commands/db/start.js +23 -8
  16. package/dist/commands/env/add.js +66 -6
  17. package/dist/commands/env/auth.js +86 -27
  18. package/dist/commands/env/info.js +52 -8
  19. package/dist/commands/env/list.js +6 -7
  20. package/dist/commands/env/shared.js +41 -3
  21. package/dist/commands/env/status.js +10 -7
  22. package/dist/commands/init.js +214 -136
  23. package/dist/commands/install.js +364 -294
  24. package/dist/commands/license/plugins/shared.js +9 -3
  25. package/dist/commands/license/plugins/sync.js +54 -25
  26. package/dist/commands/self/update.js +58 -1
  27. package/dist/commands/source/dev.js +5 -5
  28. package/dist/commands/source/download.js +29 -25
  29. package/dist/generated/command-registry.js +3 -2
  30. package/dist/lib/api-client.js +6 -0
  31. package/dist/lib/api-command-compat.js +641 -0
  32. package/dist/lib/app-health.js +31 -18
  33. package/dist/lib/app-managed-resources.js +1 -1
  34. package/dist/lib/app-runtime.js +2 -2
  35. package/dist/lib/auth-store.js +56 -19
  36. package/dist/lib/backup.js +3 -3
  37. package/dist/lib/bootstrap.js +14 -8
  38. package/dist/lib/cli-config.js +137 -4
  39. package/dist/lib/cli-entry-error.js +44 -0
  40. package/dist/lib/cli-locale.js +19 -7
  41. package/dist/lib/db-connection-check.js +61 -0
  42. package/dist/lib/docker-log-stream.js +45 -0
  43. package/dist/lib/env-auth.js +98 -11
  44. package/dist/lib/env-config.js +3 -2
  45. package/dist/lib/env-guard.js +1 -1
  46. package/dist/lib/generated-command.js +17 -0
  47. package/dist/lib/prompt-validators.js +23 -5
  48. package/dist/lib/prompt-web-ui.js +143 -19
  49. package/dist/lib/run-npm.js +133 -23
  50. package/dist/lib/skills-manager.js +80 -4
  51. package/dist/lib/source-publish.js +4 -1
  52. package/dist/lib/startup-update.js +74 -50
  53. package/dist/lib/ui.js +4 -1
  54. package/dist/locale/en-US.json +92 -5
  55. package/dist/locale/zh-CN.json +92 -5
  56. package/package.json +34 -2
@@ -6,10 +6,12 @@
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 { printInfo, startTask, stopTask, updateTask } from './ui.js';
9
+ import { startDockerLogFollower } from './docker-log-stream.js';
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;
12
13
  const APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS = 5_000;
14
+ const APP_HEALTH_CHECK_PROGRESS_LOG_INTERVAL_MS = 10_000;
13
15
  function trimValue(value) {
14
16
  return String(value ?? '').trim();
15
17
  }
@@ -92,35 +94,46 @@ export async function waitForAppReady(params) {
92
94
  }
93
95
  const healthCheckUrl = buildHealthCheckUrl(apiBaseUrl);
94
96
  const startedAt = Date.now();
97
+ const timeoutMs = params.timeoutMs ?? APP_HEALTH_CHECK_TIMEOUT_MS;
98
+ const intervalMs = params.intervalMs ?? APP_HEALTH_CHECK_INTERVAL_MS;
99
+ const progressLogIntervalMs = Math.max(1, params.progressLogIntervalMs ?? APP_HEALTH_CHECK_PROGRESS_LOG_INTERVAL_MS);
95
100
  let lastMessage = 'No response yet';
96
- let spinnerActive = true;
97
- startTask(`Waiting for NocoBase to become ready for "${params.envName}"...`);
101
+ let nextProgressLogAt = startedAt + progressLogIntervalMs;
102
+ printInfo(`Waiting for NocoBase to become ready for "${params.envName}"...`);
103
+ const dockerLogFollower = params.verbose && params.containerName ? startDockerLogFollower(params.containerName) : undefined;
98
104
  try {
99
- while (Date.now() - startedAt < APP_HEALTH_CHECK_TIMEOUT_MS) {
105
+ while (Date.now() - startedAt < timeoutMs) {
100
106
  const result = await requestAppHealthCheck({
101
107
  healthCheckUrl,
102
108
  fetchImpl: params.fetchImpl,
109
+ requestTimeoutMs: params.requestTimeoutMs,
103
110
  });
104
111
  if (result.ok) {
105
- stopTask();
106
- spinnerActive = false;
107
112
  return;
108
113
  }
109
114
  lastMessage = result.message;
110
- const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
111
- updateTask(`Waiting for NocoBase to become ready for "${params.envName}"... (${elapsedSeconds}s elapsed, last status: ${lastMessage})`);
112
- await sleep(APP_HEALTH_CHECK_INTERVAL_MS);
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));
113
128
  }
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}`);
114
135
  }
115
136
  finally {
116
- if (spinnerActive) {
117
- stopTask();
118
- }
137
+ await dockerLogFollower?.stop();
119
138
  }
120
- const hints = [
121
- params.logHint,
122
- params.containerName ? `docker logs ${params.containerName}` : undefined,
123
- ].filter(Boolean);
124
- const hintText = hints.length > 0 ? ` ${hints.join(' ')}` : '';
125
- 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}`);
126
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) {
@@ -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);
@@ -8,7 +8,8 @@
8
8
  */
9
9
  import { promises as fs } from 'node:fs';
10
10
  import path from 'node:path';
11
- import { resolveCliHomeDir, resolveConfiguredEnvPath, resolveEnvRelativePath, } from './cli-home.js';
11
+ import { resolveCliHomeDir, resolveConfiguredEnvPath, resolveEnvRelativePath } from './cli-home.js';
12
+ import { normalizeCliLocale } from './cli-locale.js';
12
13
  import { cleanupCurrentSessionAfterEnvRemoval, resolveEffectiveCurrentEnv, setSessionCurrentEnv, } from './session-store.js';
13
14
  function normalizeStoredEnvKind(value) {
14
15
  const kind = String(value ?? '').trim();
@@ -24,13 +25,27 @@ function normalizeOptionalString(value) {
24
25
  const normalized = String(value ?? '').trim();
25
26
  return normalized || undefined;
26
27
  }
28
+ function normalizeOptionalCliLocale(value) {
29
+ const normalized = normalizeOptionalString(value);
30
+ if (!normalized) {
31
+ return undefined;
32
+ }
33
+ return normalizeCliLocale(normalized);
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
+ }
27
42
  export function readEnvApiBaseUrl(config) {
28
43
  if (!config) {
29
44
  return undefined;
30
45
  }
31
- return (normalizeOptionalString(config.apiBaseUrl)
32
- ?? normalizeOptionalString(config.baseUrl)
33
- ?? normalizeOptionalString(config.apibaseUrl));
46
+ return (normalizeOptionalString(config.apiBaseUrl) ??
47
+ normalizeOptionalString(config.baseUrl) ??
48
+ normalizeOptionalString(config.apibaseUrl));
34
49
  }
35
50
  export function resolveEnvKind(config) {
36
51
  if (!config) {
@@ -70,9 +85,13 @@ function normalizeEnvConfigEntry(entry) {
70
85
  }
71
86
  function normalizeAuthConfig(config) {
72
87
  const settings = config.settings ?? {};
88
+ const locale = normalizeOptionalCliLocale(settings.locale);
89
+ const updatePolicy = normalizeOptionalCliUpdatePolicy(settings.update?.policy);
73
90
  return {
74
91
  name: config.name || config.dockerResourcePrefix,
75
92
  settings: {
93
+ ...(locale ? { locale } : {}),
94
+ ...(updatePolicy ? { update: { policy: updatePolicy } } : {}),
76
95
  ...(settings.license?.pkgUrl ? { license: { pkgUrl: normalizeOptionalString(settings.license.pkgUrl) } } : {}),
77
96
  ...(settings.docker?.network || settings.docker?.containerPrefix
78
97
  ? {
@@ -84,8 +103,19 @@ function normalizeAuthConfig(config) {
84
103
  },
85
104
  }
86
105
  : {}),
106
+ ...(settings.bin?.docker || settings.bin?.git || settings.bin?.yarn
107
+ ? {
108
+ bin: {
109
+ ...(settings.bin?.docker ? { docker: normalizeOptionalString(settings.bin.docker) } : {}),
110
+ ...(settings.bin?.git ? { git: normalizeOptionalString(settings.bin.git) } : {}),
111
+ ...(settings.bin?.yarn ? { yarn: normalizeOptionalString(settings.bin.yarn) } : {}),
112
+ },
113
+ }
114
+ : {}),
87
115
  },
88
- lastEnv: config.lastEnv || config.currentEnv || 'default',
116
+ lastEnv: config.lastEnv ||
117
+ config.currentEnv ||
118
+ 'default',
89
119
  envs: Object.fromEntries(Object.entries(config.envs || {}).map(([envName, entry]) => [envName, normalizeEnvConfigEntry(entry) ?? {}])),
90
120
  };
91
121
  }
@@ -222,10 +252,11 @@ export class Env {
222
252
  export async function getEnv(envName, options = {}) {
223
253
  const { config: snapshot, ...loadOptions } = options;
224
254
  const config = snapshot ?? (await loadAuthConfig(loadOptions));
225
- const resolved = envName?.trim() || (await resolveEffectiveCurrentEnv(Object.keys(config.envs).sort(), {
226
- scope: loadOptions.scope,
227
- lastEnv: config.lastEnv,
228
- }));
255
+ const resolved = envName?.trim() ||
256
+ (await resolveEffectiveCurrentEnv(Object.keys(config.envs).sort(), {
257
+ scope: loadOptions.scope,
258
+ lastEnv: config.lastEnv,
259
+ }));
229
260
  const envConfig = config.envs[resolved];
230
261
  if (!envConfig) {
231
262
  return undefined;
@@ -260,37 +291,40 @@ async function writeEnv(envName, updater, options = {}) {
260
291
  await saveAuthConfig(config, options);
261
292
  }
262
293
  function normalizeConfiguredAuthType(value) {
263
- return value === 'token' || value === 'oauth' ? value : undefined;
294
+ return value === 'basic' || value === 'token' || value === 'oauth' ? value : undefined;
264
295
  }
265
296
  export function resolveConfiguredAuthType(config) {
266
297
  return normalizeConfiguredAuthType(config?.authType) ?? normalizeConfiguredAuthType(config?.auth?.type);
267
298
  }
268
299
  export async function upsertEnv(envName, config, options = {}) {
269
300
  await writeEnv(envName, (previous) => {
270
- const { apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, accessToken, authType, ...rest } = config;
301
+ const { apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, accessToken, authType, authUsername, ...rest } = config;
271
302
  const nextApiBaseUrl = readEnvApiBaseUrl(config);
272
303
  const previousApiBaseUrl = readEnvApiBaseUrl(previous);
273
304
  const baseUrlChanged = previousApiBaseUrl !== nextApiBaseUrl;
274
305
  const previousAuthType = resolveConfiguredAuthType(previous);
275
306
  const requestedAuthType = normalizeConfiguredAuthType(authType);
276
307
  const nextAuthType = requestedAuthType ?? (accessToken ? 'token' : previousAuthType);
308
+ const nextAuthUsername = nextAuthType === 'basic' ? normalizeOptionalString(authUsername) ?? previous?.authUsername : undefined;
277
309
  const nextAuth = accessToken
278
310
  ? {
279
311
  type: 'token',
280
312
  accessToken,
281
313
  }
282
- : nextAuthType === 'token' || baseUrlChanged || previous?.auth?.type === 'token'
283
- ? undefined
284
- : previous?.auth;
314
+ : nextAuthType === 'oauth' && !baseUrlChanged && previous?.auth?.type === 'oauth'
315
+ ? previous.auth
316
+ : undefined;
285
317
  const authChanged = !areAuthConfigsEquivalent(previous?.auth, nextAuth);
286
318
  const authTypeChanged = previousAuthType !== nextAuthType;
319
+ const authUsernameChanged = previous?.authUsername !== nextAuthUsername;
287
320
  return {
288
321
  ...previous,
289
322
  apiBaseUrl: nextApiBaseUrl,
290
323
  authType: nextAuthType,
324
+ authUsername: nextAuthUsername,
291
325
  auth: nextAuth,
292
326
  ...rest,
293
- runtime: baseUrlChanged || authChanged || authTypeChanged ? undefined : previous?.runtime,
327
+ runtime: baseUrlChanged || authChanged || authTypeChanged || authUsernameChanged ? undefined : previous?.runtime,
294
328
  };
295
329
  }, options);
296
330
  }
@@ -302,22 +336,25 @@ export async function updateEnvConnection(envName, updates, options = {}) {
302
336
  const previousAuthType = resolveConfiguredAuthType(previous);
303
337
  const requestedAuthType = normalizeConfiguredAuthType(updates.authType);
304
338
  const nextAuthType = requestedAuthType ?? (updates.accessToken ? 'token' : previousAuthType);
339
+ const nextAuthUsername = nextAuthType === 'basic' ? normalizeOptionalString(updates.authUsername) ?? previous?.authUsername : undefined;
305
340
  const nextAuth = updates.accessToken
306
341
  ? {
307
342
  type: 'token',
308
343
  accessToken: updates.accessToken,
309
344
  }
310
- : nextAuthType === 'token' || baseUrlChanged || previous?.auth?.type === 'token'
311
- ? undefined
312
- : previous?.auth;
345
+ : nextAuthType === 'oauth' && !baseUrlChanged && previous?.auth?.type === 'oauth'
346
+ ? previous.auth
347
+ : undefined;
313
348
  const authChanged = !areAuthConfigsEquivalent(previous?.auth, nextAuth);
314
349
  const authTypeChanged = previousAuthType !== nextAuthType;
350
+ const authUsernameChanged = previous?.authUsername !== nextAuthUsername;
315
351
  return {
316
352
  ...previous,
317
353
  ...(nextApiBaseUrl !== undefined ? { apiBaseUrl: nextApiBaseUrl } : {}),
318
354
  authType: nextAuthType,
355
+ authUsername: nextAuthUsername,
319
356
  auth: nextAuth,
320
- runtime: baseUrlChanged || authChanged || authTypeChanged ? undefined : previous?.runtime,
357
+ runtime: baseUrlChanged || authChanged || authTypeChanged || authUsernameChanged ? undefined : previous?.runtime,
321
358
  };
322
359
  }, options);
323
360
  }
@@ -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'));
@@ -8,13 +8,24 @@
8
8
  */
9
9
  import { loadExactAuthConfig, saveAuthConfig } from './auth-store.js';
10
10
  import { resolveDefaultConfigScope } from './cli-home.js';
11
+ import { CLI_LOCALE_FLAG_OPTIONS, normalizeCliLocale, resolveCliLocale } from './cli-locale.js';
11
12
  export const DEFAULT_LICENSE_PKG_URL = 'https://pkg.nocobase.com/';
12
13
  export const DEFAULT_DOCKER_NETWORK = 'nocobase';
13
14
  export const DEFAULT_DOCKER_CONTAINER_PREFIX = 'nb';
15
+ export const DEFAULT_DOCKER_BIN = 'docker';
16
+ export const DEFAULT_GIT_BIN = 'git';
17
+ export const DEFAULT_YARN_BIN = 'yarn';
18
+ export const CLI_UPDATE_POLICY_OPTIONS = ['prompt', 'auto', 'off'];
19
+ export const DEFAULT_UPDATE_POLICY = 'prompt';
14
20
  export const SUPPORTED_CLI_CONFIG_KEYS = [
21
+ 'locale',
22
+ 'update.policy',
15
23
  'license.pkg-url',
16
24
  'docker.network',
17
25
  'docker.container-prefix',
26
+ 'bin.docker',
27
+ 'bin.git',
28
+ 'bin.yarn',
18
29
  ];
19
30
  function trimValue(value) {
20
31
  const text = String(value ?? '').trim();
@@ -34,13 +45,30 @@ export function assertSupportedCliConfigKey(value) {
34
45
  }
35
46
  return value;
36
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
+ }
37
55
  function cloneSettings(config) {
38
56
  return {
57
+ ...(config.settings?.locale ? { locale: trimValue(config.settings.locale) } : {}),
58
+ update: config.settings?.update ? { ...config.settings.update } : undefined,
39
59
  license: config.settings?.license ? { ...config.settings.license } : undefined,
40
60
  docker: config.settings?.docker ? { ...config.settings.docker } : undefined,
61
+ bin: config.settings?.bin ? { ...config.settings.bin } : undefined,
41
62
  };
42
63
  }
43
64
  function pruneSettings(config) {
65
+ if (config.settings && !trimValue(config.settings.locale)) {
66
+ delete config.settings.locale;
67
+ }
68
+ const update = config.settings?.update;
69
+ if (update && !normalizeCliUpdatePolicy(update.policy)) {
70
+ delete config.settings?.update;
71
+ }
44
72
  const license = config.settings?.license;
45
73
  if (license && !trimValue(license.pkgUrl)) {
46
74
  delete config.settings?.license;
@@ -49,34 +77,61 @@ function pruneSettings(config) {
49
77
  if (docker && !trimValue(docker.network) && !trimValue(docker.containerPrefix)) {
50
78
  delete config.settings?.docker;
51
79
  }
52
- if (config.settings
53
- && !config.settings.license
54
- && !config.settings.docker) {
80
+ const bin = config.settings?.bin;
81
+ if (bin && !trimValue(bin.docker) && !trimValue(bin.git) && !trimValue(bin.yarn)) {
82
+ delete config.settings?.bin;
83
+ }
84
+ if (config.settings &&
85
+ !config.settings.locale &&
86
+ !config.settings.update &&
87
+ !config.settings.license &&
88
+ !config.settings.docker &&
89
+ !config.settings.bin) {
55
90
  delete config.settings;
56
91
  }
57
92
  }
58
93
  export function getExplicitCliConfigValue(config, key) {
59
94
  switch (key) {
95
+ case 'locale':
96
+ return trimValue(config.settings?.locale);
97
+ case 'update.policy':
98
+ return normalizeCliUpdatePolicy(config.settings?.update?.policy);
60
99
  case 'license.pkg-url':
61
100
  return trimValue(config.settings?.license?.pkgUrl);
62
101
  case 'docker.network':
63
102
  return trimValue(config.settings?.docker?.network);
64
103
  case 'docker.container-prefix':
65
104
  return trimValue(config.settings?.docker?.containerPrefix);
105
+ case 'bin.docker':
106
+ return trimValue(config.settings?.bin?.docker);
107
+ case 'bin.git':
108
+ return trimValue(config.settings?.bin?.git);
109
+ case 'bin.yarn':
110
+ return trimValue(config.settings?.bin?.yarn);
66
111
  }
67
112
  }
68
113
  export function getEffectiveCliConfigValue(config, key) {
69
114
  const explicit = getExplicitCliConfigValue(config, key);
70
- if (explicit) {
115
+ if (explicit && key !== 'locale') {
71
116
  return explicit;
72
117
  }
73
118
  switch (key) {
119
+ case 'locale':
120
+ return resolveCliLocale(undefined, { configuredLocale: trimValue(config.settings?.locale) });
121
+ case 'update.policy':
122
+ return explicit ?? DEFAULT_UPDATE_POLICY;
74
123
  case 'license.pkg-url':
75
124
  return DEFAULT_LICENSE_PKG_URL;
76
125
  case 'docker.network':
77
126
  return trimValue(config.name) || DEFAULT_DOCKER_NETWORK;
78
127
  case 'docker.container-prefix':
79
128
  return trimValue(config.name) || DEFAULT_DOCKER_CONTAINER_PREFIX;
129
+ case 'bin.docker':
130
+ return DEFAULT_DOCKER_BIN;
131
+ case 'bin.git':
132
+ return DEFAULT_GIT_BIN;
133
+ case 'bin.yarn':
134
+ return DEFAULT_YARN_BIN;
80
135
  }
81
136
  }
82
137
  export function normalizeCliConfigValue(key, value) {
@@ -87,6 +142,20 @@ export function normalizeCliConfigValue(key, value) {
87
142
  if (key === 'license.pkg-url') {
88
143
  return normalized.replace(/\/+$/, '') + '/';
89
144
  }
145
+ if (key === 'locale') {
146
+ const locale = normalizeCliLocale(normalized);
147
+ if (!locale) {
148
+ throw new Error(`Config key "${key}" must be one of: ${CLI_LOCALE_FLAG_OPTIONS.join(', ')}`);
149
+ }
150
+ return locale;
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
+ }
90
159
  return normalized;
91
160
  }
92
161
  export async function loadCliConfig(options = {}) {
@@ -113,6 +182,15 @@ export async function setCliConfigValue(key, value, options = {}) {
113
182
  const normalized = normalizeCliConfigValue(key, value);
114
183
  config.settings = cloneSettings(config);
115
184
  switch (key) {
185
+ case 'locale':
186
+ config.settings.locale = normalized;
187
+ break;
188
+ case 'update.policy':
189
+ config.settings.update = {
190
+ ...(config.settings.update ?? {}),
191
+ policy: normalized,
192
+ };
193
+ break;
116
194
  case 'license.pkg-url':
117
195
  config.settings.license = {
118
196
  ...(config.settings.license ?? {}),
@@ -131,6 +209,24 @@ export async function setCliConfigValue(key, value, options = {}) {
131
209
  containerPrefix: normalized,
132
210
  };
133
211
  break;
212
+ case 'bin.docker':
213
+ config.settings.bin = {
214
+ ...(config.settings.bin ?? {}),
215
+ docker: normalized,
216
+ };
217
+ break;
218
+ case 'bin.git':
219
+ config.settings.bin = {
220
+ ...(config.settings.bin ?? {}),
221
+ git: normalized,
222
+ };
223
+ break;
224
+ case 'bin.yarn':
225
+ config.settings.bin = {
226
+ ...(config.settings.bin ?? {}),
227
+ yarn: normalized,
228
+ };
229
+ break;
134
230
  }
135
231
  pruneSettings(config);
136
232
  await saveAuthConfig(config, scope);
@@ -145,6 +241,14 @@ export async function deleteCliConfigValue(key, options = {}) {
145
241
  }
146
242
  config.settings = cloneSettings(config);
147
243
  switch (key) {
244
+ case 'locale':
245
+ delete config.settings.locale;
246
+ break;
247
+ case 'update.policy':
248
+ if (config.settings.update) {
249
+ delete config.settings.update.policy;
250
+ }
251
+ break;
148
252
  case 'license.pkg-url':
149
253
  if (config.settings.license) {
150
254
  delete config.settings.license.pkgUrl;
@@ -160,6 +264,21 @@ export async function deleteCliConfigValue(key, options = {}) {
160
264
  delete config.settings.docker.containerPrefix;
161
265
  }
162
266
  break;
267
+ case 'bin.docker':
268
+ if (config.settings.bin) {
269
+ delete config.settings.bin.docker;
270
+ }
271
+ break;
272
+ case 'bin.git':
273
+ if (config.settings.bin) {
274
+ delete config.settings.bin.git;
275
+ }
276
+ break;
277
+ case 'bin.yarn':
278
+ if (config.settings.bin) {
279
+ delete config.settings.bin.yarn;
280
+ }
281
+ break;
163
282
  }
164
283
  pruneSettings(config);
165
284
  await saveAuthConfig(config, scope);
@@ -174,3 +293,17 @@ export async function resolveDockerContainerPrefix(options = {}) {
174
293
  export async function resolveLicensePkgUrlFromConfig(options = {}) {
175
294
  return await getCliConfigValue('license.pkg-url', options);
176
295
  }
296
+ const CONFIGURABLE_COMMAND_KEYS = {
297
+ docker: 'bin.docker',
298
+ git: 'bin.git',
299
+ yarn: 'bin.yarn',
300
+ };
301
+ export function isConfigurableCommandName(value) {
302
+ return Object.prototype.hasOwnProperty.call(CONFIGURABLE_COMMAND_KEYS, value);
303
+ }
304
+ export async function resolveConfiguredCommandName(commandName, options = {}) {
305
+ if (!isConfigurableCommandName(commandName)) {
306
+ return commandName;
307
+ }
308
+ return await getCliConfigValue(CONFIGURABLE_COMMAND_KEYS[commandName], options);
309
+ }
@@ -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
+ }