@nocobase/cli 2.2.0-beta.1 → 2.2.0-beta.10

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 (53) hide show
  1. package/assets/env-proxy/nginx/snippets/proxy-location.conf +1 -0
  2. package/bin/early-locale.js +89 -0
  3. package/bin/node-version.js +35 -0
  4. package/bin/run.js +9 -0
  5. package/bin/windows-admin.js +60 -0
  6. package/dist/commands/app/destroy.js +4 -3
  7. package/dist/commands/app/restart.js +38 -0
  8. package/dist/commands/app/shared.js +49 -3
  9. package/dist/commands/app/start.js +95 -0
  10. package/dist/commands/app/upgrade.js +11 -0
  11. package/dist/commands/env/info.js +11 -1
  12. package/dist/commands/examples/prompts-stages.js +2 -2
  13. package/dist/commands/examples/prompts-test.js +2 -2
  14. package/dist/commands/init.js +33 -11
  15. package/dist/commands/install.js +159 -107
  16. package/dist/commands/license/activate.js +4 -1
  17. package/dist/commands/license/shared.js +24 -15
  18. package/dist/commands/proxy/caddy/generate.js +93 -7
  19. package/dist/commands/proxy/nginx/generate.js +98 -7
  20. package/dist/commands/revision/create.js +1 -1
  21. package/dist/commands/self/check.js +1 -1
  22. package/dist/commands/self/update.js +4 -4
  23. package/dist/commands/skills/check.js +4 -5
  24. package/dist/commands/skills/install.js +18 -1
  25. package/dist/commands/skills/update.js +19 -4
  26. package/dist/commands/source/dev.js +9 -5
  27. package/dist/commands/source/download.js +85 -16
  28. package/dist/lib/api-command-compat.js +51 -8
  29. package/dist/lib/app-managed-resources.js +104 -5
  30. package/dist/lib/auth-store.js +102 -12
  31. package/dist/lib/cli-config.js +73 -1
  32. package/dist/lib/docker-image.js +94 -6
  33. package/dist/lib/env-auth.js +291 -45
  34. package/dist/lib/env-config.js +11 -0
  35. package/dist/lib/env-proxy-config.js +48 -0
  36. package/dist/lib/env-proxy.js +164 -58
  37. package/dist/lib/hook-script.js +160 -0
  38. package/dist/lib/prompt-catalog-terminal.js +32 -19
  39. package/dist/lib/prompt-validators.js +1 -1
  40. package/dist/lib/prompt-web-ui.js +20 -13
  41. package/dist/lib/proxy-caddy.js +77 -9
  42. package/dist/lib/proxy-nginx.js +71 -11
  43. package/dist/lib/run-npm.js +4 -0
  44. package/dist/lib/self-manager.js +254 -46
  45. package/dist/lib/skills-manager.js +116 -23
  46. package/dist/lib/source-publish.js +2 -2
  47. package/dist/lib/startup-update.js +1 -1
  48. package/dist/locale/en-US.json +49 -43
  49. package/dist/locale/zh-CN.json +49 -43
  50. package/package.json +8 -2
  51. package/scripts/build.mjs +0 -34
  52. package/scripts/clean.mjs +0 -9
  53. package/tsconfig.json +0 -19
@@ -7,14 +7,12 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import fs from 'node:fs';
10
- import fsp from 'node:fs/promises';
10
+ import os from 'node:os';
11
11
  import path from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
- import { resolveCliHomeDir } from './cli-home.js';
14
13
  import { commandOutput, run } from './run-npm.js';
15
14
  const DEFAULT_PACKAGE_NAME = '@nocobase/cli';
16
15
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
17
- const INSTALL_METHOD_CACHE_FILE = 'self-install-methods.json';
18
16
  function normalizePath(value) {
19
17
  return path.resolve(value);
20
18
  }
@@ -97,6 +95,9 @@ function detectChannel(currentVersion) {
97
95
  if (/-beta(?:[.-]|$)/i.test(currentVersion)) {
98
96
  return 'beta';
99
97
  }
98
+ if (/-test(?:[.-]|$)/i.test(currentVersion)) {
99
+ return 'test';
100
+ }
100
101
  return 'latest';
101
102
  }
102
103
  function readCurrentVersion(packageRoot) {
@@ -105,56 +106,216 @@ function readCurrentVersion(packageRoot) {
105
106
  const pkg = JSON.parse(content);
106
107
  return String(pkg.version ?? '').trim();
107
108
  }
108
- function detectInstallMethod(packageRoot, globalPrefix) {
109
+ function detectInstallMethod(packageRoot, options = {}) {
109
110
  if (fs.existsSync(path.join(packageRoot, 'src'))
110
111
  && fs.existsSync(path.join(packageRoot, 'tsconfig.json'))) {
111
112
  return 'source';
112
113
  }
113
- if (globalPrefix && isSubPath(globalPrefix, packageRoot)) {
114
+ if (options.globalPrefix && isSubPath(options.globalPrefix, packageRoot)) {
114
115
  return 'npm-global';
115
116
  }
117
+ if (options.yarnGlobalDir && isSubPath(options.yarnGlobalDir, packageRoot)) {
118
+ return 'yarn-global';
119
+ }
120
+ if (isPnpmGlobalInstallPath({
121
+ packageName: options.packageName ?? DEFAULT_PACKAGE_NAME,
122
+ packageRoot,
123
+ pnpmGlobalBin: options.pnpmGlobalBin,
124
+ pnpmGlobalRoot: options.pnpmGlobalRoot,
125
+ })) {
126
+ return 'pnpm-global';
127
+ }
128
+ // Best-effort fallback for environments where pnpm probes are unavailable.
129
+ if (isPnpmGlobalPath(packageRoot)) {
130
+ return 'pnpm-global';
131
+ }
116
132
  if (packageRoot.includes(`${path.sep}node_modules${path.sep}`)) {
117
133
  return 'package-local';
118
134
  }
119
135
  return 'unknown';
120
136
  }
121
- function getInstallMethodCacheFile() {
122
- return path.join(resolveCliHomeDir('global'), INSTALL_METHOD_CACHE_FILE);
137
+ function isPnpmGlobalPath(packageRoot) {
138
+ const normalized = normalizePath(packageRoot);
139
+ const segments = normalized.split(path.sep).filter(Boolean);
140
+ const globalIndex = segments.findIndex((segment, index) => segment === 'global' && segments[index - 1] === 'pnpm');
141
+ if (globalIndex === -1) {
142
+ return false;
143
+ }
144
+ return segments.slice(globalIndex + 2).includes('node_modules');
123
145
  }
124
- function getInstallMethodCacheKey(packageRoot) {
125
- return normalizePath(path.join(packageRoot, 'bin', 'run.js'));
146
+ function normalizePnpmGlobalNodeModulesRoot(pnpmGlobalRoot) {
147
+ const normalizedGlobalRoot = normalizePath(pnpmGlobalRoot);
148
+ return path.basename(normalizedGlobalRoot) === 'node_modules'
149
+ ? normalizedGlobalRoot
150
+ : path.join(normalizedGlobalRoot, 'node_modules');
126
151
  }
127
- async function readInstallMethodCache() {
152
+ function readSiblingPnpmGlobalNodeModulesRoots(pnpmGlobalRoot) {
153
+ const globalNodeModulesDir = normalizePnpmGlobalNodeModulesRoot(pnpmGlobalRoot);
154
+ const globalProjectDir = path.dirname(globalNodeModulesDir);
155
+ let entries;
128
156
  try {
129
- const raw = await fsp.readFile(getInstallMethodCacheFile(), 'utf8');
130
- return JSON.parse(raw);
157
+ entries = fs.readdirSync(globalProjectDir);
131
158
  }
132
159
  catch {
133
- return {};
160
+ return [];
134
161
  }
162
+ return entries
163
+ .filter((entry) => {
164
+ if (entry === path.basename(globalNodeModulesDir)) {
165
+ return false;
166
+ }
167
+ try {
168
+ return fs.statSync(path.join(globalProjectDir, entry)).isDirectory();
169
+ }
170
+ catch {
171
+ return false;
172
+ }
173
+ })
174
+ .map((entry) => path.join(globalProjectDir, entry, 'node_modules'));
135
175
  }
136
- async function writeInstallMethodCache(state) {
137
- const filePath = getInstallMethodCacheFile();
138
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
139
- await fsp.writeFile(filePath, JSON.stringify(state, null, 2));
176
+ function readPnpmGlobalNodeModulesRoots(pnpmGlobalRoot) {
177
+ return [normalizePnpmGlobalNodeModulesRoot(pnpmGlobalRoot), ...readSiblingPnpmGlobalNodeModulesRoots(pnpmGlobalRoot)];
140
178
  }
141
- async function readCachedInstallMethod(packageRoot) {
142
- const state = await readInstallMethodCache();
143
- return state.entries?.[getInstallMethodCacheKey(packageRoot)];
179
+ function packageNameToPath(packageName) {
180
+ return packageName.split('/').filter(Boolean);
144
181
  }
145
- async function writeCachedInstallMethod(packageRoot, entry) {
146
- const state = await readInstallMethodCache();
147
- const entries = {
148
- ...(state.entries ?? {}),
149
- [getInstallMethodCacheKey(packageRoot)]: entry,
150
- };
151
- await writeInstallMethodCache({ entries });
182
+ function isSameRealPath(left, right) {
183
+ try {
184
+ return normalizePath(fs.realpathSync(left)) === normalizePath(fs.realpathSync(right));
185
+ }
186
+ catch {
187
+ return false;
188
+ }
189
+ }
190
+ function isPnpmGlobalRootPath(pnpmGlobalRoot, packageRoot) {
191
+ if (isSubPath(pnpmGlobalRoot, packageRoot)) {
192
+ return true;
193
+ }
194
+ const globalNodeModulesDir = normalizePnpmGlobalNodeModulesRoot(pnpmGlobalRoot);
195
+ const globalProjectDir = path.dirname(globalNodeModulesDir);
196
+ if (!isSubPath(globalProjectDir, packageRoot)) {
197
+ return false;
198
+ }
199
+ const segments = normalizePath(packageRoot).split(path.sep).filter(Boolean);
200
+ const pnpmStoreIndex = segments.indexOf('.pnpm');
201
+ if (pnpmStoreIndex === -1) {
202
+ return false;
203
+ }
204
+ return segments.slice(pnpmStoreIndex + 1).includes('node_modules');
205
+ }
206
+ function isPnpmGlobalPackagePath(pnpmGlobalRoot, packageRoot, packageName) {
207
+ return readPnpmGlobalNodeModulesRoots(pnpmGlobalRoot).some((pnpmGlobalNodeModulesRoot) => isSameRealPath(path.join(pnpmGlobalNodeModulesRoot, ...packageNameToPath(packageName)), packageRoot));
208
+ }
209
+ function isPnpmGlobalBinProjectPath(pnpmGlobalBin, packageRoot, packageName) {
210
+ const pnpmHome = path.dirname(normalizePath(pnpmGlobalBin));
211
+ const globalDir = path.join(pnpmHome, 'global');
212
+ if (isSubPath(globalDir, packageRoot)) {
213
+ return true;
214
+ }
215
+ let entries;
216
+ try {
217
+ entries = fs.readdirSync(globalDir);
218
+ }
219
+ catch {
220
+ return false;
221
+ }
222
+ return entries.some((entry) => {
223
+ const pnpmGlobalRoot = path.join(globalDir, entry);
224
+ return (isPnpmGlobalRootPath(pnpmGlobalRoot, packageRoot)
225
+ || isPnpmGlobalPackagePath(pnpmGlobalRoot, packageRoot, packageName));
226
+ });
227
+ }
228
+ function isPnpmGlobalInstallPath(options) {
229
+ if (options.pnpmGlobalRoot) {
230
+ const matchedRoot = isPnpmGlobalRootPath(options.pnpmGlobalRoot, options.packageRoot)
231
+ || isPnpmGlobalPackagePath(options.pnpmGlobalRoot, options.packageRoot, options.packageName);
232
+ if (matchedRoot) {
233
+ return true;
234
+ }
235
+ }
236
+ if (options.pnpmGlobalBin) {
237
+ return isPnpmGlobalBinProjectPath(options.pnpmGlobalBin, options.packageRoot, options.packageName);
238
+ }
239
+ return false;
240
+ }
241
+ function readCurrentBinPath(currentBinPath) {
242
+ const candidate = String(currentBinPath ?? process.argv[1] ?? '').trim();
243
+ if (!candidate) {
244
+ return undefined;
245
+ }
246
+ return normalizePath(candidate);
247
+ }
248
+ function cleanCommandPath(value) {
249
+ const trimmed = value.trim();
250
+ return trimmed ? trimmed : undefined;
251
+ }
252
+ function resolvePackageManagerProbeCwd() {
253
+ const candidates = [os.homedir(), os.tmpdir(), path.parse(process.cwd()).root];
254
+ for (const candidate of candidates) {
255
+ if (!candidate) {
256
+ continue;
257
+ }
258
+ try {
259
+ if (fs.statSync(candidate).isDirectory()) {
260
+ return candidate;
261
+ }
262
+ }
263
+ catch {
264
+ // Ignore invalid probe cwd candidates and continue to the next one.
265
+ }
266
+ }
267
+ return undefined;
152
268
  }
153
269
  async function readGlobalPrefix(commandOutputFn) {
154
270
  try {
155
- return (await commandOutputFn('npm', ['prefix', '-g'], {
271
+ return cleanCommandPath(await commandOutputFn('npm', ['prefix', '-g'], {
272
+ cwd: resolvePackageManagerProbeCwd(),
156
273
  errorName: 'npm prefix',
157
- })).trim();
274
+ }));
275
+ }
276
+ catch {
277
+ return undefined;
278
+ }
279
+ }
280
+ async function readPnpmGlobalBin(commandOutputFn) {
281
+ try {
282
+ return cleanCommandPath(await commandOutputFn('pnpm', ['bin', '-g'], {
283
+ cwd: resolvePackageManagerProbeCwd(),
284
+ errorName: 'pnpm bin',
285
+ }));
286
+ }
287
+ catch {
288
+ return undefined;
289
+ }
290
+ }
291
+ async function readPnpmGlobalRoot(commandOutputFn) {
292
+ try {
293
+ return cleanCommandPath(await commandOutputFn('pnpm', ['root', '-g'], {
294
+ cwd: resolvePackageManagerProbeCwd(),
295
+ errorName: 'pnpm root',
296
+ }));
297
+ }
298
+ catch {
299
+ return undefined;
300
+ }
301
+ }
302
+ async function readYarnGlobalDir(commandOutputFn) {
303
+ try {
304
+ return cleanCommandPath(await commandOutputFn('yarn', ['global', 'dir'], {
305
+ cwd: resolvePackageManagerProbeCwd(),
306
+ errorName: 'yarn global dir',
307
+ }));
308
+ }
309
+ catch {
310
+ return undefined;
311
+ }
312
+ }
313
+ async function readYarnGlobalBin(commandOutputFn) {
314
+ try {
315
+ return cleanCommandPath(await commandOutputFn('yarn', ['global', 'bin'], {
316
+ cwd: resolvePackageManagerProbeCwd(),
317
+ errorName: 'yarn global bin',
318
+ }));
158
319
  }
159
320
  catch {
160
321
  return undefined;
@@ -171,21 +332,21 @@ function getUnsupportedSelfUpdateReason(installMethod) {
171
332
  if (installMethod === 'source') {
172
333
  return [
173
334
  'This CLI is running from source in a repository checkout.',
174
- 'Automatic self-update is only supported for standard global npm installs.',
335
+ 'Automatic self-update is only supported for standard global npm, pnpm, or yarn installs.',
175
336
  'Upgrade this checkout through your repo workflow instead.',
176
337
  ].join(' ');
177
338
  }
178
339
  if (installMethod === 'package-local') {
179
340
  return [
180
341
  'This CLI is installed from a local project dependency tree.',
181
- 'Automatic self-update is only supported for standard global npm installs.',
342
+ 'Automatic self-update is only supported for standard global npm, pnpm, or yarn installs.',
182
343
  'Upgrade the parent project dependency that provides this CLI instead.',
183
344
  ].join(' ');
184
345
  }
185
346
  if (installMethod === 'unknown') {
186
347
  return [
187
- 'This CLI install could not be recognized as a standard global npm install.',
188
- 'Automatic self-update is only supported for standard global npm installs.',
348
+ 'This CLI install could not be recognized as a standard global npm, pnpm, or yarn install.',
349
+ 'Automatic self-update is only supported for standard global npm, pnpm, or yarn installs.',
189
350
  ].join(' ');
190
351
  }
191
352
  return undefined;
@@ -214,22 +375,45 @@ export function getSelfUpdatePackageSpec(status) {
214
375
  }
215
376
  export async function inspectSelfInstall(options = {}) {
216
377
  const packageRoot = options.packageRoot ? normalizePath(options.packageRoot) : PACKAGE_ROOT;
378
+ const packageName = options.packageName ?? DEFAULT_PACKAGE_NAME;
217
379
  const commandOutputFn = options.commandOutputFn ?? commandOutput;
218
- const cachedInstallMethod = await readCachedInstallMethod(packageRoot);
219
- const globalPrefix = cachedInstallMethod?.globalPrefix ?? await readGlobalPrefix(commandOutputFn);
220
- const installMethod = cachedInstallMethod?.installMethod ?? detectInstallMethod(packageRoot, globalPrefix);
221
- if (!cachedInstallMethod) {
222
- await writeCachedInstallMethod(packageRoot, {
223
- installMethod,
224
- globalPrefix,
225
- });
226
- }
380
+ const currentBinPath = readCurrentBinPath(options.currentBinPath);
381
+ const globalPrefix = await readGlobalPrefix(commandOutputFn);
382
+ const pnpmGlobalBin = await readPnpmGlobalBin(commandOutputFn);
383
+ const pnpmGlobalRoot = await readPnpmGlobalRoot(commandOutputFn);
384
+ const yarnGlobalDir = await readYarnGlobalDir(commandOutputFn);
385
+ const yarnGlobalBin = await readYarnGlobalBin(commandOutputFn);
386
+ const installMethod = detectInstallMethodFromSignals({
387
+ packageRoot,
388
+ currentBinPath,
389
+ globalPrefix,
390
+ packageName,
391
+ pnpmGlobalBin,
392
+ pnpmGlobalRoot,
393
+ yarnGlobalBin,
394
+ yarnGlobalDir,
395
+ });
227
396
  return {
228
397
  packageRoot,
229
398
  installMethod,
230
399
  globalPrefix,
231
400
  };
232
401
  }
402
+ function detectInstallMethodFromSignals(options) {
403
+ if (options.currentBinPath && options.yarnGlobalBin && isSubPath(options.yarnGlobalBin, options.currentBinPath)) {
404
+ return 'yarn-global';
405
+ }
406
+ if (options.currentBinPath && options.pnpmGlobalBin && isSubPath(options.pnpmGlobalBin, options.currentBinPath)) {
407
+ return 'pnpm-global';
408
+ }
409
+ return detectInstallMethod(options.packageRoot, {
410
+ globalPrefix: options.globalPrefix,
411
+ packageName: options.packageName,
412
+ pnpmGlobalBin: options.pnpmGlobalBin,
413
+ pnpmGlobalRoot: options.pnpmGlobalRoot,
414
+ yarnGlobalDir: options.yarnGlobalDir,
415
+ });
416
+ }
233
417
  export async function inspectSelfStatus(options = {}) {
234
418
  const packageRoot = options.packageRoot ? normalizePath(options.packageRoot) : PACKAGE_ROOT;
235
419
  const packageName = options.packageName ?? DEFAULT_PACKAGE_NAME;
@@ -237,6 +421,8 @@ export async function inspectSelfStatus(options = {}) {
237
421
  const channel = options.channel && options.channel !== 'auto' ? options.channel : detectChannel(currentVersion);
238
422
  const commandOutputFn = options.commandOutputFn ?? commandOutput;
239
423
  const { installMethod, globalPrefix } = await inspectSelfInstall({
424
+ currentBinPath: options.currentBinPath,
425
+ packageName,
240
426
  packageRoot,
241
427
  commandOutputFn,
242
428
  });
@@ -258,7 +444,7 @@ export async function inspectSelfStatus(options = {}) {
258
444
  latestVersion,
259
445
  updateAvailable,
260
446
  installMethod,
261
- updatable: installMethod === 'npm-global',
447
+ updatable: installMethod === 'npm-global' || installMethod === 'pnpm-global' || installMethod === 'yarn-global',
262
448
  updateBlockedReason: getUnsupportedSelfUpdateReason(installMethod),
263
449
  globalPrefix,
264
450
  registryError,
@@ -267,9 +453,30 @@ export async function inspectSelfStatus(options = {}) {
267
453
  export function formatUnsupportedSelfUpdateMessage(status) {
268
454
  return status.updateBlockedReason
269
455
  ?? [
270
- 'Automatic self-update is only supported for standard global npm installs.',
456
+ 'Automatic self-update is only supported for standard global npm, pnpm, or yarn installs.',
271
457
  ].join('\n');
272
458
  }
459
+ function resolveSelfUpdateInstallCommand(installMethod, packageSpec) {
460
+ if (installMethod === 'pnpm-global') {
461
+ return {
462
+ command: 'pnpm',
463
+ args: ['add', '-g', packageSpec],
464
+ errorName: 'pnpm add',
465
+ };
466
+ }
467
+ if (installMethod === 'yarn-global') {
468
+ return {
469
+ command: 'yarn',
470
+ args: ['global', 'add', packageSpec],
471
+ errorName: 'yarn global add',
472
+ };
473
+ }
474
+ return {
475
+ command: 'npm',
476
+ args: ['install', '-g', packageSpec],
477
+ errorName: 'npm install',
478
+ };
479
+ }
273
480
  export async function updateSelf(options = {}) {
274
481
  const status = await inspectSelfStatus(options);
275
482
  if (!status.updatable) {
@@ -288,9 +495,10 @@ export async function updateSelf(options = {}) {
288
495
  };
289
496
  }
290
497
  const packageSpec = getSelfUpdatePackageSpec(status);
291
- await (options.runFn ?? run)('npm', ['install', '-g', packageSpec], {
498
+ const installCommand = resolveSelfUpdateInstallCommand(status.installMethod, packageSpec);
499
+ await (options.runFn ?? run)(installCommand.command, installCommand.args, {
292
500
  stdio: options.verbose ? 'inherit' : 'ignore',
293
- errorName: 'npm install',
501
+ errorName: installCommand.errorName,
294
502
  });
295
503
  return {
296
504
  action: 'updated',
@@ -21,8 +21,8 @@ const NOCOBASE_SKILLS_NAME_PREFIX = 'nocobase-';
21
21
  // resolves and boots the package, even when the local skills installation is healthy.
22
22
  const SKILLS_LIST_TIMEOUT_MS = 15000;
23
23
  const SKILLS_NPM_VIEW_TIMEOUT_MS = 3000;
24
- const SKILLS_PACK_TIMEOUT_MS = 30000;
25
- const SKILLS_ADD_TIMEOUT_MS = 20000;
24
+ const SKILLS_PACK_TIMEOUT_MS = 120000;
25
+ const SKILLS_ADD_TIMEOUT_MS = 120000;
26
26
  const NPM_REGISTRY_UNAVAILABLE_PATTERNS = [
27
27
  'enotfound',
28
28
  'eai_again',
@@ -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
- if (state?.skillNames?.length) {
160
- return state.skillNames.filter((name) => installedNames.has(name)).sort();
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
@@ -251,6 +279,7 @@ async function prepareLocalSkillsPackage(globalRoot, options = {}, targetVersion
251
279
  const cachedVersion = await readCachedSkillsVersion(cacheRoot);
252
280
  await fsp.mkdir(cacheRoot, { recursive: true });
253
281
  if (targetVersion && cachedVersion && compareVersions(cachedVersion, targetVersion) === 0) {
282
+ options.onProgress?.(`Using cached ${NOCOBASE_SKILLS_PACKAGE_NAME}@${targetVersion}...`);
254
283
  return {
255
284
  packageDir,
256
285
  cleanup: async () => undefined,
@@ -259,12 +288,14 @@ async function prepareLocalSkillsPackage(globalRoot, options = {}, targetVersion
259
288
  await fsp.rm(packRoot, { recursive: true, force: true });
260
289
  await fsp.mkdir(packRoot, { recursive: true });
261
290
  try {
262
- await (options.runFn ?? run)('npm', ['pack', '--silent', packageSpec], {
291
+ options.onProgress?.(`Downloading ${packageSpec}...`);
292
+ await (options.runFn ?? run)('npm', ['pack', ...(options.verbose ? [] : ['--silent']), packageSpec], {
263
293
  cwd: packRoot,
264
294
  stdio: options.verbose ? 'inherit' : 'ignore',
265
295
  errorName: 'npm pack',
266
296
  timeoutMs: SKILLS_PACK_TIMEOUT_MS,
267
297
  });
298
+ options.onProgress?.(`Extracting ${NOCOBASE_SKILLS_PACKAGE_NAME}...`);
268
299
  const tarballPath = await resolvePackedSkillsTarball(packRoot);
269
300
  await extractPackedSkillsTarball(tarballPath, cacheRoot, targetVersion);
270
301
  }
@@ -279,14 +310,16 @@ async function prepareLocalSkillsPackage(globalRoot, options = {}, targetVersion
279
310
  export async function inspectSkillsStatus(options = {}) {
280
311
  const globalRoot = resolveSkillsRoot(options);
281
312
  const stateFile = getManagedSkillsStateFile(globalRoot);
282
- const [installedSkills, managedState] = await Promise.all([
313
+ const [installedSkills, managedState, cachedSkillNames] = await Promise.all([
283
314
  listGlobalSkills({
284
315
  globalRoot,
285
316
  commandOutputFn: options.commandOutputFn,
286
317
  }),
287
318
  readManagedSkillsState(globalRoot),
319
+ readCachedPackageSkillNames(globalRoot),
288
320
  ]);
289
- const installedSkillNames = pickInstalledNocoBaseSkillNames(installedSkills, managedState);
321
+ const installedSkillNames = pickInstalledNocoBaseSkillNames(installedSkills, managedState, cachedSkillNames);
322
+ const packageSkillNames = cachedSkillNames;
290
323
  const managedByNb = managedState?.packageName === NOCOBASE_SKILLS_PACKAGE_NAME;
291
324
  let latestVersion;
292
325
  let registryError;
@@ -312,6 +345,7 @@ export async function inspectSkillsStatus(options = {}) {
312
345
  managedByNb,
313
346
  sourcePackage: managedState?.sourcePackage ?? NOCOBASE_SKILLS_SOURCE,
314
347
  npmPackageName: managedState?.packageName ?? NOCOBASE_SKILLS_PACKAGE_NAME,
348
+ packageSkillNames,
315
349
  installedSkillNames,
316
350
  latestVersion,
317
351
  installedVersion,
@@ -321,13 +355,18 @@ export async function inspectSkillsStatus(options = {}) {
321
355
  registryError,
322
356
  };
323
357
  }
324
- async function persistManagedSkillsState(globalRoot, options = {}) {
325
- const installedSkills = await listGlobalSkills({
326
- globalRoot,
327
- commandOutputFn: options.commandOutputFn,
328
- });
329
- const managedState = await readManagedSkillsState(globalRoot);
330
- const installedSkillNames = pickInstalledNocoBaseSkillNames(installedSkills, managedState);
358
+ async function persistManagedSkillsState(globalRoot, options = {}, installedVersion) {
359
+ const [installedSkills, managedState, cachedSkillNames] = await Promise.all([
360
+ listGlobalSkills({
361
+ globalRoot,
362
+ commandOutputFn: options.commandOutputFn,
363
+ }),
364
+ readManagedSkillsState(globalRoot),
365
+ readCachedPackageSkillNames(globalRoot),
366
+ ]);
367
+ const installedSkillNames = pickInstalledNocoBaseSkillNames(installedSkills, managedState, cachedSkillNames);
368
+ const packageSkillNames = cachedSkillNames.length ? cachedSkillNames : installedSkillNames;
369
+ const cachedVersion = await readCachedSkillsVersion(getSkillsCacheRoot(globalRoot));
331
370
  const published = await readPublishedSkillsVersion({
332
371
  globalRoot,
333
372
  commandOutputFn: options.commandOutputFn,
@@ -338,8 +377,8 @@ async function persistManagedSkillsState(globalRoot, options = {}) {
338
377
  sourcePackage: NOCOBASE_SKILLS_SOURCE,
339
378
  installedAt: managedState?.installedAt ?? now,
340
379
  updatedAt: now,
341
- installedVersion: published.version,
342
- skillNames: installedSkillNames,
380
+ installedVersion: installedVersion ?? cachedVersion ?? published.version,
381
+ skillNames: packageSkillNames,
343
382
  });
344
383
  return await inspectSkillsStatus({
345
384
  globalRoot,
@@ -349,7 +388,8 @@ async function persistManagedSkillsState(globalRoot, options = {}) {
349
388
  async function reinstallManagedSkills(globalRoot, options = {}, targetVersion) {
350
389
  const prepared = await prepareLocalSkillsPackage(globalRoot, options, targetVersion);
351
390
  try {
352
- await (options.runFn ?? run)('npx', ['-y', 'skills', 'add', prepared.packageDir, '-g', '-y'], {
391
+ options.onProgress?.('Installing NocoBase AI coding skills globally...');
392
+ await (options.runFn ?? run)('npx', ['-y', 'skills', 'add', prepared.packageDir, '-g', '-y', '--skill', '*'], {
353
393
  cwd: globalRoot,
354
394
  stdio: options.verbose ? 'inherit' : 'ignore',
355
395
  errorName: 'skills add',
@@ -360,31 +400,66 @@ async function reinstallManagedSkills(globalRoot, options = {}, targetVersion) {
360
400
  await prepared.cleanup();
361
401
  }
362
402
  }
403
+ function pickObsoleteManagedSkillNames(installedSkillNames, packageSkillNames) {
404
+ if (!packageSkillNames.length) {
405
+ return [];
406
+ }
407
+ const packageSkillNameSet = new Set(packageSkillNames);
408
+ return installedSkillNames.filter((skillName) => !packageSkillNameSet.has(skillName)).sort();
409
+ }
410
+ async function removeObsoleteManagedSkills(globalRoot, installedSkillNames, options = {}) {
411
+ const packageSkillNames = await readCachedPackageSkillNames(globalRoot);
412
+ const obsoleteSkillNames = pickObsoleteManagedSkillNames(installedSkillNames, packageSkillNames);
413
+ for (const skillName of obsoleteSkillNames) {
414
+ options.onProgress?.(`Removing obsolete skill ${skillName}...`);
415
+ await (options.runFn ?? run)('npx', ['-y', 'skills', 'remove', skillName, '-g', '-y'], {
416
+ cwd: globalRoot,
417
+ stdio: options.verbose ? 'inherit' : 'ignore',
418
+ errorName: 'skills remove',
419
+ });
420
+ }
421
+ }
363
422
  export async function installNocoBaseSkills(options = {}) {
364
423
  const globalRoot = resolveSkillsRoot(options);
424
+ options.onProgress?.('Checking installed NocoBase AI coding skills...');
365
425
  const status = await inspectSkillsStatus({
366
426
  globalRoot,
367
427
  commandOutputFn: options.commandOutputFn,
368
428
  });
369
- if (status.installed) {
429
+ const cachedSkillNames = await readCachedPackageSkillNames(globalRoot);
430
+ const missingCachedSkillNames = cachedSkillNames.filter((name) => !status.installedSkillNames.includes(name));
431
+ const obsoleteSkillNames = pickObsoleteManagedSkillNames(status.installedSkillNames, cachedSkillNames);
432
+ const targetVersion = String(options.targetVersion ?? '').trim() || undefined;
433
+ const targetVersionMatches = !targetVersion || status.installedVersion === targetVersion;
434
+ if (status.installed && targetVersionMatches && missingCachedSkillNames.length === 0 && obsoleteSkillNames.length === 0) {
370
435
  return {
371
436
  action: 'noop',
372
437
  status,
373
438
  };
374
439
  }
375
440
  await ensureSkillsWorkspaceRoot(globalRoot);
376
- await reinstallManagedSkills(globalRoot, options, status.latestVersion);
441
+ if (!status.installed || !targetVersionMatches || missingCachedSkillNames.length > 0) {
442
+ const installVersion = targetVersion ?? status.latestVersion;
443
+ await reinstallManagedSkills(globalRoot, options, installVersion);
444
+ }
445
+ await removeObsoleteManagedSkills(globalRoot, status.installedSkillNames, options);
446
+ options.onProgress?.('Verifying installed NocoBase AI coding skills...');
377
447
  return {
378
448
  action: 'installed',
379
- status: await persistManagedSkillsState(globalRoot, options),
449
+ status: await persistManagedSkillsState(globalRoot, options, targetVersion),
380
450
  };
381
451
  }
382
452
  export async function updateNocoBaseSkills(options = {}) {
383
453
  const globalRoot = resolveSkillsRoot(options);
454
+ options.onProgress?.('Checking installed NocoBase AI coding skills...');
384
455
  const status = await inspectSkillsStatus({
385
456
  globalRoot,
386
457
  commandOutputFn: options.commandOutputFn,
387
458
  });
459
+ const cachedSkillNames = await readCachedPackageSkillNames(globalRoot);
460
+ const missingCachedSkillNames = cachedSkillNames.filter((name) => !status.installedSkillNames.includes(name));
461
+ const obsoleteSkillNames = pickObsoleteManagedSkillNames(status.installedSkillNames, cachedSkillNames);
462
+ const targetVersion = String(options.targetVersion ?? '').trim() || undefined;
388
463
  if (!status.installed) {
389
464
  return {
390
465
  action: 'noop',
@@ -393,8 +468,11 @@ export async function updateNocoBaseSkills(options = {}) {
393
468
  };
394
469
  }
395
470
  if (status.managedByNb &&
471
+ !targetVersion &&
396
472
  status.latestVersion &&
397
473
  status.installedVersion &&
474
+ missingCachedSkillNames.length === 0 &&
475
+ obsoleteSkillNames.length === 0 &&
398
476
  compareVersions(status.latestVersion, status.installedVersion) <= 0) {
399
477
  return {
400
478
  action: 'noop',
@@ -402,10 +480,25 @@ export async function updateNocoBaseSkills(options = {}) {
402
480
  status,
403
481
  };
404
482
  }
405
- await reinstallManagedSkills(globalRoot, options, status.latestVersion);
483
+ if (targetVersion &&
484
+ status.installedVersion === targetVersion &&
485
+ missingCachedSkillNames.length === 0 &&
486
+ obsoleteSkillNames.length === 0) {
487
+ return {
488
+ action: 'noop',
489
+ reason: 'up-to-date',
490
+ status,
491
+ };
492
+ }
493
+ if (!targetVersion || status.installedVersion !== targetVersion || missingCachedSkillNames.length > 0) {
494
+ const installVersion = targetVersion ?? status.latestVersion;
495
+ await reinstallManagedSkills(globalRoot, options, installVersion);
496
+ }
497
+ await removeObsoleteManagedSkills(globalRoot, status.installedSkillNames, options);
498
+ options.onProgress?.('Verifying installed NocoBase AI coding skills...');
406
499
  return {
407
500
  action: 'updated',
408
- status: await persistManagedSkillsState(globalRoot, options),
501
+ status: await persistManagedSkillsState(globalRoot, options, targetVersion),
409
502
  };
410
503
  }
411
504
  export async function removeNocoBaseSkills(options = {}) {