@nocobase/cli 2.1.0-beta.36 → 2.1.0-beta.38

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 (43) hide show
  1. package/README.md +20 -8
  2. package/README.zh-CN.md +20 -8
  3. package/bin/run.js +3 -2
  4. package/dist/commands/app/destroy.js +225 -0
  5. package/dist/commands/app/down.js +24 -254
  6. package/dist/commands/app/shared.js +122 -0
  7. package/dist/commands/app/start.js +40 -59
  8. package/dist/commands/app/stop.js +72 -26
  9. package/dist/commands/app/upgrade.js +310 -427
  10. package/dist/commands/config/delete.js +4 -0
  11. package/dist/commands/config/get.js +4 -0
  12. package/dist/commands/config/set.js +5 -1
  13. package/dist/commands/db/start.js +23 -8
  14. package/dist/commands/env/add.js +66 -6
  15. package/dist/commands/env/auth.js +86 -27
  16. package/dist/commands/env/info.js +52 -8
  17. package/dist/commands/env/list.js +2 -2
  18. package/dist/commands/env/shared.js +41 -3
  19. package/dist/commands/init.js +196 -136
  20. package/dist/commands/install.js +311 -265
  21. package/dist/commands/license/plugins/shared.js +9 -3
  22. package/dist/commands/license/plugins/sync.js +54 -25
  23. package/dist/commands/source/download.js +29 -25
  24. package/dist/generated/command-registry.js +3 -2
  25. package/dist/lib/api-client.js +6 -0
  26. package/dist/lib/api-command-compat.js +641 -0
  27. package/dist/lib/app-health.js +27 -21
  28. package/dist/lib/auth-store.js +47 -19
  29. package/dist/lib/cli-config.js +99 -4
  30. package/dist/lib/cli-locale.js +19 -7
  31. package/dist/lib/db-connection-check.js +61 -0
  32. package/dist/lib/env-auth.js +79 -0
  33. package/dist/lib/env-config.js +3 -2
  34. package/dist/lib/env-guard.js +1 -1
  35. package/dist/lib/generated-command.js +17 -0
  36. package/dist/lib/prompt-validators.js +23 -5
  37. package/dist/lib/prompt-web-ui.js +143 -19
  38. package/dist/lib/run-npm.js +133 -23
  39. package/dist/lib/skills-manager.js +80 -4
  40. package/dist/lib/ui.js +4 -1
  41. package/dist/locale/en-US.json +86 -5
  42. package/dist/locale/zh-CN.json +86 -5
  43. package/package.json +34 -2
@@ -19,9 +19,39 @@ import fsp from 'node:fs/promises';
19
19
  import os from 'node:os';
20
20
  import path from 'node:path';
21
21
  import spawn from 'cross-spawn';
22
+ import { translateCli } from './cli-locale.js';
23
+ import { resolveConfiguredCommandName } from './cli-config.js';
22
24
  const FORWARDED_SIGNALS = ['SIGINT', 'SIGTERM'];
23
- function resolveCommandName(name) {
24
- return name;
25
+ const PROCESS_TIMEOUT_FORCE_KILL_DELAY_MS = 1000;
26
+ const MISSING_COMMAND_SPECS = {
27
+ docker: {
28
+ displayName: 'Docker',
29
+ configKey: 'bin.docker',
30
+ },
31
+ git: {
32
+ displayName: 'Git',
33
+ configKey: 'bin.git',
34
+ },
35
+ yarn: {
36
+ displayName: 'Yarn',
37
+ configKey: 'bin.yarn',
38
+ },
39
+ };
40
+ async function resolveCommandName(name) {
41
+ return await resolveConfiguredCommandName(name);
42
+ }
43
+ function createMissingCommandError(name, label, error) {
44
+ const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : undefined;
45
+ if (code !== 'ENOENT') {
46
+ return undefined;
47
+ }
48
+ if (!Object.prototype.hasOwnProperty.call(MISSING_COMMAND_SPECS, name)) {
49
+ return undefined;
50
+ }
51
+ const spec = MISSING_COMMAND_SPECS[name];
52
+ return new Error(translateCli('commands.shared.missingCommand', { action: label, displayName: spec.displayName, configKey: spec.configKey }, {
53
+ fallback: `Couldn't run \`${label}\` because the ${spec.displayName} executable could not be found. Install ${spec.displayName} or update \`nb config set ${spec.configKey} <path>\` and try again.`,
54
+ }));
25
55
  }
26
56
  function pathExists(candidate) {
27
57
  try {
@@ -40,8 +70,8 @@ function isDirectory(candidate) {
40
70
  }
41
71
  }
42
72
  function hasLocalNocoBaseBinary(candidate) {
43
- return (pathExists(path.join(candidate, 'node_modules', '.bin', 'nocobase-v1'))
44
- || pathExists(path.join(candidate, 'node_modules', '.bin', 'nocobase-v1.cmd')));
73
+ return (pathExists(path.join(candidate, 'node_modules', '.bin', 'nocobase-v1')) ||
74
+ pathExists(path.join(candidate, 'node_modules', '.bin', 'nocobase-v1.cmd')));
45
75
  }
46
76
  export function resolveCwd(cwd) {
47
77
  const next = cwd ?? process.cwd();
@@ -61,10 +91,7 @@ export function resolveProjectCwd(cwd) {
61
91
  throw new Error(`The specified --cwd is not a directory: ${fallback}`);
62
92
  }
63
93
  let current = hasExplicitInput ? fallback : process.cwd();
64
- while (true) {
65
- if (hasLocalNocoBaseBinary(current)) {
66
- return current;
67
- }
94
+ while (!hasLocalNocoBaseBinary(current)) {
68
95
  const parent = path.dirname(current);
69
96
  if (parent === current) {
70
97
  if (hasExplicitInput) {
@@ -74,12 +101,13 @@ export function resolveProjectCwd(cwd) {
74
101
  }
75
102
  current = parent;
76
103
  }
104
+ return current;
77
105
  }
78
- export function run(name, args, options) {
106
+ export async function run(name, args, options) {
79
107
  const cwd = resolveCwd(options?.cwd);
80
108
  const label = options?.errorName ?? name;
81
- const command = resolveCommandName(name);
82
- return new Promise((resolve, reject) => {
109
+ const command = await resolveCommandName(name);
110
+ return await new Promise((resolve, reject) => {
83
111
  const child = spawn(command, [...args], {
84
112
  stdio: options?.stdio ?? 'inherit',
85
113
  cwd,
@@ -104,12 +132,19 @@ export function run(name, args, options) {
104
132
  }
105
133
  }
106
134
  const cleanupSignalForwarding = forwardSignalsToChild(child);
135
+ const timeoutController = attachProcessTimeout(child, options?.timeoutMs);
107
136
  child.once('error', (error) => {
137
+ timeoutController.cleanup();
108
138
  cleanupSignalForwarding();
109
- reject(error);
139
+ reject(createMissingCommandError(name, label, error) ?? error);
110
140
  });
111
141
  child.once('close', (code, signal) => {
142
+ timeoutController.cleanup();
112
143
  cleanupSignalForwarding();
144
+ if (timeoutController.didTimeout()) {
145
+ reject(new Error(`${label} timed out after ${options?.timeoutMs}ms`));
146
+ return;
147
+ }
113
148
  if (code === 0) {
114
149
  resolve();
115
150
  return;
@@ -122,6 +157,46 @@ export function run(name, args, options) {
122
157
  });
123
158
  });
124
159
  }
160
+ function attachProcessTimeout(child, timeoutMs) {
161
+ if (!timeoutMs || timeoutMs <= 0) {
162
+ return {
163
+ cleanup: () => undefined,
164
+ didTimeout: () => false,
165
+ };
166
+ }
167
+ let didTimeout = false;
168
+ let forceKillTimer;
169
+ const isChildRunning = () => child.exitCode === null && child.signalCode === null;
170
+ const terminateChild = (signal) => {
171
+ if (!isChildRunning()) {
172
+ return;
173
+ }
174
+ try {
175
+ child.kill(signal);
176
+ }
177
+ catch {
178
+ // Ignore kill errors here and let the child close/error handlers surface the failure.
179
+ }
180
+ };
181
+ const timeoutTimer = setTimeout(() => {
182
+ didTimeout = true;
183
+ terminateChild('SIGTERM');
184
+ forceKillTimer = setTimeout(() => {
185
+ terminateChild('SIGKILL');
186
+ }, PROCESS_TIMEOUT_FORCE_KILL_DELAY_MS);
187
+ forceKillTimer.unref?.();
188
+ }, timeoutMs);
189
+ timeoutTimer.unref?.();
190
+ return {
191
+ cleanup: () => {
192
+ clearTimeout(timeoutTimer);
193
+ if (forceKillTimer) {
194
+ clearTimeout(forceKillTimer);
195
+ }
196
+ },
197
+ didTimeout: () => didTimeout,
198
+ };
199
+ }
125
200
  function forwardSignalsToChild(child) {
126
201
  let forwardedSignalCount = 0;
127
202
  const listeners = new Map();
@@ -149,10 +224,11 @@ function forwardSignalsToChild(child) {
149
224
  }
150
225
  };
151
226
  }
152
- export function commandSucceeds(name, args, options) {
227
+ export async function commandSucceeds(name, args, options) {
153
228
  const cwd = resolveCwd(options?.cwd);
154
- const command = resolveCommandName(name);
155
- return new Promise((resolve) => {
229
+ const label = options?.errorName ?? name;
230
+ const command = await resolveCommandName(name);
231
+ return await new Promise((resolve, reject) => {
156
232
  const child = spawn(command, [...args], {
157
233
  cwd,
158
234
  env: {
@@ -162,15 +238,31 @@ export function commandSucceeds(name, args, options) {
162
238
  stdio: 'ignore',
163
239
  windowsHide: process.platform === 'win32',
164
240
  });
165
- child.once('error', () => resolve(false));
166
- child.once('close', (code) => resolve(code === 0));
241
+ const timeoutController = attachProcessTimeout(child, options?.timeoutMs);
242
+ child.once('error', (error) => {
243
+ timeoutController.cleanup();
244
+ const missingCommandError = createMissingCommandError(name, label, error);
245
+ if (missingCommandError) {
246
+ reject(missingCommandError);
247
+ return;
248
+ }
249
+ resolve(false);
250
+ });
251
+ child.once('close', (code) => {
252
+ timeoutController.cleanup();
253
+ if (timeoutController.didTimeout()) {
254
+ resolve(false);
255
+ return;
256
+ }
257
+ resolve(code === 0);
258
+ });
167
259
  });
168
260
  }
169
- export function commandOutput(name, args, options) {
261
+ export async function commandOutput(name, args, options) {
170
262
  const cwd = resolveCwd(options?.cwd);
171
263
  const label = options?.errorName ?? name;
172
- const command = resolveCommandName(name);
173
- return new Promise((resolve, reject) => {
264
+ const command = await resolveCommandName(name);
265
+ return await new Promise((resolve, reject) => {
174
266
  const child = spawn(command, [...args], {
175
267
  cwd,
176
268
  env: {
@@ -190,8 +282,17 @@ export function commandOutput(name, args, options) {
190
282
  child.stderr.on('data', (chunk) => {
191
283
  stderr += chunk;
192
284
  });
193
- child.once('error', reject);
285
+ const timeoutController = attachProcessTimeout(child, options?.timeoutMs);
286
+ child.once('error', (error) => {
287
+ timeoutController.cleanup();
288
+ reject(createMissingCommandError(name, label, error) ?? error);
289
+ });
194
290
  child.once('close', (code, signal) => {
291
+ timeoutController.cleanup();
292
+ if (timeoutController.didTimeout()) {
293
+ reject(new Error(`${label} timed out after ${options?.timeoutMs}ms`));
294
+ return;
295
+ }
195
296
  if (code === 0) {
196
297
  resolve(stdout.trim());
197
298
  return;
@@ -216,7 +317,7 @@ async function readCommandOutputFile(filePath) {
216
317
  export async function commandOutputViaFile(name, args, options) {
217
318
  const cwd = resolveCwd(options?.cwd);
218
319
  const label = options?.errorName ?? name;
219
- const command = resolveCommandName(name);
320
+ const command = await resolveCommandName(name);
220
321
  const captureDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-output-'));
221
322
  const stdoutPath = path.join(captureDir, 'stdout.log');
222
323
  const stderrPath = path.join(captureDir, 'stderr.log');
@@ -233,8 +334,17 @@ export async function commandOutputViaFile(name, args, options) {
233
334
  stdio: ['ignore', stdoutHandle.fd, stderrHandle.fd],
234
335
  windowsHide: process.platform === 'win32',
235
336
  });
236
- child.once('error', reject);
337
+ const timeoutController = attachProcessTimeout(child, options?.timeoutMs);
338
+ child.once('error', (error) => {
339
+ timeoutController.cleanup();
340
+ reject(createMissingCommandError(name, label, error) ?? error);
341
+ });
237
342
  child.once('close', (code, signal) => {
343
+ timeoutController.cleanup();
344
+ if (timeoutController.didTimeout()) {
345
+ reject(new Error(`${label} timed out after ${options?.timeoutMs}ms`));
346
+ return;
347
+ }
238
348
  resolve({ code, signal });
239
349
  });
240
350
  });
@@ -17,6 +17,72 @@ import { commandOutput, commandOutputViaFile, run } from './run-npm.js';
17
17
  export const NOCOBASE_SKILLS_SOURCE = 'nocobase/skills';
18
18
  export const NOCOBASE_SKILLS_PACKAGE_NAME = '@nocobase/skills';
19
19
  const NOCOBASE_SKILLS_NAME_PREFIX = 'nocobase-';
20
+ const SKILLS_LIST_TIMEOUT_MS = 5000;
21
+ const SKILLS_NPM_VIEW_TIMEOUT_MS = 3000;
22
+ const SKILLS_PACK_TIMEOUT_MS = 30000;
23
+ const SKILLS_ADD_TIMEOUT_MS = 20000;
24
+ const NPM_REGISTRY_UNAVAILABLE_PATTERNS = [
25
+ 'enotfound',
26
+ 'eai_again',
27
+ 'etimedout',
28
+ 'esockettimedout',
29
+ 'econnreset',
30
+ 'econnrefused',
31
+ 'ehostunreach',
32
+ 'enetunreach',
33
+ 'socket hang up',
34
+ 'getaddrinfo',
35
+ 'fetch failed',
36
+ 'network request to',
37
+ 'self_signed_cert',
38
+ 'depth_zero_self_signed_cert',
39
+ 'unable_to_verify_leaf_signature',
40
+ 'cert_has_expired',
41
+ 'timed out after',
42
+ ];
43
+ function collectErrorMessages(error) {
44
+ const messages = [];
45
+ const queue = [error];
46
+ const seen = new Set();
47
+ while (queue.length > 0) {
48
+ const current = queue.shift();
49
+ if (current === undefined || current === null || seen.has(current)) {
50
+ continue;
51
+ }
52
+ seen.add(current);
53
+ if (current instanceof Error) {
54
+ if (current.message) {
55
+ messages.push(current.message);
56
+ }
57
+ const cause = current.cause;
58
+ if (cause !== undefined) {
59
+ queue.push(cause);
60
+ }
61
+ continue;
62
+ }
63
+ if (typeof current === 'string') {
64
+ messages.push(current);
65
+ continue;
66
+ }
67
+ if (typeof current === 'object') {
68
+ if ('message' in current && typeof current.message === 'string') {
69
+ messages.push(current.message);
70
+ }
71
+ if ('cause' in current) {
72
+ queue.push(current.cause);
73
+ }
74
+ continue;
75
+ }
76
+ messages.push(String(current));
77
+ }
78
+ return messages;
79
+ }
80
+ export function isNpmRegistryUnavailable(error) {
81
+ return collectErrorMessages(error).some((message) => {
82
+ const normalized = message.toLowerCase();
83
+ return NPM_REGISTRY_UNAVAILABLE_PATTERNS.some((pattern) => normalized.includes(pattern));
84
+ });
85
+ }
20
86
  function normalizePath(value) {
21
87
  return path.resolve(value);
22
88
  }
@@ -61,6 +127,12 @@ async function readManagedSkillsState(workspaceRoot) {
61
127
  return undefined;
62
128
  }
63
129
  }
130
+ export async function readInstalledManagedSkillsVersion(options = {}) {
131
+ const globalRoot = resolveSkillsRoot(options);
132
+ const state = await readManagedSkillsState(globalRoot);
133
+ const installedVersion = String(state?.installedVersion ?? state?.installedRef ?? '').trim();
134
+ return installedVersion || undefined;
135
+ }
64
136
  async function writeManagedSkillsState(workspaceRoot, state) {
65
137
  const filePath = getManagedSkillsStateFile(workspaceRoot);
66
138
  await fsp.mkdir(path.dirname(filePath), { recursive: true });
@@ -72,6 +144,7 @@ export async function listGlobalSkills(options = {}) {
72
144
  const output = await (options.commandOutputFn ?? commandOutputViaFile)('npx', ['-y', 'skills', 'list', '-g', '--json'], {
73
145
  cwd: globalRoot,
74
146
  errorName: 'skills list',
147
+ timeoutMs: SKILLS_LIST_TIMEOUT_MS,
75
148
  });
76
149
  const parsed = JSON.parse(output);
77
150
  return Array.isArray(parsed) ? parsed : [];
@@ -95,6 +168,7 @@ async function readPublishedSkillsVersion(options = {}) {
95
168
  const output = await (options.commandOutputFn ?? commandOutput)('npm', ['view', NOCOBASE_SKILLS_PACKAGE_NAME, 'version', '--json'], {
96
169
  cwd: globalRoot,
97
170
  errorName: 'npm view',
171
+ timeoutMs: SKILLS_NPM_VIEW_TIMEOUT_MS,
98
172
  });
99
173
  const parsed = JSON.parse(output);
100
174
  const version = String(parsed ?? '').trim();
@@ -187,6 +261,7 @@ async function prepareLocalSkillsPackage(globalRoot, options = {}, targetVersion
187
261
  cwd: packRoot,
188
262
  stdio: options.verbose ? 'inherit' : 'ignore',
189
263
  errorName: 'npm pack',
264
+ timeoutMs: SKILLS_PACK_TIMEOUT_MS,
190
265
  });
191
266
  const tarballPath = await resolvePackedSkillsTarball(packRoot);
192
267
  await extractPackedSkillsTarball(tarballPath, cacheRoot, targetVersion);
@@ -276,6 +351,7 @@ async function reinstallManagedSkills(globalRoot, options = {}, targetVersion) {
276
351
  cwd: globalRoot,
277
352
  stdio: options.verbose ? 'inherit' : 'ignore',
278
353
  errorName: 'skills add',
354
+ timeoutMs: SKILLS_ADD_TIMEOUT_MS,
279
355
  });
280
356
  }
281
357
  finally {
@@ -314,10 +390,10 @@ export async function updateNocoBaseSkills(options = {}) {
314
390
  status,
315
391
  };
316
392
  }
317
- if (status.managedByNb
318
- && status.latestVersion
319
- && status.installedVersion
320
- && compareVersions(status.latestVersion, status.installedVersion) <= 0) {
393
+ if (status.managedByNb &&
394
+ status.latestVersion &&
395
+ status.installedVersion &&
396
+ compareVersions(status.latestVersion, status.installedVersion) <= 0) {
321
397
  return {
322
398
  action: 'noop',
323
399
  reason: 'up-to-date',
package/dist/lib/ui.js CHANGED
@@ -54,7 +54,10 @@ export function printInfo(message) {
54
54
  console.log(pc.cyan(message));
55
55
  }
56
56
  export function announceTargetEnv(envName) {
57
- printInfo(`Target env: ${envName}`);
57
+ if (process.env.NB_SKIP_TARGET_ENV_LOG === '1') {
58
+ return;
59
+ }
60
+ printInfo(`Using env "${envName}".`);
58
61
  }
59
62
  export function printVerbose(message) {
60
63
  if (!verboseMode) {
@@ -34,6 +34,8 @@
34
34
  "back": "Back",
35
35
  "next": "Next",
36
36
  "submit": "Submit & continue in terminal",
37
+ "showPassword": "Show password",
38
+ "hidePassword": "Hide password",
37
39
  "checking": "Checking...",
38
40
  "sending": "Sending...",
39
41
  "successTitle": "Success",
@@ -73,7 +75,8 @@
73
75
  "timeout": "Timed out connecting to the database at {{host}}:{{port}} after about {{seconds}} seconds.",
74
76
  "authenticationFailed": "Failed to sign in to database \"{{database}}\" with user \"{{user}}\". Check the username and password.",
75
77
  "databaseNotFound": "Database \"{{database}}\" does not exist or is not accessible with the current connection settings.",
76
- "connectionFailed": "Database connection check failed. Details: {{details}}"
78
+ "connectionFailed": "Database connection check failed. Details: {{details}}",
79
+ "lowerCaseTableNamesRequiresUnderscored": "MySQL lower_case_table_names=1 requires DB_UNDERSCORED=true."
77
80
  }
78
81
  },
79
82
  "commands": {
@@ -94,16 +97,28 @@
94
97
  },
95
98
  "authType": {
96
99
  "message": "How would you like to sign in?",
100
+ "basicLabel": "Basic login (username + password)",
101
+ "basicHint": "uses your credentials to fetch a token after save",
97
102
  "oauthLabel": "OAuth (browser login)",
98
103
  "oauthHint": "runs nb env auth after save",
99
104
  "tokenLabel": "API token / API key"
100
105
  },
106
+ "username": {
107
+ "message": "Enter the username for basic login",
108
+ "placeholder": "admin"
109
+ },
110
+ "password": {
111
+ "message": "Enter the password for basic login"
112
+ },
101
113
  "accessToken": {
102
114
  "message": "Enter an API token or API key",
103
115
  "placeholder": "Enter your API token / API key"
104
116
  }
105
117
  }
106
118
  },
119
+ "shared": {
120
+ "missingCommand": "Couldn't run `{{action}}` because the {{displayName}} executable could not be found. Install {{displayName}} or update `nb config set {{configKey}} <path>` and try again."
121
+ },
107
122
  "download": {
108
123
  "failures": {
109
124
  "dependencyInstall": {
@@ -206,6 +221,10 @@
206
221
  }
207
222
  },
208
223
  "install": {
224
+ "messages": {
225
+ "skipDownloadDockerImageMissing": "Cannot continue with `--skip-download` because Docker image \"{{imageRef}}\" is not available locally. Load or pull the image first, or run the command again without `--skip-download`.",
226
+ "skipDownloadLocalAppMissing": "Cannot continue with `--skip-download` because \"{{projectRoot}}\" is missing or does not contain a package.json file. Point `--app-root-path` to an existing NocoBase app, or run the command again without `--skip-download`."
227
+ },
209
228
  "validation": {
210
229
  "builtinDbUnsupported": "Built-in database does not support \"{{dialect}}\" yet. Choose PostgreSQL, MySQL, or MariaDB, or turn off built-in database."
211
230
  },
@@ -229,9 +248,6 @@
229
248
  "message": "Where should uploads and local files be stored?",
230
249
  "placeholder": "./<env>/storage/"
231
250
  },
232
- "fetchSource": {
233
- "message": "Download NocoBase automatically if the app directory is empty?"
234
- },
235
251
  "dbDialect": {
236
252
  "message": "Which database would you like to use?"
237
253
  },
@@ -259,6 +275,17 @@
259
275
  "dbPassword": {
260
276
  "message": "What is the database password?"
261
277
  },
278
+ "dbSchema": {
279
+ "message": "What is the database schema? (PostgreSQL only, optional)",
280
+ "placeholder": "Leave empty to use the default schema"
281
+ },
282
+ "dbTablePrefix": {
283
+ "message": "What table prefix should be used? (optional)",
284
+ "placeholder": "For example: nb_"
285
+ },
286
+ "dbUnderscored": {
287
+ "message": "Use underscored names for database tables and columns?"
288
+ },
262
289
  "rootUsername": {
263
290
  "message": "Choose the initial admin username",
264
291
  "placeholder": "nocobase"
@@ -290,7 +317,8 @@
290
317
  "uiOpening": "A local setup form will open in your browser. That form needs a person to fill it in. If you are using an AI agent, do not stop this process while the CLI waits for the submission.",
291
318
  "uiReady": "Local setup form is ready.",
292
319
  "uiReadyHelp": "If your browser does not open automatically, copy the URL below into your browser to continue. Keep this terminal session running while the CLI waits for the submission.",
293
- "uiOpenBrowserFallback": "We could not open your browser automatically. Copy the URL above into your browser to continue setup, and keep this terminal session running. If you are using an AI agent, do not stop the current process."
320
+ "uiOpenBrowserFallback": "We could not open your browser automatically. Copy the URL above into your browser to continue setup, and keep this terminal session running. If you are using an AI agent, do not stop the current process.",
321
+ "skillsSyncRegistryUnavailable": "Couldn't reach the npm registry to sync NocoBase AI coding skills. Skipping skills install and continuing init. Run `nb skills install` later when registry access is available."
294
322
  },
295
323
  "prompts": {
296
324
  "appName": {
@@ -308,6 +336,9 @@
308
336
  "apiBaseUrl": {
309
337
  "message": "API base URL",
310
338
  "placeholder": "https://demo.example.com/api or https://demo.example.com/api/__app/<subapp>"
339
+ },
340
+ "skipDownload": {
341
+ "message": "Skip downloading NocoBase and reuse existing local app files or Docker images"
311
342
  }
312
343
  },
313
344
  "webUi": {
@@ -340,5 +371,55 @@
340
371
  }
341
372
  }
342
373
  }
374
+ },
375
+ "apiCommandCompat": {
376
+ "refusalTitle": "Refusing to run `nb api {{commandId}}` because the current CLI version is {{cliVersion}} while the target app version is {{appVersion}}.",
377
+ "refusalTitleMissingVersion": "Refusing to run `nb api {{commandId}}` because the {{versionLabel}} is unavailable.",
378
+ "refusalTitleGeneric": "Refusing to run `nb api {{commandId}}` because the current version combination is not compatible.",
379
+ "currentVersionsLine": "Current versions: {{versions}}.",
380
+ "refusalBodyWithRequirements": "This API command is blocked when used with {{requirements}}.",
381
+ "refusalBody": "This API command may not be compatible with the current CLI and app version combination.",
382
+ "channel": {
383
+ "stable": "stable",
384
+ "beta": "beta",
385
+ "alpha": "alpha",
386
+ "rc": "rc",
387
+ "unknownPrerelease": "unsupported prerelease"
388
+ },
389
+ "requirement": {
390
+ "cli": "CLI version {{condition}}",
391
+ "app": "app version {{condition}}",
392
+ "skills": "NocoBase AI skills version {{condition}}",
393
+ "appChannel": "{{channelLabel}} app version {{condition}}",
394
+ "appUnknownPrereleaseBlocked": "{{channelLabel}} app versions are blocked"
395
+ },
396
+ "currentVersion": {
397
+ "cli": "CLI {{version}}",
398
+ "app": "app {{version}}",
399
+ "appUnavailable": "app unavailable",
400
+ "skills": "NocoBase AI skills {{version}}",
401
+ "skillsUnavailable": "NocoBase AI skills unavailable"
402
+ },
403
+ "versionLabel": {
404
+ "cli": "CLI version",
405
+ "app": "target app version",
406
+ "skills": "installed NocoBase AI skills version"
407
+ },
408
+ "join": {
409
+ "two": "{{left}} and {{right}}",
410
+ "many": "{{head}}, and {{tail}}"
411
+ },
412
+ "continueTitle": "To continue:",
413
+ "continueUpgradeApp": "- upgrade the app to a compatible version",
414
+ "continueUpgradeAppToVersion": "- upgrade the app to {{suggestedUpgradeTarget}}",
415
+ "continueUseSupportedAppVersion": "- use a supported app version: {{supportedTargets}}",
416
+ "continueRefreshAppVersion": "- refresh the target env runtime so the current app version can be detected, then retry",
417
+ "continueUseCompatibleCli": "- or use a compatible CLI version",
418
+ "continueUseCompatibleCliVersion": "- or use a CLI version {{suggestedCliTarget}}",
419
+ "continueUpdateSkillsToVersion": "- install or update the NocoBase AI coding skills to {{suggestedSkillsUpgradeTarget}}. Run `nb skills update --yes` to refresh the managed skills.",
420
+ "continueUseCompatibleSkillsVersion": "- or use a NocoBase AI skills version {{suggestedSkillsTarget}}",
421
+ "continueInstallOrUpdateSkillsToVersion": "- run `nb skills install --yes` or `nb skills update --yes`, and make sure the managed skills version is {{suggestedSkillsUpgradeTarget}} before retrying",
422
+ "continueInstallOrUpdateSkills": "- run `nb skills install --yes` or `nb skills update --yes`, then retry once the managed skills version is available",
423
+ "supportedAppTarget": "{{channelLabel}} {{target}}"
343
424
  }
344
425
  }