@nocobase/cli 2.1.0-beta.17 → 2.1.0-beta.19

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.
@@ -6,18 +6,62 @@
6
6
  * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
+ /**
10
+ * This file is part of the NocoBase (R) project.
11
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
12
+ * Authors: NocoBase Team.
13
+ *
14
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
15
+ * For more information, please refer to: https://www.nocobase.com/agreement.
16
+ */
17
+ import fs from 'node:fs';
9
18
  import path from 'node:path';
10
19
  import spawn from 'cross-spawn';
20
+ const FORWARDED_SIGNALS = ['SIGINT', 'SIGTERM'];
11
21
  function resolveCommandName(name) {
12
22
  return name;
13
23
  }
14
- function resolveCwd(cwd) {
24
+ function pathExists(candidate) {
25
+ try {
26
+ return Boolean(candidate) && fs.statSync(candidate) !== undefined;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ function hasLocalNocoBaseBinary(candidate) {
33
+ return (pathExists(path.join(candidate, 'node_modules', '.bin', 'nocobase-v1'))
34
+ || pathExists(path.join(candidate, 'node_modules', '.bin', 'nocobase-v1.cmd')));
35
+ }
36
+ export function resolveCwd(cwd) {
15
37
  const next = cwd ?? process.cwd();
16
38
  if (path.isAbsolute(next)) {
17
39
  return next;
18
40
  }
19
41
  return path.resolve(process.cwd(), next);
20
42
  }
43
+ export function resolveProjectCwd(cwd) {
44
+ const normalizedCwd = typeof cwd === 'string' && cwd.trim() === '' ? undefined : cwd;
45
+ const next = normalizedCwd ?? process.cwd();
46
+ const resolvedNext = resolveCwd(normalizedCwd);
47
+ if (!normalizedCwd || path.isAbsolute(next)) {
48
+ return resolvedNext;
49
+ }
50
+ const baseCwd = process.cwd();
51
+ let current = baseCwd;
52
+ const fallback = resolvedNext;
53
+ while (true) {
54
+ const candidate = path.resolve(current, next);
55
+ if (hasLocalNocoBaseBinary(candidate)) {
56
+ return candidate;
57
+ }
58
+ const parent = path.dirname(current);
59
+ if (parent === current) {
60
+ return fallback;
61
+ }
62
+ current = parent;
63
+ }
64
+ }
21
65
  export function run(name, args, options) {
22
66
  const cwd = resolveCwd(options?.cwd);
23
67
  const label = options?.errorName ?? name;
@@ -32,8 +76,13 @@ export function run(name, args, options) {
32
76
  },
33
77
  windowsHide: process.platform === 'win32',
34
78
  });
35
- child.once('error', reject);
79
+ const cleanupSignalForwarding = forwardSignalsToChild(child);
80
+ child.once('error', (error) => {
81
+ cleanupSignalForwarding();
82
+ reject(error);
83
+ });
36
84
  child.once('close', (code, signal) => {
85
+ cleanupSignalForwarding();
37
86
  if (code === 0) {
38
87
  resolve();
39
88
  return;
@@ -46,6 +95,33 @@ export function run(name, args, options) {
46
95
  });
47
96
  });
48
97
  }
98
+ function forwardSignalsToChild(child) {
99
+ let forwardedSignalCount = 0;
100
+ const listeners = new Map();
101
+ const isChildRunning = () => child.exitCode === null && child.signalCode === null;
102
+ for (const signal of FORWARDED_SIGNALS) {
103
+ const listener = () => {
104
+ if (!isChildRunning()) {
105
+ return;
106
+ }
107
+ const nextSignal = forwardedSignalCount > 0 ? 'SIGKILL' : signal;
108
+ forwardedSignalCount += 1;
109
+ try {
110
+ child.kill(nextSignal);
111
+ }
112
+ catch {
113
+ // Ignore kill errors here and let the child close/error handlers surface the failure.
114
+ }
115
+ };
116
+ listeners.set(signal, listener);
117
+ process.on(signal, listener);
118
+ }
119
+ return () => {
120
+ for (const [signal, listener] of listeners) {
121
+ process.off(signal, listener);
122
+ }
123
+ };
124
+ }
49
125
  export function commandSucceeds(name, args, options) {
50
126
  const cwd = resolveCwd(options?.cwd);
51
127
  const command = resolveCommandName(name);
@@ -107,16 +183,14 @@ export function runNpm(args, options) {
107
183
  return run('yarn', [...args], { ...options, errorName: 'npm' });
108
184
  }
109
185
  export function runNocoBaseCommand(args, options) {
110
- let cwd = options?.cwd ?? process.cwd();
111
- if (!path.isAbsolute(cwd)) {
112
- cwd = path.resolve(process.cwd(), cwd);
113
- }
186
+ const cwd = resolveProjectCwd(options?.cwd);
114
187
  const localBin = path.join(cwd, 'node_modules', '.bin');
115
- return run('node', ['./node_modules/.bin/nocobase-v1', ...args], {
188
+ return run('nocobase-v1', [...args], {
116
189
  ...options,
190
+ cwd,
117
191
  errorName: 'nocobase command',
118
192
  env: {
119
- PATH: `${localBin}${path.delimiter}${process.env.PATH}`,
193
+ PATH: `${localBin}${path.delimiter}${process.env.PATH ?? ''}`,
120
194
  ...options?.env,
121
195
  },
122
196
  });
@@ -0,0 +1,246 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { commandOutput, run } from './run-npm.js';
13
+ const DEFAULT_PACKAGE_NAME = '@nocobase/cli';
14
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
15
+ function normalizePath(value) {
16
+ return path.resolve(value);
17
+ }
18
+ function isSubPath(parent, child) {
19
+ const relative = path.relative(normalizePath(parent), normalizePath(child));
20
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
21
+ }
22
+ function parseVersion(version) {
23
+ const normalized = String(version ?? '').trim();
24
+ const match = normalized.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-.]+))?$/);
25
+ if (!match) {
26
+ return undefined;
27
+ }
28
+ return {
29
+ major: Number(match[1]),
30
+ minor: Number(match[2]),
31
+ patch: Number(match[3]),
32
+ prerelease: match[4] ? match[4].split('.').filter(Boolean) : [],
33
+ };
34
+ }
35
+ function compareIdentifier(left, right) {
36
+ const leftNumeric = /^\d+$/.test(left);
37
+ const rightNumeric = /^\d+$/.test(right);
38
+ if (leftNumeric && rightNumeric) {
39
+ return Number(left) - Number(right);
40
+ }
41
+ if (leftNumeric) {
42
+ return -1;
43
+ }
44
+ if (rightNumeric) {
45
+ return 1;
46
+ }
47
+ return left.localeCompare(right);
48
+ }
49
+ export function compareVersions(leftVersion, rightVersion) {
50
+ const left = parseVersion(leftVersion);
51
+ const right = parseVersion(rightVersion);
52
+ if (!left || !right) {
53
+ return String(leftVersion ?? '').localeCompare(String(rightVersion ?? ''));
54
+ }
55
+ if (left.major !== right.major) {
56
+ return left.major - right.major;
57
+ }
58
+ if (left.minor !== right.minor) {
59
+ return left.minor - right.minor;
60
+ }
61
+ if (left.patch !== right.patch) {
62
+ return left.patch - right.patch;
63
+ }
64
+ if (left.prerelease.length === 0 && right.prerelease.length === 0) {
65
+ return 0;
66
+ }
67
+ if (left.prerelease.length === 0) {
68
+ return 1;
69
+ }
70
+ if (right.prerelease.length === 0) {
71
+ return -1;
72
+ }
73
+ const maxLength = Math.max(left.prerelease.length, right.prerelease.length);
74
+ for (let index = 0; index < maxLength; index += 1) {
75
+ const leftIdentifier = left.prerelease[index];
76
+ const rightIdentifier = right.prerelease[index];
77
+ if (leftIdentifier === undefined) {
78
+ return -1;
79
+ }
80
+ if (rightIdentifier === undefined) {
81
+ return 1;
82
+ }
83
+ const compared = compareIdentifier(leftIdentifier, rightIdentifier);
84
+ if (compared !== 0) {
85
+ return compared;
86
+ }
87
+ }
88
+ return 0;
89
+ }
90
+ function detectChannel(currentVersion) {
91
+ if (/-alpha(?:[.-]|$)/i.test(currentVersion)) {
92
+ return 'alpha';
93
+ }
94
+ if (/-beta(?:[.-]|$)/i.test(currentVersion)) {
95
+ return 'beta';
96
+ }
97
+ return 'latest';
98
+ }
99
+ function readCurrentVersion(packageRoot) {
100
+ const packageJsonPath = path.join(packageRoot, 'package.json');
101
+ const content = fs.readFileSync(packageJsonPath, 'utf8');
102
+ const pkg = JSON.parse(content);
103
+ return String(pkg.version ?? '').trim();
104
+ }
105
+ function detectInstallMethod(packageRoot, globalPrefix) {
106
+ if (fs.existsSync(path.join(packageRoot, 'src'))
107
+ && fs.existsSync(path.join(packageRoot, 'tsconfig.json'))) {
108
+ return 'source';
109
+ }
110
+ if (globalPrefix && isSubPath(globalPrefix, packageRoot)) {
111
+ return 'npm-global';
112
+ }
113
+ if (packageRoot.includes(`${path.sep}node_modules${path.sep}`)) {
114
+ return 'package-local';
115
+ }
116
+ return 'unknown';
117
+ }
118
+ async function readGlobalPrefix(commandOutputFn) {
119
+ try {
120
+ return (await commandOutputFn('npm', ['prefix', '-g'], {
121
+ errorName: 'npm prefix',
122
+ })).trim();
123
+ }
124
+ catch {
125
+ return undefined;
126
+ }
127
+ }
128
+ async function readDistTags(packageName, commandOutputFn) {
129
+ const output = await commandOutputFn('npm', ['view', packageName, 'dist-tags', '--json'], {
130
+ errorName: 'npm view',
131
+ });
132
+ const parsed = JSON.parse(output);
133
+ return parsed ?? {};
134
+ }
135
+ function getUnsupportedSelfUpdateReason(installMethod) {
136
+ if (installMethod === 'source') {
137
+ return [
138
+ 'This CLI is running from source in a repository checkout.',
139
+ 'Automatic self-update is only supported for standard global npm installs.',
140
+ 'Upgrade this checkout through your repo workflow instead.',
141
+ ].join(' ');
142
+ }
143
+ if (installMethod === 'package-local') {
144
+ return [
145
+ 'This CLI is installed from a local project dependency tree.',
146
+ 'Automatic self-update is only supported for standard global npm installs.',
147
+ 'Upgrade the parent project dependency that provides this CLI instead.',
148
+ ].join(' ');
149
+ }
150
+ if (installMethod === 'unknown') {
151
+ return [
152
+ 'This CLI install could not be recognized as a standard global npm install.',
153
+ 'Automatic self-update is only supported for standard global npm installs.',
154
+ ].join(' ');
155
+ }
156
+ return undefined;
157
+ }
158
+ export function getRecommendedSelfUpdateCommand(status) {
159
+ if (!status.updatable || !status.updateAvailable) {
160
+ return undefined;
161
+ }
162
+ return 'nb self update --yes';
163
+ }
164
+ export function formatSelfUpdateUnavailableMessage(status) {
165
+ if (status.registryError) {
166
+ return [
167
+ `Couldn't resolve the latest published version for ${status.packageName}.`,
168
+ 'Check your npm registry access and try again.',
169
+ `Details: ${status.registryError}`,
170
+ ].join('\n');
171
+ }
172
+ return [
173
+ `Couldn't resolve the latest published version for ${status.packageName}.`,
174
+ 'Check your npm registry access and try again.',
175
+ ].join('\n');
176
+ }
177
+ export function getSelfUpdatePackageSpec(status) {
178
+ return `${status.packageName}@${status.channel}`;
179
+ }
180
+ export async function inspectSelfStatus(options = {}) {
181
+ const packageRoot = options.packageRoot ? normalizePath(options.packageRoot) : PACKAGE_ROOT;
182
+ const packageName = options.packageName ?? DEFAULT_PACKAGE_NAME;
183
+ const currentVersion = options.currentVersion ?? readCurrentVersion(packageRoot);
184
+ const channel = options.channel && options.channel !== 'auto' ? options.channel : detectChannel(currentVersion);
185
+ const commandOutputFn = options.commandOutputFn ?? commandOutput;
186
+ const globalPrefix = await readGlobalPrefix(commandOutputFn);
187
+ const installMethod = detectInstallMethod(packageRoot, globalPrefix);
188
+ let latestVersion;
189
+ let registryError;
190
+ try {
191
+ const distTags = await readDistTags(packageName, commandOutputFn);
192
+ latestVersion = distTags[channel] || distTags.latest;
193
+ }
194
+ catch (error) {
195
+ registryError = error instanceof Error ? error.message : String(error);
196
+ }
197
+ const updateAvailable = latestVersion ? compareVersions(latestVersion, currentVersion) > 0 : false;
198
+ return {
199
+ packageName,
200
+ packageRoot,
201
+ currentVersion,
202
+ channel,
203
+ latestVersion,
204
+ updateAvailable,
205
+ installMethod,
206
+ updatable: installMethod === 'npm-global',
207
+ updateBlockedReason: getUnsupportedSelfUpdateReason(installMethod),
208
+ globalPrefix,
209
+ registryError,
210
+ };
211
+ }
212
+ export function formatUnsupportedSelfUpdateMessage(status) {
213
+ return status.updateBlockedReason
214
+ ?? [
215
+ 'Automatic self-update is only supported for standard global npm installs.',
216
+ ].join('\n');
217
+ }
218
+ export async function updateSelf(options = {}) {
219
+ const status = await inspectSelfStatus(options);
220
+ if (!status.updatable) {
221
+ throw new Error(formatUnsupportedSelfUpdateMessage(status));
222
+ }
223
+ const targetVersion = options.targetVersion ?? status.latestVersion;
224
+ if (!targetVersion) {
225
+ throw new Error(formatSelfUpdateUnavailableMessage(status));
226
+ }
227
+ if (!targetVersion || compareVersions(targetVersion, status.currentVersion) <= 0) {
228
+ return {
229
+ action: 'noop',
230
+ status,
231
+ targetVersion,
232
+ packageSpec: getSelfUpdatePackageSpec(status),
233
+ };
234
+ }
235
+ const packageSpec = getSelfUpdatePackageSpec(status);
236
+ await (options.runFn ?? run)('npm', ['install', '-g', packageSpec], {
237
+ stdio: 'inherit',
238
+ errorName: 'npm install',
239
+ });
240
+ return {
241
+ action: 'updated',
242
+ status,
243
+ targetVersion,
244
+ packageSpec,
245
+ };
246
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import fs from 'node:fs';
10
+ import fsp from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ import { commandOutput, run } from './run-npm.js';
13
+ export const NOCOBASE_SKILLS_PACKAGE = 'nocobase/skills';
14
+ export const NOCOBASE_SKILLS_REPO_URL = 'https://github.com/nocobase/skills.git';
15
+ const NOCOBASE_SKILLS_NAME_PREFIX = 'nocobase-';
16
+ function normalizePath(value) {
17
+ return path.resolve(value);
18
+ }
19
+ export function resolveSkillsWorkspaceRoot(startCwd = process.cwd()) {
20
+ let current = normalizePath(startCwd);
21
+ while (true) {
22
+ if (fs.existsSync(path.join(current, '.nocobase')) || fs.existsSync(path.join(current, '.agents'))) {
23
+ return current;
24
+ }
25
+ const parent = path.dirname(current);
26
+ if (parent === current) {
27
+ return normalizePath(startCwd);
28
+ }
29
+ current = parent;
30
+ }
31
+ }
32
+ export function getManagedSkillsStateFile(workspaceRoot) {
33
+ return path.join(workspaceRoot, '.nocobase', 'skills.json');
34
+ }
35
+ async function readManagedSkillsState(workspaceRoot) {
36
+ const filePath = getManagedSkillsStateFile(workspaceRoot);
37
+ try {
38
+ const content = await fsp.readFile(filePath, 'utf8');
39
+ return JSON.parse(content);
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
45
+ async function writeManagedSkillsState(workspaceRoot, state) {
46
+ const filePath = getManagedSkillsStateFile(workspaceRoot);
47
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
48
+ await fsp.writeFile(filePath, JSON.stringify(state, null, 2));
49
+ }
50
+ export async function listProjectSkills(options = {}) {
51
+ const workspaceRoot = options.workspaceRoot ? normalizePath(options.workspaceRoot) : resolveSkillsWorkspaceRoot();
52
+ const output = await (options.commandOutputFn ?? commandOutput)('npx', ['-y', 'skills', 'list', '--json'], {
53
+ cwd: workspaceRoot,
54
+ errorName: 'skills list',
55
+ });
56
+ const parsed = JSON.parse(output);
57
+ return Array.isArray(parsed) ? parsed : [];
58
+ }
59
+ function pickInstalledNocoBaseSkillNames(installedSkills, state) {
60
+ const installedNames = new Set(installedSkills.map((skill) => String(skill.name ?? '').trim()).filter(Boolean));
61
+ if (state?.skillNames?.length) {
62
+ return state.skillNames.filter((name) => installedNames.has(name)).sort();
63
+ }
64
+ return Array.from(installedNames)
65
+ .filter((name) => name.startsWith(NOCOBASE_SKILLS_NAME_PREFIX))
66
+ .sort();
67
+ }
68
+ export async function readNocoBaseSkillsHeadRef(options = {}) {
69
+ try {
70
+ const output = await (options.commandOutputFn ?? commandOutput)('git', ['ls-remote', NOCOBASE_SKILLS_REPO_URL, 'HEAD'], {
71
+ cwd: options.workspaceRoot ? normalizePath(options.workspaceRoot) : resolveSkillsWorkspaceRoot(),
72
+ errorName: 'git ls-remote',
73
+ });
74
+ const ref = output.trim().split(/\s+/)[0];
75
+ return { ref: ref || undefined };
76
+ }
77
+ catch (error) {
78
+ return {
79
+ error: error instanceof Error ? error.message : String(error),
80
+ };
81
+ }
82
+ }
83
+ export async function inspectSkillsStatus(options = {}) {
84
+ const workspaceRoot = options.workspaceRoot ? normalizePath(options.workspaceRoot) : resolveSkillsWorkspaceRoot();
85
+ const stateFile = getManagedSkillsStateFile(workspaceRoot);
86
+ const [installedSkills, managedState] = await Promise.all([
87
+ listProjectSkills({
88
+ workspaceRoot,
89
+ commandOutputFn: options.commandOutputFn,
90
+ }),
91
+ readManagedSkillsState(workspaceRoot),
92
+ ]);
93
+ const installedSkillNames = pickInstalledNocoBaseSkillNames(installedSkills, managedState);
94
+ const managedByNb = managedState?.packageName === NOCOBASE_SKILLS_PACKAGE;
95
+ let latestRef;
96
+ let registryError;
97
+ let updateAvailable = installedSkillNames.length > 0 ? null : false;
98
+ if (installedSkillNames.length > 0 || managedByNb) {
99
+ const remote = await readNocoBaseSkillsHeadRef({
100
+ workspaceRoot,
101
+ commandOutputFn: options.commandOutputFn,
102
+ });
103
+ latestRef = remote.ref;
104
+ registryError = remote.error;
105
+ if (managedState?.installedRef && latestRef) {
106
+ updateAvailable = latestRef !== managedState.installedRef;
107
+ }
108
+ }
109
+ return {
110
+ workspaceRoot,
111
+ stateFile,
112
+ installed: installedSkillNames.length > 0,
113
+ managedByNb,
114
+ sourcePackage: NOCOBASE_SKILLS_PACKAGE,
115
+ installedSkillNames,
116
+ latestRef,
117
+ installedRef: managedState?.installedRef,
118
+ updateAvailable,
119
+ registryError,
120
+ };
121
+ }
122
+ function formatSkillsNotInstalledMessage() {
123
+ return [
124
+ 'NocoBase AI coding skills are not installed for this workspace.',
125
+ 'Run `nb skills install` first.',
126
+ ].join('\n');
127
+ }
128
+ async function persistManagedSkillsState(workspaceRoot, options = {}) {
129
+ const installedSkills = await listProjectSkills({
130
+ workspaceRoot,
131
+ commandOutputFn: options.commandOutputFn,
132
+ });
133
+ const managedState = await readManagedSkillsState(workspaceRoot);
134
+ const installedSkillNames = pickInstalledNocoBaseSkillNames(installedSkills, managedState);
135
+ const remote = await readNocoBaseSkillsHeadRef({
136
+ workspaceRoot,
137
+ commandOutputFn: options.commandOutputFn,
138
+ });
139
+ const now = new Date().toISOString();
140
+ await writeManagedSkillsState(workspaceRoot, {
141
+ packageName: NOCOBASE_SKILLS_PACKAGE,
142
+ repoUrl: NOCOBASE_SKILLS_REPO_URL,
143
+ installedAt: managedState?.installedAt ?? now,
144
+ updatedAt: now,
145
+ installedRef: remote.ref,
146
+ skillNames: installedSkillNames,
147
+ });
148
+ return await inspectSkillsStatus({
149
+ workspaceRoot,
150
+ commandOutputFn: options.commandOutputFn,
151
+ });
152
+ }
153
+ export async function installNocoBaseSkills(options = {}) {
154
+ const workspaceRoot = options.workspaceRoot ? normalizePath(options.workspaceRoot) : resolveSkillsWorkspaceRoot();
155
+ const status = await inspectSkillsStatus({
156
+ workspaceRoot,
157
+ commandOutputFn: options.commandOutputFn,
158
+ });
159
+ if (status.installed) {
160
+ return {
161
+ action: 'noop',
162
+ status,
163
+ };
164
+ }
165
+ await (options.runFn ?? run)('npx', ['-y', 'skills', 'add', NOCOBASE_SKILLS_PACKAGE, '-y'], {
166
+ cwd: workspaceRoot,
167
+ stdio: 'inherit',
168
+ errorName: 'skills add',
169
+ });
170
+ return {
171
+ action: 'installed',
172
+ status: await persistManagedSkillsState(workspaceRoot, options),
173
+ };
174
+ }
175
+ export async function updateNocoBaseSkills(options = {}) {
176
+ const workspaceRoot = options.workspaceRoot ? normalizePath(options.workspaceRoot) : resolveSkillsWorkspaceRoot();
177
+ const status = await inspectSkillsStatus({
178
+ workspaceRoot,
179
+ commandOutputFn: options.commandOutputFn,
180
+ });
181
+ if (!status.installed) {
182
+ throw new Error(formatSkillsNotInstalledMessage());
183
+ }
184
+ if (status.managedByNb
185
+ && status.latestRef
186
+ && status.installedRef
187
+ && status.latestRef === status.installedRef) {
188
+ return {
189
+ action: 'noop',
190
+ status,
191
+ };
192
+ }
193
+ await (options.runFn ?? run)('npx', ['-y', 'skills', 'update', '-p', '-y', ...status.installedSkillNames], {
194
+ cwd: workspaceRoot,
195
+ stdio: 'inherit',
196
+ errorName: 'skills update',
197
+ });
198
+ return {
199
+ action: 'updated',
200
+ status: await persistManagedSkillsState(workspaceRoot, options),
201
+ };
202
+ }
@@ -135,13 +135,27 @@
135
135
  "prompts": {
136
136
  "source": {
137
137
  "message": "How would you like to get NocoBase?",
138
- "npmLabel": "npm package",
139
- "gitLabel": "Git repository",
140
- "dockerLabel": "Docker image"
138
+ "dockerLabel": "Docker install (Recommended)",
139
+ "dockerHint": "Best for no-code and low-maintenance setups. You can upgrade later by pulling a newer image and restarting the app.",
140
+ "npmLabel": "create-nocobase-app install",
141
+ "npmHint": "Best when you want an app project with business code kept separate from the NocoBase framework, while still supporting low-code development.",
142
+ "gitLabel": "Git source install",
143
+ "gitHint": "Best when you want to try the latest unreleased changes, contribute code, or debug and modify NocoBase source directly. This option is more developer-oriented."
141
144
  },
142
145
  "version": {
143
- "message": "Which version would you like to use? You can enter a package version, Docker image tag, or Git ref such as a branch name.",
144
- "placeholder": "alpha"
146
+ "message": "Which version would you like to use?",
147
+ "latestLabel": "latest",
148
+ "latestHint": "Stable release. Best for production use and the most predictable experience. Currently unavailable.",
149
+ "betaLabel": "beta",
150
+ "betaHint": "Preview release. Good for trying upcoming features before general release.",
151
+ "alphaLabel": "alpha",
152
+ "alphaHint": "Development release. Includes the newest changes, but may be incomplete or unstable.",
153
+ "otherLabel": "Other",
154
+ "otherHint": "Enter another package version, Docker tag, or Git ref manually, such as a branch name."
155
+ },
156
+ "otherVersion": {
157
+ "message": "Enter the version, Docker tag, or Git ref you want to use.",
158
+ "placeholder": "For example: fix/cli-v2"
145
159
  },
146
160
  "dockerRegistry": {
147
161
  "message": "Which Docker registry would you like to use? The image tag is set separately in Version.",
@@ -135,17 +135,31 @@
135
135
  "prompts": {
136
136
  "source": {
137
137
  "message": "你想通过哪种方式获取 NocoBase?",
138
- "npmLabel": "npm ",
139
- "gitLabel": "Git 仓库",
140
- "dockerLabel": "Docker 镜像"
138
+ "dockerLabel": "Docker 安装(推荐)",
139
+ "dockerHint": "适合无代码或低维护成本场景。后续升级时,拉取新镜像并重启应用即可。",
140
+ "npmLabel": "create-nocobase-app 安装",
141
+ "npmHint": "适合希望将业务代码与 NocoBase 框架保持独立的场景,同时仍然支持低代码开发。",
142
+ "gitLabel": "Git 源码安装",
143
+ "gitHint": "适合体验最新未发布版本、参与贡献,或直接修改和调试 NocoBase 源码。这个方式更偏向开发者使用。"
141
144
  },
142
145
  "version": {
143
- "message": "你想使用哪个版本?这里可以填写 npm 版本号、Docker tag,或 Git ref(例如分支名)。",
144
- "placeholder": "alpha"
146
+ "message": "你想使用哪个版本?",
147
+ "latestLabel": "latest",
148
+ "latestHint": "稳定版。适合生产环境和希望获得稳定体验的场景。当前暂不可用。",
149
+ "betaLabel": "beta",
150
+ "betaHint": "测试版。包含即将发布的新功能,适合提前体验和反馈。",
151
+ "alphaLabel": "alpha",
152
+ "alphaHint": "开发版。功能更新最快,但可能不完整或不稳定。",
153
+ "otherLabel": "其他",
154
+ "otherHint": "手动填写其他版本号、Docker tag 或 Git ref,例如分支名。"
155
+ },
156
+ "otherVersion": {
157
+ "message": "请输入你想使用的版本号、Docker tag 或 Git ref。",
158
+ "placeholder": "例如:fix/cli-v2"
145
159
  },
146
160
  "dockerRegistry": {
147
161
  "message": "你想使用哪个 Docker registry?镜像 tag 请单独在 Version 中填写。",
148
- "placeholder": "nocobase/nocobase"
162
+ "placeholder": "registry.cn-shanghai.aliyuncs.com/nocobase/nocobase"
149
163
  },
150
164
  "dockerPlatform": {
151
165
  "message": "要使用哪个 Docker 镜像平台?",
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@nocobase/cli",
3
- "version": "2.1.0-beta.17",
3
+ "version": "2.1.0-beta.19",
4
4
  "description": "NocoBase Command Line Tool",
5
5
  "type": "module",
6
6
  "main": "dist/generated/command-registry.js",
7
7
  "scripts": {
8
- "clean": "rm -rf dist",
9
- "build": "yarn clean && tsc -p tsconfig.json && mkdir -p dist/locale && cp src/locale/*.json dist/locale/",
8
+ "clean": "node ./scripts/clean.mjs",
9
+ "build": "node ./scripts/build.mjs",
10
10
  "test": "TEST_ENV=server-side yarn --cwd ../../.. vitest run packages/core/cli"
11
11
  },
12
12
  "keywords": [],
@@ -39,6 +39,12 @@
39
39
  "env": {
40
40
  "description": "Manage NocoBase project environments and update command runtimes."
41
41
  },
42
+ "self": {
43
+ "description": "Inspect or update the NocoBase CLI itself."
44
+ },
45
+ "skills": {
46
+ "description": "Inspect or synchronize NocoBase AI coding skills for the current workspace."
47
+ },
42
48
  "api": {
43
49
  "description": "Work with NocoBase API."
44
50
  }
@@ -62,5 +68,5 @@
62
68
  "type": "git",
63
69
  "url": "git+https://github.com/nocobase/nocobase.git"
64
70
  },
65
- "gitHead": "227d625e47174ab7b3a90170e7c6c67bbdf36e23"
71
+ "gitHead": "d89ab08dbcb25877de69827d5bad6823c27b2cbb"
66
72
  }