@nocobase/cli 2.1.2 → 2.1.4-rc.1
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.
- package/bin/early-locale.js +89 -0
- package/bin/run.js +3 -0
- package/bin/windows-admin.js +60 -0
- package/dist/commands/app/destroy.js +4 -3
- package/dist/commands/app/shared.js +49 -3
- package/dist/commands/examples/prompts-stages.js +2 -2
- package/dist/commands/examples/prompts-test.js +2 -2
- package/dist/commands/init.js +1 -1
- package/dist/commands/license/activate.js +4 -1
- package/dist/commands/license/shared.js +24 -15
- package/dist/commands/self/check.js +1 -1
- package/dist/commands/self/update.js +2 -2
- package/dist/commands/skills/check.js +4 -5
- package/dist/commands/skills/install.js +5 -0
- package/dist/commands/skills/update.js +17 -4
- package/dist/lib/api-command-compat.js +51 -8
- package/dist/lib/prompt-web-ui.js +7 -11
- package/dist/lib/self-manager.js +3 -0
- package/dist/lib/skills-manager.js +104 -20
- package/dist/locale/en-US.json +6 -0
- package/dist/locale/zh-CN.json +6 -0
- package/package.json +2 -3
|
@@ -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
|
-
|
|
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
|
-
|
|
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) ||
|
|
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: ({
|
|
113
|
-
this.log(`Local Web UI (multi-stage) ready — ${url} (listening on ${
|
|
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: ({
|
|
143
|
-
this.log(`Local Web UI ready — ${url} (listening on ${
|
|
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)})`);
|
package/dist/commands/init.js
CHANGED
|
@@ -471,7 +471,7 @@ Prompt modes:
|
|
|
471
471
|
default: false,
|
|
472
472
|
}),
|
|
473
473
|
'ui-host': Flags.string({
|
|
474
|
-
description: '
|
|
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:
|
|
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
|
|
14
|
-
import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_VERSION, resolveDockerImageRef
|
|
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
|
|
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',
|
|
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'],
|
|
@@ -16,6 +16,7 @@ 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',
|
|
19
20
|
'<%= config.bin %> <%= command.id %> --json',
|
|
20
21
|
];
|
|
21
22
|
static flags = {
|
|
@@ -32,6 +33,9 @@ export default class SkillsInstall extends Command {
|
|
|
32
33
|
description: 'Show detailed install output',
|
|
33
34
|
default: false,
|
|
34
35
|
}),
|
|
36
|
+
version: Flags.string({
|
|
37
|
+
description: 'Install a specific @nocobase/skills version',
|
|
38
|
+
}),
|
|
35
39
|
};
|
|
36
40
|
async run() {
|
|
37
41
|
const { flags } = await this.parse(SkillsInstall);
|
|
@@ -52,6 +56,7 @@ export default class SkillsInstall extends Command {
|
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
const result = await installNocoBaseSkills({
|
|
59
|
+
targetVersion: flags.version,
|
|
55
60
|
verbose: flags.verbose,
|
|
56
61
|
});
|
|
57
62
|
if (flags.json) {
|
|
@@ -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 } 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,7 @@ 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',
|
|
19
20
|
'<%= config.bin %> <%= command.id %> --json',
|
|
20
21
|
];
|
|
21
22
|
static flags = {
|
|
@@ -32,6 +33,9 @@ export default class SkillsUpdate extends Command {
|
|
|
32
33
|
description: 'Show detailed update output',
|
|
33
34
|
default: false,
|
|
34
35
|
}),
|
|
36
|
+
version: Flags.string({
|
|
37
|
+
description: 'Sync to a specific @nocobase/skills version',
|
|
38
|
+
}),
|
|
35
39
|
};
|
|
36
40
|
async run() {
|
|
37
41
|
const { flags } = await this.parse(SkillsUpdate);
|
|
@@ -51,8 +55,19 @@ export default class SkillsUpdate extends Command {
|
|
|
51
55
|
return;
|
|
52
56
|
}
|
|
53
57
|
}
|
|
58
|
+
const shouldShowLoading = !flags.json && !flags.verbose;
|
|
59
|
+
if (shouldShowLoading) {
|
|
60
|
+
startTask(flags.version
|
|
61
|
+
? `Syncing NocoBase AI coding skills to ${flags.version}...`
|
|
62
|
+
: 'Updating NocoBase AI coding skills...');
|
|
63
|
+
}
|
|
54
64
|
const result = await updateNocoBaseSkills({
|
|
65
|
+
targetVersion: flags.version,
|
|
55
66
|
verbose: flags.verbose,
|
|
67
|
+
}).finally(() => {
|
|
68
|
+
if (shouldShowLoading) {
|
|
69
|
+
stopTask();
|
|
70
|
+
}
|
|
56
71
|
});
|
|
57
72
|
if (flags.json) {
|
|
58
73
|
this.log(JSON.stringify({
|
|
@@ -80,8 +95,6 @@ export default class SkillsUpdate extends Command {
|
|
|
80
95
|
: 'NocoBase AI coding skills are up to date.');
|
|
81
96
|
return;
|
|
82
97
|
}
|
|
83
|
-
this.log(flags.verbose
|
|
84
|
-
? 'Updated the global NocoBase AI coding skills.'
|
|
85
|
-
: 'Updated NocoBase AI coding skills globally.');
|
|
98
|
+
this.log(flags.verbose ? 'Updated the global NocoBase AI coding skills.' : 'Updated NocoBase AI coding skills globally.');
|
|
86
99
|
}
|
|
87
100
|
}
|
|
@@ -114,12 +114,22 @@ function compareWithOperator(version, operator, expected) {
|
|
|
114
114
|
}
|
|
115
115
|
function normalizeAppByChannelComparableVersion(version) {
|
|
116
116
|
const normalized = String(version ?? '').trim();
|
|
117
|
-
const match = normalized.match(/^(\d+\.\d+\.\d+)
|
|
117
|
+
const match = normalized.match(/^(\d+\.\d+\.\d+)(?:-([0-9A-Za-z-.]+))?$/);
|
|
118
118
|
if (!match) {
|
|
119
119
|
return normalized;
|
|
120
120
|
}
|
|
121
|
-
const [, base,
|
|
122
|
-
|
|
121
|
+
const [, base, prerelease] = match;
|
|
122
|
+
if (!prerelease) {
|
|
123
|
+
return base;
|
|
124
|
+
}
|
|
125
|
+
const [channel, sequence] = prerelease.split('.');
|
|
126
|
+
if ((channel === 'alpha' || channel === 'beta') && sequence && /^\d+$/.test(sequence)) {
|
|
127
|
+
return `${base}-${channel}.${sequence}`;
|
|
128
|
+
}
|
|
129
|
+
if (channel === 'alpha' || channel === 'beta') {
|
|
130
|
+
return `${base}-${channel}`;
|
|
131
|
+
}
|
|
132
|
+
return base;
|
|
123
133
|
}
|
|
124
134
|
function normalizeAppByChannelCondition(condition) {
|
|
125
135
|
if (!condition) {
|
|
@@ -134,6 +144,42 @@ function normalizeAppByChannelCondition(condition) {
|
|
|
134
144
|
}
|
|
135
145
|
return normalized;
|
|
136
146
|
}
|
|
147
|
+
function compareAppByChannelVersions(version, expected) {
|
|
148
|
+
const comparableVersion = normalizeAppByChannelComparableVersion(version);
|
|
149
|
+
const comparableExpected = normalizeAppByChannelComparableVersion(expected);
|
|
150
|
+
if (comparableVersion === comparableExpected ||
|
|
151
|
+
comparableVersion.startsWith(`${comparableExpected}.`) ||
|
|
152
|
+
comparableVersion.startsWith(`${comparableExpected}-`)) {
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
return compareVersions(comparableVersion, comparableExpected);
|
|
156
|
+
}
|
|
157
|
+
function compareAppByChannelWithOperator(version, operator, expected) {
|
|
158
|
+
const compared = compareAppByChannelVersions(version, expected);
|
|
159
|
+
switch (operator) {
|
|
160
|
+
case 'eq':
|
|
161
|
+
return compared === 0;
|
|
162
|
+
case 'gt':
|
|
163
|
+
return compared > 0;
|
|
164
|
+
case 'gte':
|
|
165
|
+
return compared >= 0;
|
|
166
|
+
case 'lt':
|
|
167
|
+
return compared < 0;
|
|
168
|
+
case 'lte':
|
|
169
|
+
return compared <= 0;
|
|
170
|
+
default:
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function matchesAppByChannelVersionCondition(version, condition) {
|
|
175
|
+
if (!condition) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
return ['eq', 'gt', 'gte', 'lt', 'lte'].every((operator) => {
|
|
179
|
+
const expected = condition[operator];
|
|
180
|
+
return expected ? compareAppByChannelWithOperator(version, operator, expected) : true;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
137
183
|
function matchesVersionCondition(version, condition) {
|
|
138
184
|
if (!condition) {
|
|
139
185
|
return true;
|
|
@@ -168,10 +214,7 @@ function resolveAppChannel(version) {
|
|
|
168
214
|
if (channel === 'beta') {
|
|
169
215
|
return 'beta';
|
|
170
216
|
}
|
|
171
|
-
|
|
172
|
-
return 'rc';
|
|
173
|
-
}
|
|
174
|
-
return 'unknownPrerelease';
|
|
217
|
+
return 'stable';
|
|
175
218
|
}
|
|
176
219
|
function evaluateAppByChannelCondition(version, condition) {
|
|
177
220
|
if (!condition) {
|
|
@@ -208,7 +251,7 @@ function evaluateAppByChannelCondition(version, condition) {
|
|
|
208
251
|
const comparableVersion = normalizeAppByChannelComparableVersion(version);
|
|
209
252
|
const comparableCondition = normalizeAppByChannelCondition(channelCondition);
|
|
210
253
|
return {
|
|
211
|
-
result:
|
|
254
|
+
result: matchesAppByChannelVersionCondition(comparableVersion, comparableCondition) ? 'match' : 'mismatch',
|
|
212
255
|
channel,
|
|
213
256
|
condition: channelCondition,
|
|
214
257
|
};
|
|
@@ -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
|
|
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
|
|
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://${
|
|
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,
|
|
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://${
|
|
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);
|
package/dist/lib/self-manager.js
CHANGED
|
@@ -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) {
|
|
@@ -154,10 +154,13 @@ export async function listGlobalSkills(options = {}) {
|
|
|
154
154
|
export async function listProjectSkills(options = {}) {
|
|
155
155
|
return await listGlobalSkills(options);
|
|
156
156
|
}
|
|
157
|
-
function pickInstalledNocoBaseSkillNames(installedSkills, state) {
|
|
157
|
+
function pickInstalledNocoBaseSkillNames(installedSkills, state, sourceSkillNames = []) {
|
|
158
158
|
const installedNames = new Set(installedSkills.map((skill) => String(skill.name ?? '').trim()).filter(Boolean));
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
const managedNames = new Set([...sourceSkillNames, ...(state?.skillNames ?? [])]);
|
|
160
|
+
if (managedNames.size > 0) {
|
|
161
|
+
return Array.from(managedNames)
|
|
162
|
+
.filter((name) => installedNames.has(name))
|
|
163
|
+
.sort();
|
|
161
164
|
}
|
|
162
165
|
return Array.from(installedNames)
|
|
163
166
|
.filter((name) => name.startsWith(NOCOBASE_SKILLS_NAME_PREFIX))
|
|
@@ -194,6 +197,31 @@ async function readCachedSkillsVersion(cacheRoot) {
|
|
|
194
197
|
return undefined;
|
|
195
198
|
}
|
|
196
199
|
}
|
|
200
|
+
async function readCachedPackageSkillNames(globalRoot) {
|
|
201
|
+
const skillsDir = path.join(getCachedSkillsPackageDir(getSkillsCacheRoot(globalRoot)), 'skills');
|
|
202
|
+
try {
|
|
203
|
+
const entries = await fsp.readdir(skillsDir, { withFileTypes: true });
|
|
204
|
+
const skillNames = await Promise.all(entries
|
|
205
|
+
.filter((entry) => entry.isDirectory())
|
|
206
|
+
.map(async (entry) => {
|
|
207
|
+
const skillName = entry.name.trim();
|
|
208
|
+
if (!skillName) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
await fsp.access(path.join(skillsDir, skillName, 'SKILL.md'));
|
|
213
|
+
return skillName;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
}));
|
|
219
|
+
return skillNames.filter((name) => Boolean(name)).sort();
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
197
225
|
async function resolvePackedSkillsTarball(packRoot) {
|
|
198
226
|
const entries = await fsp.readdir(packRoot, { withFileTypes: true });
|
|
199
227
|
const tarballs = entries
|
|
@@ -279,14 +307,16 @@ async function prepareLocalSkillsPackage(globalRoot, options = {}, targetVersion
|
|
|
279
307
|
export async function inspectSkillsStatus(options = {}) {
|
|
280
308
|
const globalRoot = resolveSkillsRoot(options);
|
|
281
309
|
const stateFile = getManagedSkillsStateFile(globalRoot);
|
|
282
|
-
const [installedSkills, managedState] = await Promise.all([
|
|
310
|
+
const [installedSkills, managedState, cachedSkillNames] = await Promise.all([
|
|
283
311
|
listGlobalSkills({
|
|
284
312
|
globalRoot,
|
|
285
313
|
commandOutputFn: options.commandOutputFn,
|
|
286
314
|
}),
|
|
287
315
|
readManagedSkillsState(globalRoot),
|
|
316
|
+
readCachedPackageSkillNames(globalRoot),
|
|
288
317
|
]);
|
|
289
|
-
const installedSkillNames = pickInstalledNocoBaseSkillNames(installedSkills, managedState);
|
|
318
|
+
const installedSkillNames = pickInstalledNocoBaseSkillNames(installedSkills, managedState, cachedSkillNames);
|
|
319
|
+
const packageSkillNames = cachedSkillNames;
|
|
290
320
|
const managedByNb = managedState?.packageName === NOCOBASE_SKILLS_PACKAGE_NAME;
|
|
291
321
|
let latestVersion;
|
|
292
322
|
let registryError;
|
|
@@ -312,6 +342,7 @@ export async function inspectSkillsStatus(options = {}) {
|
|
|
312
342
|
managedByNb,
|
|
313
343
|
sourcePackage: managedState?.sourcePackage ?? NOCOBASE_SKILLS_SOURCE,
|
|
314
344
|
npmPackageName: managedState?.packageName ?? NOCOBASE_SKILLS_PACKAGE_NAME,
|
|
345
|
+
packageSkillNames,
|
|
315
346
|
installedSkillNames,
|
|
316
347
|
latestVersion,
|
|
317
348
|
installedVersion,
|
|
@@ -321,13 +352,18 @@ export async function inspectSkillsStatus(options = {}) {
|
|
|
321
352
|
registryError,
|
|
322
353
|
};
|
|
323
354
|
}
|
|
324
|
-
async function persistManagedSkillsState(globalRoot, options = {}) {
|
|
325
|
-
const installedSkills = await
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
355
|
+
async function persistManagedSkillsState(globalRoot, options = {}, installedVersion) {
|
|
356
|
+
const [installedSkills, managedState, cachedSkillNames] = await Promise.all([
|
|
357
|
+
listGlobalSkills({
|
|
358
|
+
globalRoot,
|
|
359
|
+
commandOutputFn: options.commandOutputFn,
|
|
360
|
+
}),
|
|
361
|
+
readManagedSkillsState(globalRoot),
|
|
362
|
+
readCachedPackageSkillNames(globalRoot),
|
|
363
|
+
]);
|
|
364
|
+
const installedSkillNames = pickInstalledNocoBaseSkillNames(installedSkills, managedState, cachedSkillNames);
|
|
365
|
+
const packageSkillNames = cachedSkillNames.length ? cachedSkillNames : installedSkillNames;
|
|
366
|
+
const cachedVersion = await readCachedSkillsVersion(getSkillsCacheRoot(globalRoot));
|
|
331
367
|
const published = await readPublishedSkillsVersion({
|
|
332
368
|
globalRoot,
|
|
333
369
|
commandOutputFn: options.commandOutputFn,
|
|
@@ -338,8 +374,8 @@ async function persistManagedSkillsState(globalRoot, options = {}) {
|
|
|
338
374
|
sourcePackage: NOCOBASE_SKILLS_SOURCE,
|
|
339
375
|
installedAt: managedState?.installedAt ?? now,
|
|
340
376
|
updatedAt: now,
|
|
341
|
-
installedVersion: published.version,
|
|
342
|
-
skillNames:
|
|
377
|
+
installedVersion: installedVersion ?? cachedVersion ?? published.version,
|
|
378
|
+
skillNames: packageSkillNames,
|
|
343
379
|
});
|
|
344
380
|
return await inspectSkillsStatus({
|
|
345
381
|
globalRoot,
|
|
@@ -349,7 +385,7 @@ async function persistManagedSkillsState(globalRoot, options = {}) {
|
|
|
349
385
|
async function reinstallManagedSkills(globalRoot, options = {}, targetVersion) {
|
|
350
386
|
const prepared = await prepareLocalSkillsPackage(globalRoot, options, targetVersion);
|
|
351
387
|
try {
|
|
352
|
-
await (options.runFn ?? run)('npx', ['-y', 'skills', 'add', prepared.packageDir, '-g', '-y'], {
|
|
388
|
+
await (options.runFn ?? run)('npx', ['-y', 'skills', 'add', prepared.packageDir, '-g', '-y', '--skill', '*'], {
|
|
353
389
|
cwd: globalRoot,
|
|
354
390
|
stdio: options.verbose ? 'inherit' : 'ignore',
|
|
355
391
|
errorName: 'skills add',
|
|
@@ -360,23 +396,50 @@ async function reinstallManagedSkills(globalRoot, options = {}, targetVersion) {
|
|
|
360
396
|
await prepared.cleanup();
|
|
361
397
|
}
|
|
362
398
|
}
|
|
399
|
+
function pickObsoleteManagedSkillNames(installedSkillNames, packageSkillNames) {
|
|
400
|
+
if (!packageSkillNames.length) {
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
const packageSkillNameSet = new Set(packageSkillNames);
|
|
404
|
+
return installedSkillNames.filter((skillName) => !packageSkillNameSet.has(skillName)).sort();
|
|
405
|
+
}
|
|
406
|
+
async function removeObsoleteManagedSkills(globalRoot, installedSkillNames, options = {}) {
|
|
407
|
+
const packageSkillNames = await readCachedPackageSkillNames(globalRoot);
|
|
408
|
+
const obsoleteSkillNames = pickObsoleteManagedSkillNames(installedSkillNames, packageSkillNames);
|
|
409
|
+
for (const skillName of obsoleteSkillNames) {
|
|
410
|
+
await (options.runFn ?? run)('npx', ['-y', 'skills', 'remove', skillName, '-g', '-y'], {
|
|
411
|
+
cwd: globalRoot,
|
|
412
|
+
stdio: options.verbose ? 'inherit' : 'ignore',
|
|
413
|
+
errorName: 'skills remove',
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
363
417
|
export async function installNocoBaseSkills(options = {}) {
|
|
364
418
|
const globalRoot = resolveSkillsRoot(options);
|
|
365
419
|
const status = await inspectSkillsStatus({
|
|
366
420
|
globalRoot,
|
|
367
421
|
commandOutputFn: options.commandOutputFn,
|
|
368
422
|
});
|
|
369
|
-
|
|
423
|
+
const cachedSkillNames = await readCachedPackageSkillNames(globalRoot);
|
|
424
|
+
const missingCachedSkillNames = cachedSkillNames.filter((name) => !status.installedSkillNames.includes(name));
|
|
425
|
+
const obsoleteSkillNames = pickObsoleteManagedSkillNames(status.installedSkillNames, cachedSkillNames);
|
|
426
|
+
const targetVersion = String(options.targetVersion ?? '').trim() || undefined;
|
|
427
|
+
const targetVersionMatches = !targetVersion || status.installedVersion === targetVersion;
|
|
428
|
+
if (status.installed && targetVersionMatches && missingCachedSkillNames.length === 0 && obsoleteSkillNames.length === 0) {
|
|
370
429
|
return {
|
|
371
430
|
action: 'noop',
|
|
372
431
|
status,
|
|
373
432
|
};
|
|
374
433
|
}
|
|
375
434
|
await ensureSkillsWorkspaceRoot(globalRoot);
|
|
376
|
-
|
|
435
|
+
if (!status.installed || !targetVersionMatches || missingCachedSkillNames.length > 0) {
|
|
436
|
+
const installVersion = targetVersion ?? status.latestVersion;
|
|
437
|
+
await reinstallManagedSkills(globalRoot, options, installVersion);
|
|
438
|
+
}
|
|
439
|
+
await removeObsoleteManagedSkills(globalRoot, status.installedSkillNames, options);
|
|
377
440
|
return {
|
|
378
441
|
action: 'installed',
|
|
379
|
-
status: await persistManagedSkillsState(globalRoot, options),
|
|
442
|
+
status: await persistManagedSkillsState(globalRoot, options, targetVersion),
|
|
380
443
|
};
|
|
381
444
|
}
|
|
382
445
|
export async function updateNocoBaseSkills(options = {}) {
|
|
@@ -385,6 +448,10 @@ export async function updateNocoBaseSkills(options = {}) {
|
|
|
385
448
|
globalRoot,
|
|
386
449
|
commandOutputFn: options.commandOutputFn,
|
|
387
450
|
});
|
|
451
|
+
const cachedSkillNames = await readCachedPackageSkillNames(globalRoot);
|
|
452
|
+
const missingCachedSkillNames = cachedSkillNames.filter((name) => !status.installedSkillNames.includes(name));
|
|
453
|
+
const obsoleteSkillNames = pickObsoleteManagedSkillNames(status.installedSkillNames, cachedSkillNames);
|
|
454
|
+
const targetVersion = String(options.targetVersion ?? '').trim() || undefined;
|
|
388
455
|
if (!status.installed) {
|
|
389
456
|
return {
|
|
390
457
|
action: 'noop',
|
|
@@ -393,8 +460,11 @@ export async function updateNocoBaseSkills(options = {}) {
|
|
|
393
460
|
};
|
|
394
461
|
}
|
|
395
462
|
if (status.managedByNb &&
|
|
463
|
+
!targetVersion &&
|
|
396
464
|
status.latestVersion &&
|
|
397
465
|
status.installedVersion &&
|
|
466
|
+
missingCachedSkillNames.length === 0 &&
|
|
467
|
+
obsoleteSkillNames.length === 0 &&
|
|
398
468
|
compareVersions(status.latestVersion, status.installedVersion) <= 0) {
|
|
399
469
|
return {
|
|
400
470
|
action: 'noop',
|
|
@@ -402,10 +472,24 @@ export async function updateNocoBaseSkills(options = {}) {
|
|
|
402
472
|
status,
|
|
403
473
|
};
|
|
404
474
|
}
|
|
405
|
-
|
|
475
|
+
if (targetVersion &&
|
|
476
|
+
status.installedVersion === targetVersion &&
|
|
477
|
+
missingCachedSkillNames.length === 0 &&
|
|
478
|
+
obsoleteSkillNames.length === 0) {
|
|
479
|
+
return {
|
|
480
|
+
action: 'noop',
|
|
481
|
+
reason: 'up-to-date',
|
|
482
|
+
status,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
if (!targetVersion || status.installedVersion !== targetVersion || missingCachedSkillNames.length > 0) {
|
|
486
|
+
const installVersion = targetVersion ?? status.latestVersion;
|
|
487
|
+
await reinstallManagedSkills(globalRoot, options, installVersion);
|
|
488
|
+
}
|
|
489
|
+
await removeObsoleteManagedSkills(globalRoot, status.installedSkillNames, options);
|
|
406
490
|
return {
|
|
407
491
|
action: 'updated',
|
|
408
|
-
status: await persistManagedSkillsState(globalRoot, options),
|
|
492
|
+
status: await persistManagedSkillsState(globalRoot, options, targetVersion),
|
|
409
493
|
};
|
|
410
494
|
}
|
|
411
495
|
export async function removeNocoBaseSkills(options = {}) {
|
package/dist/locale/en-US.json
CHANGED
|
@@ -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.",
|
package/dist/locale/zh-CN.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/cli",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.4-rc.1",
|
|
4
4
|
"description": "NocoBase Command Line Tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/generated/command-registry.js",
|
|
@@ -142,6 +142,5 @@
|
|
|
142
142
|
"repository": {
|
|
143
143
|
"type": "git",
|
|
144
144
|
"url": "git+https://github.com/nocobase/nocobase.git"
|
|
145
|
-
}
|
|
146
|
-
"gitHead": "e1b28561425c5c34ff3bc6ae1f81c66b72f02872"
|
|
145
|
+
}
|
|
147
146
|
}
|