@nocobase/cli 2.1.0-beta.20 → 2.1.0-beta.22

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 +32 -50
  2. package/README.zh-CN.md +29 -46
  3. package/bin/run.js +15 -0
  4. package/dist/commands/app/down.js +260 -0
  5. package/dist/commands/app/info.js +140 -0
  6. package/dist/commands/app/logs.js +98 -0
  7. package/dist/commands/app/ps.js +60 -0
  8. package/dist/commands/app/restart.js +75 -0
  9. package/dist/commands/app/shared.js +95 -0
  10. package/dist/commands/app/start.js +252 -0
  11. package/dist/commands/app/stop.js +98 -0
  12. package/dist/commands/app/upgrade.js +595 -0
  13. package/dist/commands/build.js +3 -48
  14. package/dist/commands/db/shared.js +19 -5
  15. package/dist/commands/dev.js +3 -140
  16. package/dist/commands/down.js +3 -184
  17. package/dist/commands/download.js +4 -856
  18. package/dist/commands/env/add.js +33 -48
  19. package/dist/commands/env/auth.js +6 -13
  20. package/dist/commands/env/list.js +10 -15
  21. package/dist/commands/env/remove.js +4 -10
  22. package/dist/commands/env/update.js +7 -13
  23. package/dist/commands/env/use.js +5 -13
  24. package/dist/commands/{prompts-stages.js → examples/prompts-stages.js} +3 -3
  25. package/dist/commands/{prompts-test.js → examples/prompts-test.js} +3 -3
  26. package/dist/commands/init.js +262 -63
  27. package/dist/commands/install.js +352 -86
  28. package/dist/commands/logs.js +3 -81
  29. package/dist/commands/plugin/disable.js +64 -0
  30. package/dist/commands/plugin/enable.js +64 -0
  31. package/dist/commands/plugin/list.js +62 -0
  32. package/dist/commands/pm/disable.js +3 -54
  33. package/dist/commands/pm/enable.js +3 -54
  34. package/dist/commands/pm/list.js +3 -45
  35. package/dist/commands/ps.js +3 -107
  36. package/dist/commands/restart.js +3 -65
  37. package/dist/commands/scaffold/migration.js +1 -1
  38. package/dist/commands/scaffold/plugin.js +1 -1
  39. package/dist/commands/self/check.js +1 -1
  40. package/dist/commands/self/update.js +13 -3
  41. package/dist/commands/skills/check.js +11 -5
  42. package/dist/commands/skills/index.js +1 -1
  43. package/dist/commands/skills/install.js +20 -7
  44. package/dist/commands/skills/remove.js +71 -0
  45. package/dist/commands/skills/update.js +27 -7
  46. package/dist/commands/source/build.js +58 -0
  47. package/dist/commands/source/dev.js +157 -0
  48. package/dist/commands/source/download.js +866 -0
  49. package/dist/commands/source/test.js +467 -0
  50. package/dist/commands/start.js +3 -202
  51. package/dist/commands/stop.js +3 -81
  52. package/dist/commands/test.js +3 -457
  53. package/dist/commands/upgrade.js +3 -574
  54. package/dist/help/runtime-help.js +3 -0
  55. package/dist/lib/api-client.js +3 -2
  56. package/dist/lib/app-health.js +126 -0
  57. package/dist/lib/app-managed-resources.js +264 -0
  58. package/dist/lib/app-runtime.js +16 -5
  59. package/dist/lib/auth-store.js +162 -43
  60. package/dist/lib/bootstrap.js +13 -12
  61. package/dist/lib/cli-home.js +38 -6
  62. package/dist/lib/cli-locale.js +15 -1
  63. package/dist/lib/env-auth.js +3 -3
  64. package/dist/lib/env-config.js +80 -0
  65. package/dist/lib/generated-command.js +10 -2
  66. package/dist/lib/http-request.js +49 -0
  67. package/dist/lib/resource-command.js +10 -2
  68. package/dist/lib/runtime-generator.js +1 -1
  69. package/dist/lib/self-manager.js +1 -1
  70. package/dist/lib/skills-manager.js +173 -79
  71. package/dist/lib/startup-update.js +203 -0
  72. package/dist/locale/en-US.json +4 -1
  73. package/dist/locale/zh-CN.json +4 -1
  74. package/package.json +26 -3
@@ -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
+ }
@@ -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
  }
@@ -0,0 +1,203 @@
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/promises';
10
+ import path from 'node:path';
11
+ import * as p from '@clack/prompts';
12
+ import { inspectSelfStatus, } from './self-manager.js';
13
+ import { inspectSkillsStatus } from './skills-manager.js';
14
+ import { resolveCliHomeDir } from './cli-home.js';
15
+ import { isInteractiveTerminal, printWarning } from './ui.js';
16
+ import { run } from './run-npm.js';
17
+ const STARTUP_UPDATE_STATE_FILE = 'startup-update.json';
18
+ const NB_SKIP_STARTUP_UPDATE_ENV = 'NB_SKIP_STARTUP_UPDATE';
19
+ function getStateFile() {
20
+ return path.join(resolveCliHomeDir('global'), STARTUP_UPDATE_STATE_FILE);
21
+ }
22
+ function todayStamp(now = new Date()) {
23
+ return now.toISOString().slice(0, 10);
24
+ }
25
+ function shouldSkipByArgv(argv) {
26
+ const tokens = argv.filter((token) => token && !token.startsWith('-'));
27
+ if (tokens.length === 0) {
28
+ return false;
29
+ }
30
+ if (tokens[0] === 'self' || tokens[0] === 'skills') {
31
+ return true;
32
+ }
33
+ return false;
34
+ }
35
+ async function readState() {
36
+ try {
37
+ const raw = await fs.readFile(getStateFile(), 'utf8');
38
+ return JSON.parse(raw);
39
+ }
40
+ catch {
41
+ return {};
42
+ }
43
+ }
44
+ async function writeState(state) {
45
+ const filePath = getStateFile();
46
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
47
+ await fs.writeFile(filePath, JSON.stringify(state, null, 2));
48
+ }
49
+ async function markChecked(now = new Date()) {
50
+ await writeState({ lastCheckedDate: todayStamp(now) });
51
+ }
52
+ export async function shouldRunStartupUpdateCheck(argv, now = new Date()) {
53
+ if (process.env[NB_SKIP_STARTUP_UPDATE_ENV] === '1') {
54
+ return false;
55
+ }
56
+ if (shouldSkipByArgv(argv)) {
57
+ return false;
58
+ }
59
+ const state = await readState();
60
+ return state.lastCheckedDate !== todayStamp(now);
61
+ }
62
+ export function shouldEnableStartupUpdateForInstallMethod(installMethod) {
63
+ return installMethod === 'npm-global';
64
+ }
65
+ function hasPendingUpdates(selfStatus, skillsStatus) {
66
+ return Boolean(selfStatus.updateAvailable || skillsStatus.updateAvailable === true);
67
+ }
68
+ function describeCliUpdate(selfStatus) {
69
+ return selfStatus.latestVersion
70
+ ? `NocoBase CLI: ${selfStatus.currentVersion} -> ${selfStatus.latestVersion}`
71
+ : `NocoBase CLI: update available from ${selfStatus.currentVersion}`;
72
+ }
73
+ function describeSkillsUpdate() {
74
+ return 'NocoBase AI skills: update available';
75
+ }
76
+ function describeSkillsUpdateWithVersion(skillsStatus) {
77
+ if (skillsStatus.installedVersion && skillsStatus.latestVersion) {
78
+ return `NocoBase AI skills: ${skillsStatus.installedVersion} -> ${skillsStatus.latestVersion}`;
79
+ }
80
+ if (skillsStatus.latestVersion) {
81
+ return `NocoBase AI skills: latest ${skillsStatus.latestVersion} available`;
82
+ }
83
+ return describeSkillsUpdate();
84
+ }
85
+ function buildPromptMessage(selfStatus, skillsStatus) {
86
+ const lines = [];
87
+ const hasCliUpdate = selfStatus.updateAvailable;
88
+ const hasSkillsUpdate = skillsStatus.updateAvailable === true;
89
+ if (hasCliUpdate && hasSkillsUpdate) {
90
+ lines.push('Updates are available for your NocoBase CLI and AI skills.');
91
+ }
92
+ else if (hasCliUpdate) {
93
+ lines.push('An update is available for your NocoBase CLI.');
94
+ }
95
+ else if (hasSkillsUpdate) {
96
+ lines.push('An update is available for your NocoBase AI skills.');
97
+ }
98
+ else {
99
+ lines.push('A NocoBase CLI or skills update is available.');
100
+ }
101
+ if (hasCliUpdate) {
102
+ lines.push(`- ${describeCliUpdate(selfStatus)}`);
103
+ }
104
+ if (hasSkillsUpdate) {
105
+ lines.push(`- ${describeSkillsUpdateWithVersion(skillsStatus)}`);
106
+ }
107
+ lines.push('Update now?');
108
+ return lines.join('\n');
109
+ }
110
+ function buildUpdateCommands(selfStatus, skillsStatus) {
111
+ const commands = [];
112
+ if (selfStatus.updateAvailable && selfStatus.updatable) {
113
+ commands.push('nb self update --yes');
114
+ }
115
+ if (skillsStatus.updateAvailable === true) {
116
+ commands.push('nb skills update --yes');
117
+ }
118
+ return commands;
119
+ }
120
+ function buildNonInteractiveWarning(selfStatus, skillsStatus) {
121
+ const commands = buildUpdateCommands(selfStatus, skillsStatus);
122
+ const details = [];
123
+ if (selfStatus.updateAvailable) {
124
+ details.push(describeCliUpdate(selfStatus));
125
+ }
126
+ if (skillsStatus.updateAvailable === true) {
127
+ details.push(describeSkillsUpdateWithVersion(skillsStatus));
128
+ }
129
+ return [
130
+ `Updates available${details.length ? `: ${details.join(', ')}` : '.'}`,
131
+ 'Non-interactive session, skipped auto-update.',
132
+ commands.length
133
+ ? `Run: ${commands.join(' && ')}`
134
+ : 'Check with: `nb self check` and `nb skills check`.',
135
+ 'You may run into compatibility issues until you update.',
136
+ ].join(' ');
137
+ }
138
+ function buildDeclinedWarning(selfStatus, skillsStatus) {
139
+ const commands = buildUpdateCommands(selfStatus, skillsStatus);
140
+ const details = [];
141
+ if (selfStatus.updateAvailable) {
142
+ details.push(describeCliUpdate(selfStatus));
143
+ }
144
+ if (skillsStatus.updateAvailable === true) {
145
+ details.push(describeSkillsUpdateWithVersion(skillsStatus));
146
+ }
147
+ return [
148
+ `Skipped updates${details.length ? `: ${details.join(', ')}` : '.'}`,
149
+ commands.length
150
+ ? `Run: ${commands.join(' && ')}`
151
+ : 'Check with: `nb self check` and `nb skills check`.',
152
+ 'You may run into compatibility issues until you update.',
153
+ ].join(' ');
154
+ }
155
+ async function runStartupUpdates() {
156
+ await run('nb', ['self', 'update', '--yes'], {
157
+ stdio: 'inherit',
158
+ env: {
159
+ [NB_SKIP_STARTUP_UPDATE_ENV]: '1',
160
+ },
161
+ errorName: 'nb self update',
162
+ });
163
+ await run('nb', ['skills', 'update', '--yes'], {
164
+ stdio: 'inherit',
165
+ env: {
166
+ [NB_SKIP_STARTUP_UPDATE_ENV]: '1',
167
+ },
168
+ errorName: 'nb skills update',
169
+ });
170
+ }
171
+ export async function maybeRunStartupUpdatePrompt(argv) {
172
+ if (!(await shouldRunStartupUpdateCheck(argv))) {
173
+ return { kind: 'skipped' };
174
+ }
175
+ const selfStatus = await inspectSelfStatus();
176
+ if (!shouldEnableStartupUpdateForInstallMethod(selfStatus.installMethod)) {
177
+ return { kind: 'skipped' };
178
+ }
179
+ const skillsStatus = await inspectSkillsStatus();
180
+ if (!hasPendingUpdates(selfStatus, skillsStatus)) {
181
+ await markChecked();
182
+ return { kind: 'no-update' };
183
+ }
184
+ if (!isInteractiveTerminal()) {
185
+ printWarning(buildNonInteractiveWarning(selfStatus, skillsStatus));
186
+ await markChecked();
187
+ return { kind: 'warned' };
188
+ }
189
+ const answer = await p.confirm({
190
+ message: buildPromptMessage(selfStatus, skillsStatus),
191
+ active: 'Yes',
192
+ inactive: 'No',
193
+ initialValue: true,
194
+ });
195
+ if (p.isCancel(answer) || !answer) {
196
+ printWarning(buildDeclinedWarning(selfStatus, skillsStatus));
197
+ await markChecked();
198
+ return { kind: 'declined' };
199
+ }
200
+ await runStartupUpdates();
201
+ await markChecked();
202
+ return { kind: 'updated' };
203
+ }