@nocobase/cli 2.1.0-beta.35 → 2.1.0-beta.37

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 (39) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/bin/run.js +3 -2
  4. package/dist/commands/app/upgrade.js +38 -16
  5. package/dist/commands/backup/create.js +147 -0
  6. package/dist/commands/backup/index.js +20 -0
  7. package/dist/commands/backup/restore.js +105 -0
  8. package/dist/commands/config/delete.js +4 -0
  9. package/dist/commands/config/get.js +4 -0
  10. package/dist/commands/config/set.js +5 -1
  11. package/dist/commands/env/add.js +129 -15
  12. package/dist/commands/env/auth.js +145 -12
  13. package/dist/commands/env/info.js +52 -8
  14. package/dist/commands/env/list.js +2 -2
  15. package/dist/commands/env/shared.js +41 -3
  16. package/dist/commands/init.js +254 -136
  17. package/dist/commands/install.js +447 -272
  18. package/dist/commands/license/activate.js +6 -4
  19. package/dist/commands/source/publish.js +17 -0
  20. package/dist/commands/v1.js +210 -0
  21. package/dist/lib/app-managed-resources.js +20 -1
  22. package/dist/lib/app-runtime.js +13 -4
  23. package/dist/lib/auth-store.js +69 -18
  24. package/dist/lib/backup.js +171 -0
  25. package/dist/lib/bootstrap.js +23 -13
  26. package/dist/lib/cli-config.js +99 -4
  27. package/dist/lib/cli-locale.js +19 -7
  28. package/dist/lib/db-connection-check.js +61 -0
  29. package/dist/lib/env-auth.js +79 -0
  30. package/dist/lib/env-config.js +8 -1
  31. package/dist/lib/prompt-validators.js +23 -5
  32. package/dist/lib/prompt-web-ui.js +143 -19
  33. package/dist/lib/run-npm.js +166 -30
  34. package/dist/lib/skills-manager.js +74 -4
  35. package/dist/lib/source-publish.js +20 -1
  36. package/dist/lib/source-registry.js +2 -2
  37. package/dist/locale/en-US.json +36 -5
  38. package/dist/locale/zh-CN.json +36 -5
  39. package/package.json +6 -3
@@ -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 {
@@ -31,9 +61,17 @@ function pathExists(candidate) {
31
61
  return false;
32
62
  }
33
63
  }
64
+ function isDirectory(candidate) {
65
+ try {
66
+ return Boolean(candidate) && fs.statSync(candidate).isDirectory();
67
+ }
68
+ catch {
69
+ return false;
70
+ }
71
+ }
34
72
  function hasLocalNocoBaseBinary(candidate) {
35
- return (pathExists(path.join(candidate, 'node_modules', '.bin', 'nocobase-v1'))
36
- || 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')));
37
75
  }
38
76
  export function resolveCwd(cwd) {
39
77
  const next = cwd ?? process.cwd();
@@ -45,29 +83,31 @@ export function resolveCwd(cwd) {
45
83
  export function resolveProjectCwd(cwd) {
46
84
  const normalizedCwd = typeof cwd === 'string' && cwd.trim() === '' ? undefined : cwd;
47
85
  const fallback = resolveCwd(normalizedCwd);
48
- const isAbsoluteInput = typeof normalizedCwd === 'string' && path.isAbsolute(normalizedCwd);
49
- let current = isAbsoluteInput ? fallback : process.cwd();
50
- while (true) {
51
- const candidate = isAbsoluteInput
52
- ? current
53
- : normalizedCwd
54
- ? path.resolve(current, normalizedCwd)
55
- : current;
56
- if (hasLocalNocoBaseBinary(candidate)) {
57
- return candidate;
58
- }
86
+ const hasExplicitInput = normalizedCwd !== undefined;
87
+ if (hasExplicitInput && !pathExists(fallback)) {
88
+ throw new Error(`The specified --cwd does not exist: ${fallback}`);
89
+ }
90
+ if (hasExplicitInput && !isDirectory(fallback)) {
91
+ throw new Error(`The specified --cwd is not a directory: ${fallback}`);
92
+ }
93
+ let current = hasExplicitInput ? fallback : process.cwd();
94
+ while (!hasLocalNocoBaseBinary(current)) {
59
95
  const parent = path.dirname(current);
60
96
  if (parent === current) {
97
+ if (hasExplicitInput) {
98
+ throw new Error(`Couldn't find a NocoBase source project from --cwd: ${fallback}`);
99
+ }
61
100
  return fallback;
62
101
  }
63
102
  current = parent;
64
103
  }
104
+ return current;
65
105
  }
66
- export function run(name, args, options) {
106
+ export async function run(name, args, options) {
67
107
  const cwd = resolveCwd(options?.cwd);
68
108
  const label = options?.errorName ?? name;
69
- const command = resolveCommandName(name);
70
- return new Promise((resolve, reject) => {
109
+ const command = await resolveCommandName(name);
110
+ return await new Promise((resolve, reject) => {
71
111
  const child = spawn(command, [...args], {
72
112
  stdio: options?.stdio ?? 'inherit',
73
113
  cwd,
@@ -77,13 +117,34 @@ export function run(name, args, options) {
77
117
  },
78
118
  windowsHide: process.platform === 'win32',
79
119
  });
120
+ if (options?.stdio === 'pipe') {
121
+ child.stdout?.setEncoding('utf8');
122
+ child.stderr?.setEncoding('utf8');
123
+ if (options.onStdout) {
124
+ child.stdout?.on('data', (chunk) => {
125
+ options.onStdout?.(String(chunk));
126
+ });
127
+ }
128
+ if (options.onStderr) {
129
+ child.stderr?.on('data', (chunk) => {
130
+ options.onStderr?.(String(chunk));
131
+ });
132
+ }
133
+ }
80
134
  const cleanupSignalForwarding = forwardSignalsToChild(child);
135
+ const timeoutController = attachProcessTimeout(child, options?.timeoutMs);
81
136
  child.once('error', (error) => {
137
+ timeoutController.cleanup();
82
138
  cleanupSignalForwarding();
83
- reject(error);
139
+ reject(createMissingCommandError(name, label, error) ?? error);
84
140
  });
85
141
  child.once('close', (code, signal) => {
142
+ timeoutController.cleanup();
86
143
  cleanupSignalForwarding();
144
+ if (timeoutController.didTimeout()) {
145
+ reject(new Error(`${label} timed out after ${options?.timeoutMs}ms`));
146
+ return;
147
+ }
87
148
  if (code === 0) {
88
149
  resolve();
89
150
  return;
@@ -96,6 +157,46 @@ export function run(name, args, options) {
96
157
  });
97
158
  });
98
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
+ }
99
200
  function forwardSignalsToChild(child) {
100
201
  let forwardedSignalCount = 0;
101
202
  const listeners = new Map();
@@ -123,10 +224,11 @@ function forwardSignalsToChild(child) {
123
224
  }
124
225
  };
125
226
  }
126
- export function commandSucceeds(name, args, options) {
227
+ export async function commandSucceeds(name, args, options) {
127
228
  const cwd = resolveCwd(options?.cwd);
128
- const command = resolveCommandName(name);
129
- return new Promise((resolve) => {
229
+ const label = options?.errorName ?? name;
230
+ const command = await resolveCommandName(name);
231
+ return await new Promise((resolve, reject) => {
130
232
  const child = spawn(command, [...args], {
131
233
  cwd,
132
234
  env: {
@@ -136,15 +238,31 @@ export function commandSucceeds(name, args, options) {
136
238
  stdio: 'ignore',
137
239
  windowsHide: process.platform === 'win32',
138
240
  });
139
- child.once('error', () => resolve(false));
140
- 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
+ });
141
259
  });
142
260
  }
143
- export function commandOutput(name, args, options) {
261
+ export async function commandOutput(name, args, options) {
144
262
  const cwd = resolveCwd(options?.cwd);
145
263
  const label = options?.errorName ?? name;
146
- const command = resolveCommandName(name);
147
- return new Promise((resolve, reject) => {
264
+ const command = await resolveCommandName(name);
265
+ return await new Promise((resolve, reject) => {
148
266
  const child = spawn(command, [...args], {
149
267
  cwd,
150
268
  env: {
@@ -164,8 +282,17 @@ export function commandOutput(name, args, options) {
164
282
  child.stderr.on('data', (chunk) => {
165
283
  stderr += chunk;
166
284
  });
167
- 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
+ });
168
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
+ }
169
296
  if (code === 0) {
170
297
  resolve(stdout.trim());
171
298
  return;
@@ -190,7 +317,7 @@ async function readCommandOutputFile(filePath) {
190
317
  export async function commandOutputViaFile(name, args, options) {
191
318
  const cwd = resolveCwd(options?.cwd);
192
319
  const label = options?.errorName ?? name;
193
- const command = resolveCommandName(name);
320
+ const command = await resolveCommandName(name);
194
321
  const captureDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-output-'));
195
322
  const stdoutPath = path.join(captureDir, 'stdout.log');
196
323
  const stderrPath = path.join(captureDir, 'stderr.log');
@@ -207,8 +334,17 @@ export async function commandOutputViaFile(name, args, options) {
207
334
  stdio: ['ignore', stdoutHandle.fd, stderrHandle.fd],
208
335
  windowsHide: process.platform === 'win32',
209
336
  });
210
- 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
+ });
211
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
+ }
212
348
  resolve({ code, signal });
213
349
  });
214
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
  }
@@ -72,6 +138,7 @@ export async function listGlobalSkills(options = {}) {
72
138
  const output = await (options.commandOutputFn ?? commandOutputViaFile)('npx', ['-y', 'skills', 'list', '-g', '--json'], {
73
139
  cwd: globalRoot,
74
140
  errorName: 'skills list',
141
+ timeoutMs: SKILLS_LIST_TIMEOUT_MS,
75
142
  });
76
143
  const parsed = JSON.parse(output);
77
144
  return Array.isArray(parsed) ? parsed : [];
@@ -95,6 +162,7 @@ async function readPublishedSkillsVersion(options = {}) {
95
162
  const output = await (options.commandOutputFn ?? commandOutput)('npm', ['view', NOCOBASE_SKILLS_PACKAGE_NAME, 'version', '--json'], {
96
163
  cwd: globalRoot,
97
164
  errorName: 'npm view',
165
+ timeoutMs: SKILLS_NPM_VIEW_TIMEOUT_MS,
98
166
  });
99
167
  const parsed = JSON.parse(output);
100
168
  const version = String(parsed ?? '').trim();
@@ -187,6 +255,7 @@ async function prepareLocalSkillsPackage(globalRoot, options = {}, targetVersion
187
255
  cwd: packRoot,
188
256
  stdio: options.verbose ? 'inherit' : 'ignore',
189
257
  errorName: 'npm pack',
258
+ timeoutMs: SKILLS_PACK_TIMEOUT_MS,
190
259
  });
191
260
  const tarballPath = await resolvePackedSkillsTarball(packRoot);
192
261
  await extractPackedSkillsTarball(tarballPath, cacheRoot, targetVersion);
@@ -276,6 +345,7 @@ async function reinstallManagedSkills(globalRoot, options = {}, targetVersion) {
276
345
  cwd: globalRoot,
277
346
  stdio: options.verbose ? 'inherit' : 'ignore',
278
347
  errorName: 'skills add',
348
+ timeoutMs: SKILLS_ADD_TIMEOUT_MS,
279
349
  });
280
350
  }
281
351
  finally {
@@ -314,10 +384,10 @@ export async function updateNocoBaseSkills(options = {}) {
314
384
  status,
315
385
  };
316
386
  }
317
- if (status.managedByNb
318
- && status.latestVersion
319
- && status.installedVersion
320
- && compareVersions(status.latestVersion, status.installedVersion) <= 0) {
387
+ if (status.managedByNb &&
388
+ status.latestVersion &&
389
+ status.installedVersion &&
390
+ compareVersions(status.latestVersion, status.installedVersion) <= 0) {
321
391
  return {
322
392
  action: 'noop',
323
393
  reason: 'up-to-date',
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import path from 'node:path';
10
- import { commandOutput, resolveProjectCwd, run } from './run-npm.js';
10
+ import { commandOutput, resolveProjectCwd, run, runNocoBaseCommand } from './run-npm.js';
11
11
  import { DEFAULT_SOURCE_REGISTRY_PORT, parseSourceRegistryUrl, resolveSourceRegistryInfo } from './source-registry.js';
12
12
  function trimValue(value) {
13
13
  return String(value ?? '').trim();
@@ -98,6 +98,16 @@ async function commitSourceSnapshotVersion(params) {
98
98
  errorName: 'git commit',
99
99
  });
100
100
  }
101
+ async function buildSourceSnapshot(params) {
102
+ const args = ['build'];
103
+ if (!params.buildDts) {
104
+ args.push('--no-dts');
105
+ }
106
+ await runNocoBaseCommand(args, {
107
+ cwd: params.cwd,
108
+ stdio: params.stdio,
109
+ });
110
+ }
101
111
  async function createSourcePublishStash(params) {
102
112
  if (!(await hasLocalGitChanges(params.cwd))) {
103
113
  return undefined;
@@ -158,6 +168,8 @@ export async function publishSourceSnapshot(params) {
158
168
  const version = buildSnapshotVersion(baseVersion, gitSha, params.now);
159
169
  const temporaryBranch = buildSourcePublishBranchName(gitSha, params.now);
160
170
  const stdio = params.verbose ? 'inherit' : 'ignore';
171
+ const shouldBuild = params.build !== false;
172
+ const shouldBuildDts = params.buildDts !== false;
161
173
  let stash;
162
174
  let onTemporaryBranch = false;
163
175
  let branchCreated = false;
@@ -186,6 +198,13 @@ export async function publishSourceSnapshot(params) {
186
198
  errorName: 'git stash apply',
187
199
  });
188
200
  }
201
+ if (shouldBuild) {
202
+ await buildSourceSnapshot({
203
+ cwd: projectRoot,
204
+ buildDts: shouldBuildDts,
205
+ stdio,
206
+ });
207
+ }
189
208
  await run('yarn', ['lerna', 'version', version, '--force-publish=*', '--no-git-tag-version', '-y'], {
190
209
  cwd: projectRoot,
191
210
  errorName: 'lerna version',
@@ -9,7 +9,7 @@
9
9
  import fsp from 'node:fs/promises';
10
10
  import path from 'node:path';
11
11
  import { commandOutput, commandSucceeds, resolveCwd, resolveProjectCwd, run } from './run-npm.js';
12
- import { resolveCliHomeRoot } from './cli-home.js';
12
+ import { resolveCliHomeDir } from './cli-home.js';
13
13
  export const DEFAULT_SOURCE_REGISTRY_HOST = '127.0.0.1';
14
14
  export const DEFAULT_SOURCE_REGISTRY_PORT = 4873;
15
15
  export const DEFAULT_SOURCE_REGISTRY_CONTAINER_NAME = 'nb-source-registry';
@@ -31,7 +31,7 @@ function asPosixPathForDockerMount(value) {
31
31
  return resolveCwd(value).replace(/\\/g, '/');
32
32
  }
33
33
  export function resolveSourceRegistryRootDir() {
34
- return path.join(resolveCliHomeRoot(), 'verdaccio');
34
+ return path.join(resolveCliHomeDir(), 'verdaccio');
35
35
  }
36
36
  export function resolveSourceRegistryConfigPath() {
37
37
  return path.join(resolveSourceRegistryRootDir(), 'config.yaml');
@@ -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": {