@nocobase/cli 2.1.0-alpha.24 → 2.1.0-alpha.26

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 (74) hide show
  1. package/README.md +41 -49
  2. package/README.zh-CN.md +38 -45
  3. package/bin/run.js +15 -0
  4. package/dist/commands/app/down.js +260 -0
  5. package/dist/commands/app/logs.js +98 -0
  6. package/dist/commands/app/restart.js +75 -0
  7. package/dist/commands/app/start.js +252 -0
  8. package/dist/commands/app/stop.js +98 -0
  9. package/dist/commands/app/upgrade.js +595 -0
  10. package/dist/commands/build.js +3 -48
  11. package/dist/commands/db/shared.js +19 -5
  12. package/dist/commands/dev.js +3 -140
  13. package/dist/commands/down.js +3 -184
  14. package/dist/commands/download.js +4 -856
  15. package/dist/commands/env/add.js +33 -48
  16. package/dist/commands/env/auth.js +6 -13
  17. package/dist/commands/env/info.js +152 -0
  18. package/dist/commands/env/list.js +27 -18
  19. package/dist/commands/env/remove.js +4 -10
  20. package/dist/commands/env/shared.js +158 -0
  21. package/dist/commands/env/update.js +7 -13
  22. package/dist/commands/env/use.js +5 -13
  23. package/dist/commands/{prompts-stages.js → examples/prompts-stages.js} +3 -3
  24. package/dist/commands/{prompts-test.js → examples/prompts-test.js} +3 -3
  25. package/dist/commands/init.js +270 -64
  26. package/dist/commands/install.js +352 -86
  27. package/dist/commands/logs.js +3 -81
  28. package/dist/commands/plugin/disable.js +64 -0
  29. package/dist/commands/plugin/enable.js +64 -0
  30. package/dist/commands/plugin/list.js +62 -0
  31. package/dist/commands/pm/disable.js +3 -54
  32. package/dist/commands/pm/enable.js +3 -54
  33. package/dist/commands/pm/list.js +3 -45
  34. package/dist/commands/restart.js +12 -0
  35. package/dist/commands/scaffold/migration.js +1 -1
  36. package/dist/commands/scaffold/plugin.js +1 -1
  37. package/dist/commands/self/check.js +1 -1
  38. package/dist/commands/self/update.js +13 -3
  39. package/dist/commands/skills/check.js +11 -5
  40. package/dist/commands/skills/index.js +1 -1
  41. package/dist/commands/skills/install.js +20 -7
  42. package/dist/commands/skills/remove.js +71 -0
  43. package/dist/commands/skills/update.js +27 -7
  44. package/dist/commands/source/build.js +58 -0
  45. package/dist/commands/source/dev.js +157 -0
  46. package/dist/commands/source/download.js +866 -0
  47. package/dist/commands/source/test.js +467 -0
  48. package/dist/commands/start.js +3 -202
  49. package/dist/commands/stop.js +3 -81
  50. package/dist/commands/test.js +3 -457
  51. package/dist/commands/upgrade.js +3 -574
  52. package/dist/help/runtime-help.js +3 -0
  53. package/dist/lib/api-client.js +22 -7
  54. package/dist/lib/app-health.js +126 -0
  55. package/dist/lib/app-managed-resources.js +264 -0
  56. package/dist/lib/app-runtime.js +16 -5
  57. package/dist/lib/auth-store.js +162 -43
  58. package/dist/lib/bootstrap.js +13 -12
  59. package/dist/lib/cli-home.js +38 -6
  60. package/dist/lib/cli-locale.js +15 -1
  61. package/dist/lib/env-auth.js +3 -3
  62. package/dist/lib/env-config.js +80 -0
  63. package/dist/lib/generated-command.js +10 -2
  64. package/dist/lib/http-request.js +49 -0
  65. package/dist/lib/prompt-web-ui.js +13 -6
  66. package/dist/lib/resource-command.js +10 -2
  67. package/dist/lib/runtime-generator.js +1 -1
  68. package/dist/lib/self-manager.js +1 -1
  69. package/dist/lib/skills-manager.js +173 -79
  70. package/dist/lib/startup-update.js +203 -0
  71. package/dist/locale/en-US.json +4 -1
  72. package/dist/locale/zh-CN.json +4 -1
  73. package/package.json +27 -4
  74. package/dist/commands/ps.js +0 -116
@@ -0,0 +1,49 @@
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
+ function normalizeLocationUrl(location, currentUrl) {
10
+ try {
11
+ return new URL(location, currentUrl).toString();
12
+ }
13
+ catch (_error) {
14
+ return undefined;
15
+ }
16
+ }
17
+ function shouldPreserveAuthorizationRedirect(fromUrl, toUrl) {
18
+ try {
19
+ const from = new URL(fromUrl);
20
+ const to = new URL(toUrl);
21
+ return (from.hostname === to.hostname &&
22
+ from.port === to.port &&
23
+ from.pathname === to.pathname &&
24
+ from.search === to.search &&
25
+ from.protocol === 'http:' &&
26
+ to.protocol === 'https:');
27
+ }
28
+ catch (_error) {
29
+ return false;
30
+ }
31
+ }
32
+ export async function fetchWithPreservedAuthRedirect(url, init = {}) {
33
+ const response = await fetch(url, {
34
+ ...init,
35
+ redirect: 'manual',
36
+ });
37
+ const location = response.headers.get('location');
38
+ if (!location || ![301, 302, 307, 308].includes(response.status)) {
39
+ return response;
40
+ }
41
+ const nextUrl = normalizeLocationUrl(location, url);
42
+ if (!nextUrl || !shouldPreserveAuthorizationRedirect(url, nextUrl)) {
43
+ return response;
44
+ }
45
+ return fetch(nextUrl, {
46
+ ...init,
47
+ redirect: 'manual',
48
+ });
49
+ }
@@ -559,17 +559,23 @@ function readFormFromClientStrippingPwcMeta(o) {
559
559
  const { [PWC_FORM_META_STEP]: _meta, ...rest } = o;
560
560
  return rest;
561
561
  }
562
- function openUrlInDefaultBrowser(url) {
562
+ function openUrlInDefaultBrowser(url, onError) {
563
+ const reportError = (error) => {
564
+ onError?.(url, error);
565
+ };
563
566
  const platform = process.platform;
567
+ let child;
564
568
  if (platform === 'darwin') {
565
- spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
569
+ child = spawn('open', [url], { stdio: 'ignore', detached: true });
566
570
  }
567
571
  else if (platform === 'win32') {
568
- spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true, windowsHide: true }).unref();
572
+ child = spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true, windowsHide: true });
569
573
  }
570
574
  else {
571
- spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();
575
+ child = spawn('xdg-open', [url], { stdio: 'ignore', detached: true });
572
576
  }
577
+ child.once('error', reportError);
578
+ child.unref();
573
579
  }
574
580
  function closePromptWebUiServer(server, done) {
575
581
  server.close(done);
@@ -2084,11 +2090,12 @@ function runPromptCatalogWebUIImpl(options) {
2084
2090
  const port = addr.port;
2085
2091
  const startUrl = `http://${host}:${port}/`;
2086
2092
  options.onServerStart?.({ host, port, url: startUrl });
2093
+ const onOpenBrowserError = options.onOpenBrowserError ?? ((u, err) => console.warn(String(err), u));
2087
2094
  try {
2088
- openUrlInDefaultBrowser(startUrl);
2095
+ openUrlInDefaultBrowser(startUrl, onOpenBrowserError);
2089
2096
  }
2090
2097
  catch (e) {
2091
- (options.onOpenBrowserError ?? ((u, err) => console.warn(String(err), u)))(startUrl, e);
2098
+ onOpenBrowserError(startUrl, e);
2092
2099
  }
2093
2100
  timeoutId = setTimeout(() => rejectAndClose(new Error('Local UI timeout — close the tab and try again, or resubmit within the time limit.')), timeoutMs);
2094
2101
  });
@@ -1,3 +1,11 @@
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
+ */
1
9
  import { Flags } from '@oclif/core';
2
10
  import { executeResourceRequest } from './resource-request.js';
3
11
  import { setVerboseMode } from './ui.js';
@@ -80,7 +88,7 @@ function printResponse(command, response, jsonOutput) {
80
88
  command.log(`HTTP ${response.status}`);
81
89
  }
82
90
  export const resourceBaseFlags = {
83
- 'base-url': Flags.string({
91
+ 'api-base-url': Flags.string({
84
92
  description: 'NocoBase API base URL, for example http://localhost:13000/api',
85
93
  }),
86
94
  verbose: Flags.boolean({
@@ -325,7 +333,7 @@ export async function runResourceCommand(command, action, flags, args) {
325
333
  setVerboseMode(Boolean(flags.verbose));
326
334
  const response = await executeResourceRequest({
327
335
  envName: flags.env,
328
- baseUrl: flags['base-url'],
336
+ baseUrl: flags['api-base-url'],
329
337
  role: flags.role,
330
338
  token: flags.token,
331
339
  action,
@@ -10,7 +10,7 @@ import { createHash } from 'node:crypto';
10
10
  import { loadBuildConfig } from './build-config.js';
11
11
  import { toKebabCase, toLogicalActionName, toLogicalResourceName, toResourceSegments } from './naming.js';
12
12
  import { collectOperations } from './openapi.js';
13
- const RESERVED_FLAG_NAMES = new Set(['base-url', 'env', 'token', 'json-output', 'body', 'body-file']);
13
+ const RESERVED_FLAG_NAMES = new Set(['api-base-url', 'base-url', 'env', 'token', 'json-output', 'body', 'body-file']);
14
14
  function matchesPattern(value, pattern) {
15
15
  if (!value) {
16
16
  return false;
@@ -234,7 +234,7 @@ export async function updateSelf(options = {}) {
234
234
  }
235
235
  const packageSpec = getSelfUpdatePackageSpec(status);
236
236
  await (options.runFn ?? run)('npm', ['install', '-g', packageSpec], {
237
- stdio: 'inherit',
237
+ stdio: options.verbose ? 'inherit' : 'ignore',
238
238
  errorName: 'npm install',
239
239
  });
240
240
  return {
@@ -6,31 +6,38 @@
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
- import fs from 'node:fs';
10
9
  import fsp from 'node:fs/promises';
11
10
  import path from 'node:path';
11
+ import { resolveCliHomeDir } from './cli-home.js';
12
+ import { compareVersions } from './self-manager.js';
12
13
  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';
14
+ export const NOCOBASE_SKILLS_SOURCE = 'nocobase/skills';
15
+ export const NOCOBASE_SKILLS_PACKAGE_NAME = '@nocobase/skills';
15
16
  const NOCOBASE_SKILLS_NAME_PREFIX = 'nocobase-';
16
17
  function normalizePath(value) {
17
18
  return path.resolve(value);
18
19
  }
20
+ export function resolveGlobalSkillsRoot(_startCwd = process.cwd()) {
21
+ return normalizePath(resolveCliHomeDir('global'));
22
+ }
19
23
  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
- }
24
+ return resolveGlobalSkillsRoot(startCwd);
25
+ }
26
+ function resolveSkillsRoot(options = {}) {
27
+ return options.globalRoot
28
+ ? normalizePath(options.globalRoot)
29
+ : options.workspaceRoot
30
+ ? normalizePath(options.workspaceRoot)
31
+ : resolveGlobalSkillsRoot();
32
+ }
33
+ function getSkillsCacheRoot(globalRoot) {
34
+ return path.join(globalRoot, 'cache', 'skills');
31
35
  }
32
36
  export function getManagedSkillsStateFile(workspaceRoot) {
33
- return path.join(workspaceRoot, '.nocobase', 'skills.json');
37
+ return path.join(workspaceRoot, 'skills.json');
38
+ }
39
+ async function ensureSkillsWorkspaceRoot(workspaceRoot) {
40
+ await fsp.mkdir(workspaceRoot, { recursive: true });
34
41
  }
35
42
  async function readManagedSkillsState(workspaceRoot) {
36
43
  const filePath = getManagedSkillsStateFile(workspaceRoot);
@@ -47,15 +54,19 @@ async function writeManagedSkillsState(workspaceRoot, state) {
47
54
  await fsp.mkdir(path.dirname(filePath), { recursive: true });
48
55
  await fsp.writeFile(filePath, JSON.stringify(state, null, 2));
49
56
  }
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,
57
+ export async function listGlobalSkills(options = {}) {
58
+ const globalRoot = resolveSkillsRoot(options);
59
+ await ensureSkillsWorkspaceRoot(globalRoot);
60
+ const output = await (options.commandOutputFn ?? commandOutput)('npx', ['-y', 'skills', 'list', '-g', '--json'], {
61
+ cwd: globalRoot,
54
62
  errorName: 'skills list',
55
63
  });
56
64
  const parsed = JSON.parse(output);
57
65
  return Array.isArray(parsed) ? parsed : [];
58
66
  }
67
+ export async function listProjectSkills(options = {}) {
68
+ return await listGlobalSkills(options);
69
+ }
59
70
  function pickInstalledNocoBaseSkillNames(installedSkills, state) {
60
71
  const installedNames = new Set(installedSkills.map((skill) => String(skill.name ?? '').trim()).filter(Boolean));
61
72
  if (state?.skillNames?.length) {
@@ -65,14 +76,17 @@ function pickInstalledNocoBaseSkillNames(installedSkills, state) {
65
76
  .filter((name) => name.startsWith(NOCOBASE_SKILLS_NAME_PREFIX))
66
77
  .sort();
67
78
  }
68
- export async function readNocoBaseSkillsHeadRef(options = {}) {
79
+ async function readPublishedSkillsVersion(options = {}) {
80
+ const globalRoot = resolveSkillsRoot(options);
81
+ await ensureSkillsWorkspaceRoot(globalRoot);
69
82
  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',
83
+ const output = await (options.commandOutputFn ?? commandOutput)('npm', ['view', NOCOBASE_SKILLS_PACKAGE_NAME, 'version', '--json'], {
84
+ cwd: globalRoot,
85
+ errorName: 'npm view',
73
86
  });
74
- const ref = output.trim().split(/\s+/)[0];
75
- return { ref: ref || undefined };
87
+ const parsed = JSON.parse(output);
88
+ const version = String(parsed ?? '').trim();
89
+ return { version: version || undefined };
76
90
  }
77
91
  catch (error) {
78
92
  return {
@@ -80,80 +94,134 @@ export async function readNocoBaseSkillsHeadRef(options = {}) {
80
94
  };
81
95
  }
82
96
  }
97
+ async function readCachedSkillsVersion(cacheRoot) {
98
+ const packageJsonPath = path.join(cacheRoot, 'node_modules', '@nocobase', 'skills', 'package.json');
99
+ try {
100
+ const content = await fsp.readFile(packageJsonPath, 'utf8');
101
+ const parsed = JSON.parse(content);
102
+ const version = String(parsed.version ?? '').trim();
103
+ return version || undefined;
104
+ }
105
+ catch {
106
+ return undefined;
107
+ }
108
+ }
109
+ async function prepareLocalSkillsPackage(globalRoot, options = {}, targetVersion) {
110
+ const cacheRoot = getSkillsCacheRoot(globalRoot);
111
+ const packageDir = path.join(cacheRoot, 'node_modules', '@nocobase', 'skills');
112
+ const packageSpec = targetVersion ? `${NOCOBASE_SKILLS_PACKAGE_NAME}@${targetVersion}` : NOCOBASE_SKILLS_PACKAGE_NAME;
113
+ const cachedVersion = await readCachedSkillsVersion(cacheRoot);
114
+ await fsp.mkdir(cacheRoot, { recursive: true });
115
+ if (targetVersion && cachedVersion && compareVersions(cachedVersion, targetVersion) === 0) {
116
+ return {
117
+ packageDir,
118
+ cleanup: async () => undefined,
119
+ };
120
+ }
121
+ await fsp.rm(path.join(cacheRoot, 'node_modules'), { recursive: true, force: true });
122
+ await (options.runFn ?? run)('npm', ['install', '--no-save', '--ignore-scripts', '--no-package-lock', packageSpec], {
123
+ cwd: cacheRoot,
124
+ stdio: options.verbose ? 'inherit' : 'ignore',
125
+ errorName: 'npm install',
126
+ });
127
+ try {
128
+ await fsp.access(packageDir);
129
+ }
130
+ catch {
131
+ throw new Error(`npm install did not produce a local ${NOCOBASE_SKILLS_PACKAGE_NAME} package.`);
132
+ }
133
+ return {
134
+ packageDir,
135
+ cleanup: async () => undefined,
136
+ };
137
+ }
83
138
  export async function inspectSkillsStatus(options = {}) {
84
- const workspaceRoot = options.workspaceRoot ? normalizePath(options.workspaceRoot) : resolveSkillsWorkspaceRoot();
85
- const stateFile = getManagedSkillsStateFile(workspaceRoot);
139
+ const globalRoot = resolveSkillsRoot(options);
140
+ const stateFile = getManagedSkillsStateFile(globalRoot);
86
141
  const [installedSkills, managedState] = await Promise.all([
87
- listProjectSkills({
88
- workspaceRoot,
142
+ listGlobalSkills({
143
+ globalRoot,
89
144
  commandOutputFn: options.commandOutputFn,
90
145
  }),
91
- readManagedSkillsState(workspaceRoot),
146
+ readManagedSkillsState(globalRoot),
92
147
  ]);
93
148
  const installedSkillNames = pickInstalledNocoBaseSkillNames(installedSkills, managedState);
94
- const managedByNb = managedState?.packageName === NOCOBASE_SKILLS_PACKAGE;
95
- let latestRef;
149
+ const managedByNb = managedState?.packageName === NOCOBASE_SKILLS_PACKAGE_NAME;
150
+ let latestVersion;
96
151
  let registryError;
97
152
  let updateAvailable = installedSkillNames.length > 0 ? null : false;
98
153
  if (installedSkillNames.length > 0 || managedByNb) {
99
- const remote = await readNocoBaseSkillsHeadRef({
100
- workspaceRoot,
154
+ const published = await readPublishedSkillsVersion({
155
+ globalRoot,
101
156
  commandOutputFn: options.commandOutputFn,
102
157
  });
103
- latestRef = remote.ref;
104
- registryError = remote.error;
105
- if (managedState?.installedRef && latestRef) {
106
- updateAvailable = latestRef !== managedState.installedRef;
158
+ latestVersion = published.version;
159
+ registryError = published.error;
160
+ const installedVersion = managedState?.installedVersion ?? managedState?.installedRef;
161
+ if (installedVersion && latestVersion) {
162
+ updateAvailable = compareVersions(latestVersion, installedVersion) > 0;
107
163
  }
108
164
  }
165
+ const installedVersion = managedState?.installedVersion ?? managedState?.installedRef;
109
166
  return {
110
- workspaceRoot,
167
+ globalRoot,
168
+ workspaceRoot: globalRoot,
111
169
  stateFile,
112
170
  installed: installedSkillNames.length > 0,
113
171
  managedByNb,
114
- sourcePackage: NOCOBASE_SKILLS_PACKAGE,
172
+ sourcePackage: managedState?.sourcePackage ?? NOCOBASE_SKILLS_SOURCE,
173
+ npmPackageName: managedState?.packageName ?? NOCOBASE_SKILLS_PACKAGE_NAME,
115
174
  installedSkillNames,
116
- latestRef,
117
- installedRef: managedState?.installedRef,
175
+ latestVersion,
176
+ installedVersion,
177
+ latestRef: latestVersion,
178
+ installedRef: installedVersion,
118
179
  updateAvailable,
119
180
  registryError,
120
181
  };
121
182
  }
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,
183
+ async function persistManagedSkillsState(globalRoot, options = {}) {
184
+ const installedSkills = await listGlobalSkills({
185
+ globalRoot,
131
186
  commandOutputFn: options.commandOutputFn,
132
187
  });
133
- const managedState = await readManagedSkillsState(workspaceRoot);
188
+ const managedState = await readManagedSkillsState(globalRoot);
134
189
  const installedSkillNames = pickInstalledNocoBaseSkillNames(installedSkills, managedState);
135
- const remote = await readNocoBaseSkillsHeadRef({
136
- workspaceRoot,
190
+ const published = await readPublishedSkillsVersion({
191
+ globalRoot,
137
192
  commandOutputFn: options.commandOutputFn,
138
193
  });
139
194
  const now = new Date().toISOString();
140
- await writeManagedSkillsState(workspaceRoot, {
141
- packageName: NOCOBASE_SKILLS_PACKAGE,
142
- repoUrl: NOCOBASE_SKILLS_REPO_URL,
195
+ await writeManagedSkillsState(globalRoot, {
196
+ packageName: NOCOBASE_SKILLS_PACKAGE_NAME,
197
+ sourcePackage: NOCOBASE_SKILLS_SOURCE,
143
198
  installedAt: managedState?.installedAt ?? now,
144
199
  updatedAt: now,
145
- installedRef: remote.ref,
200
+ installedVersion: published.version,
146
201
  skillNames: installedSkillNames,
147
202
  });
148
203
  return await inspectSkillsStatus({
149
- workspaceRoot,
204
+ globalRoot,
150
205
  commandOutputFn: options.commandOutputFn,
151
206
  });
152
207
  }
208
+ async function reinstallManagedSkills(globalRoot, options = {}, targetVersion) {
209
+ const prepared = await prepareLocalSkillsPackage(globalRoot, options, targetVersion);
210
+ try {
211
+ await (options.runFn ?? run)('npx', ['-y', 'skills', 'add', prepared.packageDir, '-g', '-y'], {
212
+ cwd: globalRoot,
213
+ stdio: options.verbose ? 'inherit' : 'ignore',
214
+ errorName: 'skills add',
215
+ });
216
+ }
217
+ finally {
218
+ await prepared.cleanup();
219
+ }
220
+ }
153
221
  export async function installNocoBaseSkills(options = {}) {
154
- const workspaceRoot = options.workspaceRoot ? normalizePath(options.workspaceRoot) : resolveSkillsWorkspaceRoot();
222
+ const globalRoot = resolveSkillsRoot(options);
155
223
  const status = await inspectSkillsStatus({
156
- workspaceRoot,
224
+ globalRoot,
157
225
  commandOutputFn: options.commandOutputFn,
158
226
  });
159
227
  if (status.installed) {
@@ -162,41 +230,67 @@ export async function installNocoBaseSkills(options = {}) {
162
230
  status,
163
231
  };
164
232
  }
165
- await (options.runFn ?? run)('npx', ['-y', 'skills', 'add', NOCOBASE_SKILLS_PACKAGE, '-y'], {
166
- cwd: workspaceRoot,
167
- stdio: 'inherit',
168
- errorName: 'skills add',
169
- });
233
+ await ensureSkillsWorkspaceRoot(globalRoot);
234
+ await reinstallManagedSkills(globalRoot, options, status.latestVersion);
170
235
  return {
171
236
  action: 'installed',
172
- status: await persistManagedSkillsState(workspaceRoot, options),
237
+ status: await persistManagedSkillsState(globalRoot, options),
173
238
  };
174
239
  }
175
240
  export async function updateNocoBaseSkills(options = {}) {
176
- const workspaceRoot = options.workspaceRoot ? normalizePath(options.workspaceRoot) : resolveSkillsWorkspaceRoot();
241
+ const globalRoot = resolveSkillsRoot(options);
177
242
  const status = await inspectSkillsStatus({
178
- workspaceRoot,
243
+ globalRoot,
179
244
  commandOutputFn: options.commandOutputFn,
180
245
  });
181
246
  if (!status.installed) {
182
- throw new Error(formatSkillsNotInstalledMessage());
247
+ return {
248
+ action: 'noop',
249
+ reason: 'not-installed',
250
+ status,
251
+ };
183
252
  }
184
253
  if (status.managedByNb
185
- && status.latestRef
186
- && status.installedRef
187
- && status.latestRef === status.installedRef) {
254
+ && status.latestVersion
255
+ && status.installedVersion
256
+ && compareVersions(status.latestVersion, status.installedVersion) <= 0) {
188
257
  return {
189
258
  action: 'noop',
259
+ reason: 'up-to-date',
190
260
  status,
191
261
  };
192
262
  }
193
- await (options.runFn ?? run)('npx', ['-y', 'skills', 'update', '-p', '-y', ...status.installedSkillNames], {
194
- cwd: workspaceRoot,
195
- stdio: 'inherit',
196
- errorName: 'skills update',
197
- });
263
+ await reinstallManagedSkills(globalRoot, options, status.latestVersion);
198
264
  return {
199
265
  action: 'updated',
200
- status: await persistManagedSkillsState(workspaceRoot, options),
266
+ status: await persistManagedSkillsState(globalRoot, options),
267
+ };
268
+ }
269
+ export async function removeNocoBaseSkills(options = {}) {
270
+ const globalRoot = resolveSkillsRoot(options);
271
+ const status = await inspectSkillsStatus({
272
+ globalRoot,
273
+ commandOutputFn: options.commandOutputFn,
274
+ });
275
+ if (!status.installed || status.installedSkillNames.length === 0) {
276
+ return {
277
+ action: 'noop',
278
+ status,
279
+ };
280
+ }
281
+ for (const skillName of status.installedSkillNames) {
282
+ await (options.runFn ?? run)('npx', ['-y', 'skills', 'remove', skillName, '-g', '-y'], {
283
+ cwd: globalRoot,
284
+ stdio: options.verbose ? 'inherit' : 'ignore',
285
+ errorName: 'skills remove',
286
+ });
287
+ }
288
+ await fsp.rm(getManagedSkillsStateFile(globalRoot), { force: true });
289
+ return {
290
+ action: 'removed',
291
+ status: await inspectSkillsStatus({
292
+ globalRoot,
293
+ commandOutputFn: options.commandOutputFn,
294
+ }),
201
295
  };
202
296
  }