@nocobase/cli 2.1.2 → 2.1.4

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.
@@ -12,10 +12,7 @@ import { printInfo, renderTable } from '../../lib/ui.js';
12
12
  export default class SkillsCheck extends Command {
13
13
  static summary = 'Check the globally installed NocoBase AI coding skills';
14
14
  static description = 'Inspect the global NocoBase AI coding skills and report whether they are managed by the CLI and whether an update is available.';
15
- static examples = [
16
- '<%= config.bin %> <%= command.id %>',
17
- '<%= config.bin %> <%= command.id %> --json',
18
- ];
15
+ static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --json'];
19
16
  static flags = {
20
17
  json: Flags.boolean({
21
18
  description: 'Output the result as JSON',
@@ -25,6 +22,7 @@ export default class SkillsCheck extends Command {
25
22
  async run() {
26
23
  const { flags } = await this.parse(SkillsCheck);
27
24
  const status = await inspectSkillsStatus();
25
+ const displaySkillNames = status.packageSkillNames.length ? status.packageSkillNames : status.installedSkillNames;
28
26
  if (flags.json) {
29
27
  this.log(JSON.stringify({
30
28
  ok: true,
@@ -35,6 +33,7 @@ export default class SkillsCheck extends Command {
35
33
  managedByNb: status.managedByNb,
36
34
  sourcePackage: status.sourcePackage,
37
35
  npmPackageName: status.npmPackageName,
36
+ packageSkillNames: status.packageSkillNames,
38
37
  installedSkillNames: status.installedSkillNames,
39
38
  installedVersion: status.installedVersion,
40
39
  latestVersion: status.latestVersion,
@@ -50,7 +49,7 @@ export default class SkillsCheck extends Command {
50
49
  ['Skills home', status.globalRoot],
51
50
  ['Installed', status.installed ? 'yes' : 'no'],
52
51
  ['Managed by nb', status.managedByNb ? 'yes' : 'no'],
53
- ['Installed skills', status.installedSkillNames.length ? status.installedSkillNames.join(', ') : '(none)'],
52
+ ['Installed skills', displaySkillNames.length ? displaySkillNames.join(', ') : '(none)'],
54
53
  ['Installed version', status.installedVersion ?? '(unknown)'],
55
54
  ['Latest version', status.latestVersion ?? '(unknown)'],
56
55
  ['Update available', status.updateAvailable === null ? 'unknown' : status.updateAvailable ? 'yes' : 'no'],
@@ -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+)-(alpha|beta|rc)(?:\.(\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, channel, sequence] = match;
122
- return sequence ? `${base}-${channel}.${sequence}` : `${base}-${channel}`;
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
- if (channel === 'rc') {
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: matchesVersionCondition(comparableVersion, comparableCondition) ? 'match' : 'mismatch',
254
+ result: matchesAppByChannelVersionCondition(comparableVersion, comparableCondition) ? 'match' : 'mismatch',
212
255
  channel,
213
256
  condition: channelCondition,
214
257
  };
@@ -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
@@ -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 listGlobalSkills({
326
- globalRoot,
327
- commandOutputFn: options.commandOutputFn,
328
- });
329
- const managedState = await readManagedSkillsState(globalRoot);
330
- const installedSkillNames = pickInstalledNocoBaseSkillNames(installedSkills, managedState);
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: installedSkillNames,
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
- if (status.installed) {
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
- await reinstallManagedSkills(globalRoot, options, status.latestVersion);
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
- await reinstallManagedSkills(globalRoot, options, status.latestVersion);
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/cli",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "description": "NocoBase Command Line Tool",
5
5
  "type": "module",
6
6
  "main": "dist/generated/command-registry.js",
@@ -143,5 +143,5 @@
143
143
  "type": "git",
144
144
  "url": "git+https://github.com/nocobase/nocobase.git"
145
145
  },
146
- "gitHead": "e1b28561425c5c34ff3bc6ae1f81c66b72f02872"
146
+ "gitHead": "73c01b1e842afdaafdffd6fa4bb090c1da9b0816"
147
147
  }