@nocobase/cli 2.2.0-beta.2 → 2.2.0-beta.3

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.
@@ -0,0 +1,89 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ export function normalizeEarlyCliLocale(value) {
7
+ const normalized = String(value ?? '')
8
+ .trim()
9
+ .replace(/\..*$/, '')
10
+ .replace(/_/g, '-')
11
+ .toLowerCase();
12
+
13
+ if (normalized === 'zh' || normalized.startsWith('zh-')) {
14
+ return 'zh-CN';
15
+ }
16
+
17
+ if (normalized === 'en' || normalized.startsWith('en-')) {
18
+ return 'en-US';
19
+ }
20
+
21
+ return undefined;
22
+ }
23
+
24
+ function readConfiguredEarlyCliLocale() {
25
+ try {
26
+ const cliHomeRoot = String(process.env.NB_CLI_ROOT ?? '').trim() || os.homedir();
27
+ const configPath = path.join(cliHomeRoot, '.nocobase', 'config.json');
28
+ const content = fs.readFileSync(configPath, 'utf8');
29
+ const parsed = JSON.parse(content);
30
+ return normalizeEarlyCliLocale(parsed?.settings?.locale);
31
+ } catch {
32
+ return undefined;
33
+ }
34
+ }
35
+
36
+ export function detectEarlyCliLocale() {
37
+ const candidates = [
38
+ process.env.NB_LOCALE,
39
+ readConfiguredEarlyCliLocale(),
40
+ process.env.LC_ALL,
41
+ process.env.LC_MESSAGES,
42
+ process.env.LANG,
43
+ Intl.DateTimeFormat().resolvedOptions().locale,
44
+ ];
45
+
46
+ for (const candidate of candidates) {
47
+ const locale = normalizeEarlyCliLocale(candidate);
48
+ if (locale) {
49
+ return locale;
50
+ }
51
+ }
52
+
53
+ return 'en-US';
54
+ }
55
+
56
+ function getEarlyLocalePathValue(input, key) {
57
+ let current = input;
58
+ for (const part of key.split('.')) {
59
+ if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, part)) {
60
+ return undefined;
61
+ }
62
+ current = current[part];
63
+ }
64
+ return typeof current === 'string' ? current : undefined;
65
+ }
66
+
67
+ function readEarlyLocaleMessages(locale) {
68
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
69
+ const packageRoot = path.resolve(moduleDir, '..');
70
+ const localePaths = [
71
+ path.join(packageRoot, 'src', 'locale', `${locale}.json`),
72
+ path.join(packageRoot, 'dist', 'locale', `${locale}.json`),
73
+ ];
74
+
75
+ for (const localePath of localePaths) {
76
+ try {
77
+ return JSON.parse(fs.readFileSync(localePath, 'utf8'));
78
+ } catch {
79
+ // Try the next runtime layout.
80
+ }
81
+ }
82
+
83
+ return undefined;
84
+ }
85
+
86
+ export function translateEarlyCli(key, fallback, locale = detectEarlyCliLocale()) {
87
+ const messages = readEarlyLocaleMessages(locale);
88
+ return getEarlyLocalePathValue(messages, key) ?? fallback;
89
+ }
package/bin/run.js CHANGED
@@ -8,6 +8,7 @@ import pc from 'picocolors';
8
8
  import { fileURLToPath, pathToFileURL } from 'node:url';
9
9
  import { formatUnsupportedNodeVersionMessage, isSupportedNodeVersion } from './node-version.js';
10
10
  import { normalizeNodeOptions, normalizeSessionEnv } from './session-env.js';
11
+ import { ensureWindowsAdministrator } from './windows-admin.js';
11
12
 
12
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
14
  const requireFromCli = createRequire(import.meta.url);
@@ -27,6 +28,8 @@ if (!isSupportedNodeVersion()) {
27
28
  normalizeSessionEnv();
28
29
  normalizeNodeOptions();
29
30
 
31
+ ensureWindowsAdministrator();
32
+
30
33
  /**
31
34
  * In the monorepo, plain `node` cannot load `.ts`. Re-exec once with `--import <tsx>`
32
35
  * (same effect as a dedicated dev entry with `#!/usr/bin/env -S node --import tsx`).
@@ -0,0 +1,60 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import pc from 'picocolors';
3
+ import { detectEarlyCliLocale, translateEarlyCli } from './early-locale.js';
4
+
5
+ const windowsAdministratorCheckScript = [
6
+ '$identity = [Security.Principal.WindowsIdentity]::GetCurrent();',
7
+ '$principal = New-Object Security.Principal.WindowsPrincipal($identity);',
8
+ 'if ($principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit 0 }',
9
+ 'exit 1',
10
+ ].join(' ');
11
+
12
+ export function formatWindowsAdministratorRequiredMessage() {
13
+ const locale = detectEarlyCliLocale();
14
+ const message = translateEarlyCli(
15
+ 'entry.windowsAdministratorRequired.message',
16
+ 'NocoBase CLI must be run as Administrator on Windows.',
17
+ locale,
18
+ );
19
+ const hint = translateEarlyCli(
20
+ 'entry.windowsAdministratorRequired.hint',
21
+ 'Open your terminal as Administrator, then run the command again.',
22
+ locale,
23
+ );
24
+
25
+ return [message, hint].join('\n');
26
+ }
27
+
28
+ function isWindowsAdministrator() {
29
+ for (const command of ['pwsh.exe', 'powershell.exe']) {
30
+ const result = spawnSync(
31
+ command,
32
+ ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', windowsAdministratorCheckScript],
33
+ {
34
+ stdio: 'ignore',
35
+ windowsHide: true,
36
+ },
37
+ );
38
+
39
+ if (result.error?.code === 'ENOENT') {
40
+ continue;
41
+ }
42
+
43
+ return result.status === 0;
44
+ }
45
+
46
+ return false;
47
+ }
48
+
49
+ export function ensureWindowsAdministrator() {
50
+ if (process.platform !== 'win32' || process.env.NB_CLI_WINDOWS_ADMIN_CHECKED === '1') {
51
+ return;
52
+ }
53
+
54
+ if (!isWindowsAdministrator()) {
55
+ console.error(pc.red(formatWindowsAdministratorRequiredMessage()));
56
+ process.exit(1);
57
+ }
58
+
59
+ process.env.NB_CLI_WINDOWS_ADMIN_CHECKED = '1';
60
+ }
@@ -143,6 +143,7 @@ export default class AppDestroy extends Command {
143
143
  }
144
144
  announceTargetEnv(runtime.envName);
145
145
  try {
146
+ const retryCommand = `nb env remove ${runtime.envName} --purge --force`;
146
147
  if (runtime.kind === 'docker') {
147
148
  startTask(`Removing Docker app container for "${runtime.envName}"...`);
148
149
  const state = await removeDockerContainerIfExists(runtime.containerName, {
@@ -192,7 +193,7 @@ export default class AppDestroy extends Command {
192
193
  const localAppPath = resolveManagedLocalAppPath(runtime);
193
194
  if (localAppPath && removesManagedLocalAppFiles) {
194
195
  startTask(`Removing managed local app files for "${runtime.envName}"...`);
195
- await removePathIfExists(localAppPath, `managed app files for "${runtime.envName}"`);
196
+ await removePathIfExists(localAppPath, `managed app files for "${runtime.envName}"`, { retryCommand });
196
197
  succeedTask(`Managed local app files removed for "${runtime.envName}".`);
197
198
  }
198
199
  else {
@@ -206,13 +207,13 @@ export default class AppDestroy extends Command {
206
207
  ];
207
208
  startTask(`Removing proxy entry files for "${runtime.envName}"...`);
208
209
  for (const proxyEntryDir of proxyEntryDirs) {
209
- await removePathIfExists(proxyEntryDir, `proxy entry files for "${runtime.envName}"`);
210
+ await removePathIfExists(proxyEntryDir, `proxy entry files for "${runtime.envName}"`, { retryCommand });
210
211
  }
211
212
  succeedTask(`Proxy entry files removed for "${runtime.envName}".`);
212
213
  const configuredStoragePath = resolveConfiguredStoragePath(runtime.env.config);
213
214
  if (configuredStoragePath) {
214
215
  startTask(`Removing storage data for "${runtime.envName}"...`);
215
- await removePathIfExists(configuredStoragePath, `storage data for "${runtime.envName}"`);
216
+ await removePathIfExists(configuredStoragePath, `storage data for "${runtime.envName}"`, { retryCommand });
216
217
  succeedTask(`Storage data removed for "${runtime.envName}".`);
217
218
  }
218
219
  else {
@@ -25,10 +25,54 @@ function assertSafeRemovalPath(target, label) {
25
25
  throw new Error(`Refusing to remove ${label} at "${resolved}" because it is too broad.`);
26
26
  }
27
27
  }
28
- export async function removePathIfExists(target, label) {
28
+ function getErrorCode(error) {
29
+ if (!(error instanceof Error)) {
30
+ return undefined;
31
+ }
32
+ const { code } = error;
33
+ return typeof code === 'string' ? code : undefined;
34
+ }
35
+ function isPermissionDeniedError(error) {
36
+ const code = getErrorCode(error);
37
+ return code === 'EACCES' || code === 'EPERM';
38
+ }
39
+ function formatOriginalError(error) {
40
+ const message = error instanceof Error ? error.message : String(error);
41
+ const code = getErrorCode(error);
42
+ return code && !message.includes(code) ? `${code}: ${message}` : message;
43
+ }
44
+ function quoteShellValue(value) {
45
+ return `"${value.replace(/(["\\$`])/g, '\\$1')}"`;
46
+ }
47
+ function formatPermissionDeniedRemovalError(target, label, error, options) {
48
+ const retryLines = os.platform() === 'win32' ? [] : [` sudo chown -R "$(id -u):$(id -g)" ${quoteShellValue(target)}`];
49
+ if (options.retryCommand) {
50
+ retryLines.push(` ${options.retryCommand}`);
51
+ }
52
+ return [
53
+ `Failed to remove ${label} at "${target}".`,
54
+ 'The current user cannot delete one or more files under this path. Files may have been created by a Docker container running as root.',
55
+ '',
56
+ retryLines.length > 0
57
+ ? 'Fix ownership or permissions, then retry:'
58
+ : 'Fix ownership or permissions, then retry the command.',
59
+ ...retryLines,
60
+ '',
61
+ `Original error: ${formatOriginalError(error)}`,
62
+ ].join('\n');
63
+ }
64
+ export async function removePathIfExists(target, label, options = {}) {
29
65
  const resolved = path.resolve(target);
30
66
  assertSafeRemovalPath(resolved, label);
31
- await fsp.rm(resolved, { recursive: true, force: true });
67
+ try {
68
+ await fsp.rm(resolved, { recursive: true, force: true });
69
+ }
70
+ catch (error) {
71
+ if (isPermissionDeniedError(error)) {
72
+ throw new Error(formatPermissionDeniedRemovalError(resolved, label, error, options));
73
+ }
74
+ throw error;
75
+ }
32
76
  }
33
77
  function isMissingDockerContainerError(error) {
34
78
  const message = error instanceof Error ? error.message : String(error);
@@ -116,7 +160,9 @@ export function managedDockerNetworkName(runtime) {
116
160
  return runtime.dockerNetworkName?.trim() || runtime.workspaceName?.trim() || undefined;
117
161
  }
118
162
  export function resolveManagedLocalAppPath(runtime) {
119
- return resolveConfiguredAppPath(runtime.env.config) || runtime.projectRoot || resolveConfiguredPath(runtime.env.config.appRootPath);
163
+ return (resolveConfiguredAppPath(runtime.env.config) ||
164
+ runtime.projectRoot ||
165
+ resolveConfiguredPath(runtime.env.config.appRootPath));
120
166
  }
121
167
  export function shouldRemoveManagedLocalAppFiles(runtime) {
122
168
  return runtime.source === 'npm' || runtime.source === 'git' || runtime.source === 'local';
@@ -109,8 +109,8 @@ export default class PromptsStages extends Command {
109
109
  values: presetValues,
110
110
  pageTitle: 'nb prompts-stages — Web UI',
111
111
  documentHeading: 'nb prompts-stages — `stages` demo',
112
- onServerStart: ({ host, port, url }) => {
113
- this.log(`Local Web UI (multi-stage) ready — ${url} (listening on ${host}:${port}). Submit the form in the browser to continue.`);
112
+ onServerStart: ({ listenHost, port, url }) => {
113
+ this.log(`Local Web UI (multi-stage) ready — ${url} (listening on ${listenHost}:${port}). Submit the form in the browser to continue.`);
114
114
  },
115
115
  onOpenBrowserError: (url, err) => {
116
116
  this.log(`Open this URL in a browser: ${url} (${err instanceof Error ? err.message : String(err)})`);
@@ -139,8 +139,8 @@ export default class PromptsTest extends Command {
139
139
  values: presetValues,
140
140
  pageTitle: 'nb prompts-test — UI',
141
141
  documentHeading: 'nb prompts-test',
142
- onServerStart: ({ host, port, url }) => {
143
- this.log(`Local Web UI ready — ${url} (listening on ${host}:${port}). Submit the form in the browser to continue.`);
142
+ onServerStart: ({ listenHost, port, url }) => {
143
+ this.log(`Local Web UI ready — ${url} (listening on ${listenHost}:${port}). Submit the form in the browser to continue.`);
144
144
  },
145
145
  onOpenBrowserError: (url, err) => {
146
146
  this.log(`Open this URL in a browser: ${url} (${err instanceof Error ? err.message : String(err)})`);
@@ -471,7 +471,7 @@ Prompt modes:
471
471
  default: false,
472
472
  }),
473
473
  'ui-host': Flags.string({
474
- description: 'Host for the local --ui setup server (default: 127.0.0.1)',
474
+ description: 'Browser-accessible host for the --ui setup page URL (default: 127.0.0.1)',
475
475
  }),
476
476
  'ui-port': Flags.integer({
477
477
  description: 'Port for the local --ui setup server; 0 lets the OS choose an available port',
@@ -143,11 +143,14 @@ export default class LicenseActivate extends Command {
143
143
  const validation = await validateLicenseKey(runtime, resolvedKey);
144
144
  const ok = !validation.keyStatus && validation.envMatch && validation.domainMatch && validation.licenseStatus === 'active';
145
145
  const licenseKeyPath = ok ? await saveLicenseKey(runtime, resolvedKey) : resolveLicenseKeyFile(runtime);
146
+ const shouldResolveInstanceId = Boolean(interactiveKeyFlowInstanceId || ok || (flags.json && !validation.keyStatus));
146
147
  const payload = {
147
148
  ok,
148
149
  env: runtime.envName,
149
150
  kind: runtime.kind,
150
- instanceId: interactiveKeyFlowInstanceId ?? (await ensureInstanceId(runtime)),
151
+ instanceId: shouldResolveInstanceId
152
+ ? interactiveKeyFlowInstanceId ?? (await ensureInstanceId(runtime))
153
+ : undefined,
151
154
  mode: 'key',
152
155
  key: redactLicenseKey(resolvedKey),
153
156
  keyFile: keyFile || undefined,
@@ -10,8 +10,8 @@ import { Flags } from '@oclif/core';
10
10
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
11
11
  import path from 'node:path';
12
12
  import { getEnvAsync, getInstanceIdAsync, keyDecrypt } from '@nocobase/license-kit';
13
- import { checkExternalDbConnection, readExternalDbConnectionConfig, } from "../../lib/db-connection-check.js";
14
- import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_VERSION, resolveDockerImageRef, } from "../../lib/docker-image.js";
13
+ import { checkExternalDbConnection, readExternalDbConnectionConfig } from "../../lib/db-connection-check.js";
14
+ import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_VERSION, resolveDockerImageRef } from "../../lib/docker-image.js";
15
15
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime } from '../../lib/app-runtime.js';
16
16
  import { buildRuntimeEnvVars } from '../../lib/runtime-env-vars.js';
17
17
  import { resolveLicensePkgUrlFromConfig } from '../../lib/cli-config.js';
@@ -102,12 +102,7 @@ function buildDockerLicenseDbFlagArgs(envVars) {
102
102
  ];
103
103
  }
104
104
  async function runDockerLicenseJsonCommand(runtime, commandArgs) {
105
- const args = [
106
- 'run',
107
- '--rm',
108
- '--network',
109
- runtime.dockerNetworkName || runtime.workspaceName,
110
- ];
105
+ const args = ['run', '--rm', '--network', runtime.dockerNetworkName || runtime.workspaceName];
111
106
  const dockerPlatform = normalizeDockerPlatform(runtime.env.config?.dockerPlatform);
112
107
  if (dockerPlatform) {
113
108
  args.push('--platform', dockerPlatform);
@@ -193,11 +188,11 @@ export async function generateValidatedInstanceIdFromEnvVars(envVars) {
193
188
  }
194
189
  async function generateInstanceIdForDockerRuntime(runtime) {
195
190
  const envVars = await buildRuntimeEnvVars(runtime);
196
- const payload = await runDockerLicenseJsonCommand(runtime, [
191
+ const payload = (await runDockerLicenseJsonCommand(runtime, [
197
192
  'license',
198
193
  'generate-id',
199
194
  ...buildDockerLicenseDbFlagArgs(envVars),
200
- ]);
195
+ ]));
201
196
  const instanceId = trimValue(payload.instanceId);
202
197
  if (!instanceId) {
203
198
  throw new Error('Docker instance ID generation did not return an instance ID.');
@@ -337,7 +332,7 @@ export async function getLicenseStatus(keyData) {
337
332
  }),
338
333
  signal: controller.signal,
339
334
  });
340
- const payload = await response.json();
335
+ const payload = (await response.json());
341
336
  return payload?.data?.status === 'active' ? 'active' : 'invalid';
342
337
  }
343
338
  catch {
@@ -356,6 +351,22 @@ export async function validateLicenseKey(runtime, key) {
356
351
  catch {
357
352
  keyStatus = 'invalid';
358
353
  }
354
+ if (keyStatus) {
355
+ const currentDomain = appUrl(runtime);
356
+ return {
357
+ current: {
358
+ env: undefined,
359
+ domain: currentDomain ? new URL(currentDomain).host : '',
360
+ },
361
+ keyData,
362
+ keyStatus,
363
+ dbMatch: false,
364
+ sysMatch: false,
365
+ envMatch: false,
366
+ domainMatch: false,
367
+ licenseStatus: 'invalid',
368
+ };
369
+ }
359
370
  const currentEnv = await getCurrentLicenseEnv(runtime);
360
371
  const currentDomain = appUrl(runtime);
361
372
  const dbMatch = isDbMatch(currentEnv, keyData);
@@ -391,7 +402,7 @@ export async function resolveLicenseServiceUrl(value) {
391
402
  return (await resolveLicensePkgUrl(value)).replace(/\/+$/, '');
392
403
  }
393
404
  export async function resolveLicensePkgUrl(value) {
394
- const normalized = String(value ?? '').trim() || await resolveLicensePkgUrlFromConfig();
405
+ const normalized = String(value ?? '').trim() || (await resolveLicensePkgUrlFromConfig());
395
406
  return normalized.replace(/\/+$/, '') + '/';
396
407
  }
397
408
  function shouldRedactOutputKey(key) {
@@ -414,9 +425,7 @@ export function sanitizeLicenseOutput(value) {
414
425
  if (value && typeof value === 'object') {
415
426
  return Object.fromEntries(Object.entries(value).map(([key, nestedValue]) => [
416
427
  key,
417
- shouldRedactOutputKey(key)
418
- ? redactOutputValue(String(nestedValue ?? ''))
419
- : sanitizeLicenseOutput(nestedValue),
428
+ shouldRedactOutputKey(key) ? redactOutputValue(String(nestedValue ?? '')) : sanitizeLicenseOutput(nestedValue),
420
429
  ]));
421
430
  }
422
431
  return value;
@@ -20,7 +20,7 @@ export default class SelfCheck extends Command {
20
20
  static flags = {
21
21
  channel: Flags.string({
22
22
  description: 'Release channel to compare against. Defaults to the current CLI channel.',
23
- options: ['auto', 'latest', 'beta', 'alpha'],
23
+ options: ['auto', 'latest', 'test', 'beta', 'alpha'],
24
24
  default: 'auto',
25
25
  }),
26
26
  json: Flags.boolean({
@@ -31,12 +31,12 @@ export default class SelfUpdate extends Command {
31
31
  '<%= config.bin %> <%= command.id %>',
32
32
  '<%= config.bin %> <%= command.id %> --yes',
33
33
  '<%= config.bin %> <%= command.id %> --skills',
34
- '<%= config.bin %> <%= command.id %> --channel alpha --json',
34
+ '<%= config.bin %> <%= command.id %> --channel test --json',
35
35
  ];
36
36
  static flags = {
37
37
  channel: Flags.string({
38
38
  description: 'Release channel to update to. Defaults to the current CLI channel.',
39
- options: ['auto', 'latest', 'beta', 'alpha'],
39
+ options: ['auto', 'latest', 'test', 'beta', 'alpha'],
40
40
  default: 'auto',
41
41
  }),
42
42
  yes: Flags.boolean({
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
10
  import { confirm } from "../../lib/inquirer.js";
11
- import { setVerboseMode } from '../../lib/ui.js';
11
+ import { setVerboseMode, startTask, stopTask, updateTask } from '../../lib/ui.js';
12
12
  import { installNocoBaseSkills } from '../../lib/skills-manager.js';
13
13
  export default class SkillsInstall extends Command {
14
14
  static summary = 'Install the NocoBase AI coding skills globally';
@@ -17,6 +17,7 @@ export default class SkillsInstall extends Command {
17
17
  '<%= config.bin %> <%= command.id %>',
18
18
  '<%= config.bin %> <%= command.id %> --yes',
19
19
  '<%= config.bin %> <%= command.id %> --version 1.0.4',
20
+ '<%= config.bin %> <%= command.id %> --verbose',
20
21
  '<%= config.bin %> <%= command.id %> --json',
21
22
  ];
22
23
  static flags = {
@@ -55,9 +56,20 @@ export default class SkillsInstall extends Command {
55
56
  return;
56
57
  }
57
58
  }
59
+ const shouldShowLoading = !flags.json && !flags.verbose;
60
+ if (shouldShowLoading) {
61
+ startTask(flags.version
62
+ ? `Installing NocoBase AI coding skills ${flags.version}...`
63
+ : 'Installing NocoBase AI coding skills...');
64
+ }
58
65
  const result = await installNocoBaseSkills({
59
66
  targetVersion: flags.version,
60
67
  verbose: flags.verbose,
68
+ onProgress: shouldShowLoading ? updateTask : undefined,
69
+ }).finally(() => {
70
+ if (shouldShowLoading) {
71
+ stopTask();
72
+ }
61
73
  });
62
74
  if (flags.json) {
63
75
  this.log(JSON.stringify({
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
10
  import { confirm } from "../../lib/inquirer.js";
11
- import { setVerboseMode, startTask, stopTask } from '../../lib/ui.js';
11
+ import { setVerboseMode, startTask, stopTask, updateTask } from '../../lib/ui.js';
12
12
  import { updateNocoBaseSkills } from '../../lib/skills-manager.js';
13
13
  export default class SkillsUpdate extends Command {
14
14
  static summary = 'Update the globally installed NocoBase AI coding skills';
@@ -17,6 +17,7 @@ export default class SkillsUpdate extends Command {
17
17
  '<%= config.bin %> <%= command.id %>',
18
18
  '<%= config.bin %> <%= command.id %> --yes',
19
19
  '<%= config.bin %> <%= command.id %> --version 1.0.4',
20
+ '<%= config.bin %> <%= command.id %> --verbose',
20
21
  '<%= config.bin %> <%= command.id %> --json',
21
22
  ];
22
23
  static flags = {
@@ -64,6 +65,7 @@ export default class SkillsUpdate extends Command {
64
65
  const result = await updateNocoBaseSkills({
65
66
  targetVersion: flags.version,
66
67
  verbose: flags.verbose,
68
+ onProgress: shouldShowLoading ? updateTask : undefined,
67
69
  }).finally(() => {
68
70
  if (shouldShowLoading) {
69
71
  stopTask();
@@ -19,7 +19,8 @@ export const PWC_FORM_META_STEP = '_pwcStep';
19
19
  /** Form POST JSON meta field: current field key when validating a single field. */
20
20
  export const PWC_FORM_META_FIELD = '_pwcField';
21
21
  const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
22
- const DEFAULT_HOST = '127.0.0.1';
22
+ const DEFAULT_PUBLIC_HOST = '127.0.0.1';
23
+ const LISTEN_HOST = '0.0.0.0';
23
24
  function resolveUiText(text, locale, fallback = '') {
24
25
  return resolveLocalizedText(text, { locale, fallback });
25
26
  }
@@ -705,7 +706,7 @@ function runPromptCatalogWebUIImpl(options) {
705
706
  const initialShow = reflowWebFormState(merged, Object.fromEntries(Object.entries(formDefaults).map(([k, v]) => [k, v])), userSeed).show;
706
707
  const submitPath = options.submitPath ?? DEFAULT_SUBMIT;
707
708
  const reflowPath = options.reflowPath ?? DEFAULT_REFLOW;
708
- const host = options.host ?? DEFAULT_HOST;
709
+ const publicHost = options.host ?? DEFAULT_PUBLIC_HOST;
709
710
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
710
711
  const pageTitle = resolveUiText(options.pageTitle, locale, t('promptCatalog.web.pageTitle'));
711
712
  const h1 = resolveUiText(options.documentHeading, locale, t('promptCatalog.web.documentHeading'));
@@ -753,7 +754,7 @@ function runPromptCatalogWebUIImpl(options) {
753
754
  }
754
755
  };
755
756
  const servePage = (port) => {
756
- const base = `http://${host}:${port}`;
757
+ const base = `http://${publicHost}:${port}`;
757
758
  const formInner = buildPwcFormHtml(catalog, formDefaults, initialShow, pwcStepDefs, 0, pwcNSteps, locale, uiText);
758
759
  const wizardClientJson = JSON.stringify({ n: pwcNSteps, stepDefs: pwcStepDefs });
759
760
  const pwcValStepUrl = pwcNSteps > 1 ? JSON.stringify(base + resolveValidateStepPath) : 'null';
@@ -2071,11 +2072,6 @@ function runPromptCatalogWebUIImpl(options) {
2071
2072
  return page;
2072
2073
  };
2073
2074
  server = createServer((req, res) => {
2074
- if (!req.socket.remoteAddress ||
2075
- !['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(req.socket.remoteAddress)) {
2076
- res.writeHead(403).end();
2077
- return;
2078
- }
2079
2075
  if (req.method === 'GET' && (req.url === '/' || req.url === '')) {
2080
2076
  const addr = server?.address();
2081
2077
  const port = typeof addr === 'object' && addr ? Number(addr.port) : 0;
@@ -2212,15 +2208,15 @@ function runPromptCatalogWebUIImpl(options) {
2212
2208
  }
2213
2209
  res.writeHead(404).end();
2214
2210
  });
2215
- server.listen(options.port ?? 0, host, () => {
2211
+ server.listen(options.port ?? 0, LISTEN_HOST, () => {
2216
2212
  const addr = server?.address();
2217
2213
  if (typeof addr !== 'object' || !addr) {
2218
2214
  rejectAndClose(new Error('Failed to bind HTTP server'));
2219
2215
  return;
2220
2216
  }
2221
2217
  const port = addr.port;
2222
- const startUrl = `http://${host}:${port}/`;
2223
- options.onServerStart?.({ host, port, url: startUrl });
2218
+ const startUrl = `http://${publicHost}:${port}/`;
2219
+ options.onServerStart?.({ host: publicHost, listenHost: LISTEN_HOST, port, url: startUrl });
2224
2220
  const onOpenBrowserError = options.onOpenBrowserError ?? ((u, err) => console.warn(String(err), u));
2225
2221
  try {
2226
2222
  openUrlInDefaultBrowser(startUrl, onOpenBrowserError);
@@ -97,6 +97,9 @@ function detectChannel(currentVersion) {
97
97
  if (/-beta(?:[.-]|$)/i.test(currentVersion)) {
98
98
  return 'beta';
99
99
  }
100
+ if (/-test(?:[.-]|$)/i.test(currentVersion)) {
101
+ return 'test';
102
+ }
100
103
  return 'latest';
101
104
  }
102
105
  function readCurrentVersion(packageRoot) {
@@ -21,8 +21,8 @@ const NOCOBASE_SKILLS_NAME_PREFIX = 'nocobase-';
21
21
  // resolves and boots the package, even when the local skills installation is healthy.
22
22
  const SKILLS_LIST_TIMEOUT_MS = 15000;
23
23
  const SKILLS_NPM_VIEW_TIMEOUT_MS = 3000;
24
- const SKILLS_PACK_TIMEOUT_MS = 30000;
25
- const SKILLS_ADD_TIMEOUT_MS = 20000;
24
+ const SKILLS_PACK_TIMEOUT_MS = 120000;
25
+ const SKILLS_ADD_TIMEOUT_MS = 120000;
26
26
  const NPM_REGISTRY_UNAVAILABLE_PATTERNS = [
27
27
  'enotfound',
28
28
  'eai_again',
@@ -279,6 +279,7 @@ async function prepareLocalSkillsPackage(globalRoot, options = {}, targetVersion
279
279
  const cachedVersion = await readCachedSkillsVersion(cacheRoot);
280
280
  await fsp.mkdir(cacheRoot, { recursive: true });
281
281
  if (targetVersion && cachedVersion && compareVersions(cachedVersion, targetVersion) === 0) {
282
+ options.onProgress?.(`Using cached ${NOCOBASE_SKILLS_PACKAGE_NAME}@${targetVersion}...`);
282
283
  return {
283
284
  packageDir,
284
285
  cleanup: async () => undefined,
@@ -287,12 +288,14 @@ async function prepareLocalSkillsPackage(globalRoot, options = {}, targetVersion
287
288
  await fsp.rm(packRoot, { recursive: true, force: true });
288
289
  await fsp.mkdir(packRoot, { recursive: true });
289
290
  try {
290
- await (options.runFn ?? run)('npm', ['pack', '--silent', packageSpec], {
291
+ options.onProgress?.(`Downloading ${packageSpec}...`);
292
+ await (options.runFn ?? run)('npm', ['pack', ...(options.verbose ? [] : ['--silent']), packageSpec], {
291
293
  cwd: packRoot,
292
294
  stdio: options.verbose ? 'inherit' : 'ignore',
293
295
  errorName: 'npm pack',
294
296
  timeoutMs: SKILLS_PACK_TIMEOUT_MS,
295
297
  });
298
+ options.onProgress?.(`Extracting ${NOCOBASE_SKILLS_PACKAGE_NAME}...`);
296
299
  const tarballPath = await resolvePackedSkillsTarball(packRoot);
297
300
  await extractPackedSkillsTarball(tarballPath, cacheRoot, targetVersion);
298
301
  }
@@ -385,6 +388,7 @@ async function persistManagedSkillsState(globalRoot, options = {}, installedVers
385
388
  async function reinstallManagedSkills(globalRoot, options = {}, targetVersion) {
386
389
  const prepared = await prepareLocalSkillsPackage(globalRoot, options, targetVersion);
387
390
  try {
391
+ options.onProgress?.('Installing NocoBase AI coding skills globally...');
388
392
  await (options.runFn ?? run)('npx', ['-y', 'skills', 'add', prepared.packageDir, '-g', '-y', '--skill', '*'], {
389
393
  cwd: globalRoot,
390
394
  stdio: options.verbose ? 'inherit' : 'ignore',
@@ -407,6 +411,7 @@ async function removeObsoleteManagedSkills(globalRoot, installedSkillNames, opti
407
411
  const packageSkillNames = await readCachedPackageSkillNames(globalRoot);
408
412
  const obsoleteSkillNames = pickObsoleteManagedSkillNames(installedSkillNames, packageSkillNames);
409
413
  for (const skillName of obsoleteSkillNames) {
414
+ options.onProgress?.(`Removing obsolete skill ${skillName}...`);
410
415
  await (options.runFn ?? run)('npx', ['-y', 'skills', 'remove', skillName, '-g', '-y'], {
411
416
  cwd: globalRoot,
412
417
  stdio: options.verbose ? 'inherit' : 'ignore',
@@ -416,6 +421,7 @@ async function removeObsoleteManagedSkills(globalRoot, installedSkillNames, opti
416
421
  }
417
422
  export async function installNocoBaseSkills(options = {}) {
418
423
  const globalRoot = resolveSkillsRoot(options);
424
+ options.onProgress?.('Checking installed NocoBase AI coding skills...');
419
425
  const status = await inspectSkillsStatus({
420
426
  globalRoot,
421
427
  commandOutputFn: options.commandOutputFn,
@@ -437,6 +443,7 @@ export async function installNocoBaseSkills(options = {}) {
437
443
  await reinstallManagedSkills(globalRoot, options, installVersion);
438
444
  }
439
445
  await removeObsoleteManagedSkills(globalRoot, status.installedSkillNames, options);
446
+ options.onProgress?.('Verifying installed NocoBase AI coding skills...');
440
447
  return {
441
448
  action: 'installed',
442
449
  status: await persistManagedSkillsState(globalRoot, options, targetVersion),
@@ -444,6 +451,7 @@ export async function installNocoBaseSkills(options = {}) {
444
451
  }
445
452
  export async function updateNocoBaseSkills(options = {}) {
446
453
  const globalRoot = resolveSkillsRoot(options);
454
+ options.onProgress?.('Checking installed NocoBase AI coding skills...');
447
455
  const status = await inspectSkillsStatus({
448
456
  globalRoot,
449
457
  commandOutputFn: options.commandOutputFn,
@@ -487,6 +495,7 @@ export async function updateNocoBaseSkills(options = {}) {
487
495
  await reinstallManagedSkills(globalRoot, options, installVersion);
488
496
  }
489
497
  await removeObsoleteManagedSkills(globalRoot, status.installedSkillNames, options);
498
+ options.onProgress?.('Verifying installed NocoBase AI coding skills...');
490
499
  return {
491
500
  action: 'updated',
492
501
  status: await persistManagedSkillsState(globalRoot, options, targetVersion),
@@ -1,4 +1,10 @@
1
1
  {
2
+ "entry": {
3
+ "windowsAdministratorRequired": {
4
+ "message": "NocoBase CLI must be run as Administrator on Windows.",
5
+ "hint": "Open your terminal as Administrator, then run the command again."
6
+ }
7
+ },
2
8
  "promptCatalog": {
3
9
  "common": {
4
10
  "cancelled": "Cancelled.",
@@ -1,4 +1,10 @@
1
1
  {
2
+ "entry": {
3
+ "windowsAdministratorRequired": {
4
+ "message": "Windows 上运行 NocoBase CLI 必须使用管理员模式。",
5
+ "hint": "请以管理员身份打开终端,然后重新执行命令。"
6
+ }
7
+ },
2
8
  "promptCatalog": {
3
9
  "common": {
4
10
  "cancelled": "已取消。",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/cli",
3
- "version": "2.2.0-beta.2",
3
+ "version": "2.2.0-beta.3",
4
4
  "description": "NocoBase Command Line Tool",
5
5
  "type": "module",
6
6
  "main": "dist/generated/command-registry.js",
@@ -143,5 +143,5 @@
143
143
  "type": "git",
144
144
  "url": "git+https://github.com/nocobase/nocobase.git"
145
145
  },
146
- "gitHead": "2fe3e6a86fc1d0cd039349a68b2e4d1d6945ce45"
146
+ "gitHead": "7b16bb2cfd427c110c6671252138cd85155723c5"
147
147
  }