@nocobase/cli 2.2.0-beta.1 → 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.
Files changed (37) hide show
  1. package/bin/early-locale.js +89 -0
  2. package/bin/node-version.js +35 -0
  3. package/bin/run.js +9 -0
  4. package/bin/windows-admin.js +60 -0
  5. package/dist/commands/app/destroy.js +4 -3
  6. package/dist/commands/app/shared.js +49 -3
  7. package/dist/commands/examples/prompts-stages.js +2 -2
  8. package/dist/commands/examples/prompts-test.js +2 -2
  9. package/dist/commands/init.js +5 -9
  10. package/dist/commands/license/activate.js +4 -1
  11. package/dist/commands/license/shared.js +24 -15
  12. package/dist/commands/self/check.js +1 -1
  13. package/dist/commands/self/update.js +2 -2
  14. package/dist/commands/skills/check.js +4 -5
  15. package/dist/commands/skills/install.js +18 -1
  16. package/dist/commands/skills/update.js +19 -4
  17. package/dist/commands/source/download.js +1 -2
  18. package/dist/lib/api-command-compat.js +51 -8
  19. package/dist/lib/prompt-web-ui.js +7 -11
  20. package/dist/lib/self-manager.js +3 -0
  21. package/dist/lib/skills-manager.js +116 -23
  22. package/dist/locale/en-US.json +10 -4
  23. package/dist/locale/zh-CN.json +10 -4
  24. package/package.json +7 -2
  25. package/assets/env-proxy/nginx/app.conf.tpl +0 -23
  26. package/assets/env-proxy/nginx/nocobase.conf.tpl +0 -5
  27. package/assets/env-proxy/nginx/snippets/dist-location.conf +0 -5
  28. package/assets/env-proxy/nginx/snippets/gzip.conf +0 -17
  29. package/assets/env-proxy/nginx/snippets/log-format-http.conf +0 -13
  30. package/assets/env-proxy/nginx/snippets/maps-http.conf +0 -14
  31. package/assets/env-proxy/nginx/snippets/mime-types.conf +0 -98
  32. package/assets/env-proxy/nginx/snippets/proxy-location.conf +0 -17
  33. package/assets/env-proxy/nginx/snippets/spa-location.conf +0 -6
  34. package/assets/env-proxy/nginx/snippets/uploads-location.conf +0 -21
  35. package/scripts/build.mjs +0 -34
  36. package/scripts/clean.mjs +0 -9
  37. package/tsconfig.json +0 -19
@@ -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
+ }
@@ -0,0 +1,35 @@
1
+ const MINIMUM_NODE_MAJOR_VERSION = 22;
2
+
3
+ export function getNodeMajorVersion(version = process.versions.node) {
4
+ const match = String(version ?? '')
5
+ .trim()
6
+ .match(/^v?(\d+)/);
7
+
8
+ if (!match) {
9
+ return Number.NaN;
10
+ }
11
+
12
+ return Number.parseInt(match[1], 10);
13
+ }
14
+
15
+ export function isSupportedNodeVersion(
16
+ version = process.versions.node,
17
+ minimumMajorVersion = MINIMUM_NODE_MAJOR_VERSION,
18
+ ) {
19
+ const majorVersion = getNodeMajorVersion(version);
20
+ return Number.isInteger(majorVersion) && majorVersion >= minimumMajorVersion;
21
+ }
22
+
23
+ export function formatUnsupportedNodeVersionMessage(
24
+ version = process.version,
25
+ minimumMajorVersion = MINIMUM_NODE_MAJOR_VERSION,
26
+ ) {
27
+ const currentVersion = String(version ?? '').trim() || 'unknown';
28
+
29
+ return [
30
+ `[nocobase cli]: Node.js ${minimumMajorVersion} or later is required to run nb.`,
31
+ `[nocobase cli]: Current version: ${currentVersion}. Please install Node.js ${minimumMajorVersion} or later and try again.`,
32
+ ].join('\n');
33
+ }
34
+
35
+ export { MINIMUM_NODE_MAJOR_VERSION };
package/bin/run.js CHANGED
@@ -6,7 +6,9 @@ import { createRequire } from 'node:module';
6
6
  import path from 'node:path';
7
7
  import pc from 'picocolors';
8
8
  import { fileURLToPath, pathToFileURL } from 'node:url';
9
+ import { formatUnsupportedNodeVersionMessage, isSupportedNodeVersion } from './node-version.js';
9
10
  import { normalizeNodeOptions, normalizeSessionEnv } from './session-env.js';
11
+ import { ensureWindowsAdministrator } from './windows-admin.js';
10
12
 
11
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
14
  const requireFromCli = createRequire(import.meta.url);
@@ -18,9 +20,16 @@ if (process.env.NB_CLI_USE_DIST === '1') {
18
20
  isDev = false;
19
21
  }
20
22
 
23
+ if (!isSupportedNodeVersion()) {
24
+ console.error(pc.red(formatUnsupportedNodeVersionMessage(process.version)));
25
+ process.exit(1);
26
+ }
27
+
21
28
  normalizeSessionEnv();
22
29
  normalizeNodeOptions();
23
30
 
31
+ ensureWindowsAdministrator();
32
+
24
33
  /**
25
34
  * In the monorepo, plain `node` cannot load `.ts`. Re-exec once with `--import <tsx>`
26
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)})`);
@@ -246,16 +246,12 @@ function formatBrowserOpenError(error) {
246
246
  return String(error);
247
247
  }
248
248
  export default class Init extends Command {
249
- static summary = 'Set up NocoBase so coding agents can connect and work with it';
250
- static description = `Set up NocoBase for coding agents in the current workspace.
249
+ static summary = 'Set up or connect a NocoBase environment in the current workspace';
250
+ static description = `Set up NocoBase in the current workspace.
251
251
 
252
- \`nb init\` prepares a NocoBase environment that coding agents can use. It supports three setup paths:
252
+ \`nb init\` helps you install a new NocoBase app, take over managing one that already exists on this machine, or connect a remote NocoBase app and save it as a CLI env.
253
253
 
254
- - Install a new NocoBase app, then save it as a CLI env.
255
- - Take over managing an app that already exists on this machine by reusing its database.
256
- - Connect a remote NocoBase app and save it as a CLI env.
257
-
258
- It can also install NocoBase AI coding skills (\`nocobase/skills\`) so agents get the project-specific workflow guidance.
254
+ You can use the saved environment directly, or let a coding agent access it later. It can also install NocoBase AI coding skills (\`nocobase/skills\`) when you want agent-specific workflow guidance.
259
255
 
260
256
  If setup was interrupted earlier, use \`--resume\` with an existing env name to continue from the saved workspace config.
261
257
 
@@ -475,7 +471,7 @@ Prompt modes:
475
471
  default: false,
476
472
  }),
477
473
  'ui-host': Flags.string({
478
- 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)',
479
475
  }),
480
476
  'ui-port': Flags.integer({
481
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({
@@ -12,10 +12,7 @@ import { printInfo, renderTable } from '../../lib/ui.js';
12
12
  export default class SkillsCheck extends Command {
13
13
  static summary = 'Check the globally installed NocoBase AI coding skills';
14
14
  static description = 'Inspect the global NocoBase AI coding skills and report whether they are managed by the CLI and whether an update is available.';
15
- static examples = [
16
- '<%= config.bin %> <%= command.id %>',
17
- '<%= config.bin %> <%= command.id %> --json',
18
- ];
15
+ static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --json'];
19
16
  static flags = {
20
17
  json: Flags.boolean({
21
18
  description: 'Output the result as JSON',
@@ -25,6 +22,7 @@ export default class SkillsCheck extends Command {
25
22
  async run() {
26
23
  const { flags } = await this.parse(SkillsCheck);
27
24
  const status = await inspectSkillsStatus();
25
+ const displaySkillNames = status.packageSkillNames.length ? status.packageSkillNames : status.installedSkillNames;
28
26
  if (flags.json) {
29
27
  this.log(JSON.stringify({
30
28
  ok: true,
@@ -35,6 +33,7 @@ export default class SkillsCheck extends Command {
35
33
  managedByNb: status.managedByNb,
36
34
  sourcePackage: status.sourcePackage,
37
35
  npmPackageName: status.npmPackageName,
36
+ packageSkillNames: status.packageSkillNames,
38
37
  installedSkillNames: status.installedSkillNames,
39
38
  installedVersion: status.installedVersion,
40
39
  latestVersion: status.latestVersion,
@@ -50,7 +49,7 @@ export default class SkillsCheck extends Command {
50
49
  ['Skills home', status.globalRoot],
51
50
  ['Installed', status.installed ? 'yes' : 'no'],
52
51
  ['Managed by nb', status.managedByNb ? 'yes' : 'no'],
53
- ['Installed skills', status.installedSkillNames.length ? status.installedSkillNames.join(', ') : '(none)'],
52
+ ['Installed skills', displaySkillNames.length ? displaySkillNames.join(', ') : '(none)'],
54
53
  ['Installed version', status.installedVersion ?? '(unknown)'],
55
54
  ['Latest version', status.latestVersion ?? '(unknown)'],
56
55
  ['Update available', status.updateAvailable === null ? 'unknown' : status.updateAvailable ? 'yes' : 'no'],
@@ -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';
@@ -16,6 +16,8 @@ export default class SkillsInstall extends Command {
16
16
  static examples = [
17
17
  '<%= config.bin %> <%= command.id %>',
18
18
  '<%= config.bin %> <%= command.id %> --yes',
19
+ '<%= config.bin %> <%= command.id %> --version 1.0.4',
20
+ '<%= config.bin %> <%= command.id %> --verbose',
19
21
  '<%= config.bin %> <%= command.id %> --json',
20
22
  ];
21
23
  static flags = {
@@ -32,6 +34,9 @@ export default class SkillsInstall extends Command {
32
34
  description: 'Show detailed install output',
33
35
  default: false,
34
36
  }),
37
+ version: Flags.string({
38
+ description: 'Install a specific @nocobase/skills version',
39
+ }),
35
40
  };
36
41
  async run() {
37
42
  const { flags } = await this.parse(SkillsInstall);
@@ -51,8 +56,20 @@ export default class SkillsInstall extends Command {
51
56
  return;
52
57
  }
53
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
+ }
54
65
  const result = await installNocoBaseSkills({
66
+ targetVersion: flags.version,
55
67
  verbose: flags.verbose,
68
+ onProgress: shouldShowLoading ? updateTask : undefined,
69
+ }).finally(() => {
70
+ if (shouldShowLoading) {
71
+ stopTask();
72
+ }
56
73
  });
57
74
  if (flags.json) {
58
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 } 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';
@@ -16,6 +16,8 @@ export default class SkillsUpdate extends Command {
16
16
  static examples = [
17
17
  '<%= config.bin %> <%= command.id %>',
18
18
  '<%= config.bin %> <%= command.id %> --yes',
19
+ '<%= config.bin %> <%= command.id %> --version 1.0.4',
20
+ '<%= config.bin %> <%= command.id %> --verbose',
19
21
  '<%= config.bin %> <%= command.id %> --json',
20
22
  ];
21
23
  static flags = {
@@ -32,6 +34,9 @@ export default class SkillsUpdate extends Command {
32
34
  description: 'Show detailed update output',
33
35
  default: false,
34
36
  }),
37
+ version: Flags.string({
38
+ description: 'Sync to a specific @nocobase/skills version',
39
+ }),
35
40
  };
36
41
  async run() {
37
42
  const { flags } = await this.parse(SkillsUpdate);
@@ -51,8 +56,20 @@ export default class SkillsUpdate extends Command {
51
56
  return;
52
57
  }
53
58
  }
59
+ const shouldShowLoading = !flags.json && !flags.verbose;
60
+ if (shouldShowLoading) {
61
+ startTask(flags.version
62
+ ? `Syncing NocoBase AI coding skills to ${flags.version}...`
63
+ : 'Updating NocoBase AI coding skills...');
64
+ }
54
65
  const result = await updateNocoBaseSkills({
66
+ targetVersion: flags.version,
55
67
  verbose: flags.verbose,
68
+ onProgress: shouldShowLoading ? updateTask : undefined,
69
+ }).finally(() => {
70
+ if (shouldShowLoading) {
71
+ stopTask();
72
+ }
56
73
  });
57
74
  if (flags.json) {
58
75
  this.log(JSON.stringify({
@@ -80,8 +97,6 @@ export default class SkillsUpdate extends Command {
80
97
  : 'NocoBase AI coding skills are up to date.');
81
98
  return;
82
99
  }
83
- this.log(flags.verbose
84
- ? 'Updated the global NocoBase AI coding skills.'
85
- : 'Updated NocoBase AI coding skills globally.');
100
+ this.log(flags.verbose ? 'Updated the global NocoBase AI coding skills.' : 'Updated NocoBase AI coding skills globally.');
86
101
  }
87
102
  }