@nocobase/cli 2.1.0-beta.20 → 2.1.0-beta.21
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.
- package/README.md +4 -4
- package/README.zh-CN.md +2 -2
- package/bin/run.js +15 -0
- package/dist/commands/db/shared.js +19 -5
- package/dist/commands/dev.js +8 -1
- package/dist/commands/down.js +10 -6
- package/dist/commands/env/add.js +14 -34
- package/dist/commands/env/auth.js +6 -13
- package/dist/commands/env/list.js +10 -15
- package/dist/commands/env/remove.js +4 -10
- package/dist/commands/env/update.js +7 -13
- package/dist/commands/env/use.js +5 -13
- package/dist/commands/init.js +190 -62
- package/dist/commands/install.js +65 -26
- package/dist/commands/logs.js +8 -1
- package/dist/commands/pm/list.js +8 -1
- package/dist/commands/ps.js +18 -15
- package/dist/commands/self/check.js +1 -1
- package/dist/commands/self/update.js +13 -3
- package/dist/commands/skills/check.js +11 -5
- package/dist/commands/skills/index.js +1 -1
- package/dist/commands/skills/install.js +20 -7
- package/dist/commands/skills/update.js +20 -7
- package/dist/commands/start.js +8 -1
- package/dist/commands/stop.js +8 -1
- package/dist/commands/upgrade.js +12 -1
- package/dist/lib/api-client.js +3 -2
- package/dist/lib/app-runtime.js +16 -5
- package/dist/lib/auth-store.js +159 -43
- package/dist/lib/bootstrap.js +13 -12
- package/dist/lib/cli-home.js +33 -2
- package/dist/lib/env-auth.js +3 -3
- package/dist/lib/generated-command.js +10 -2
- package/dist/lib/http-request.js +49 -0
- package/dist/lib/resource-command.js +10 -2
- package/dist/lib/runtime-generator.js +1 -1
- package/dist/lib/self-manager.js +1 -1
- package/dist/lib/skills-manager.js +140 -73
- package/dist/lib/startup-update.js +203 -0
- package/dist/locale/en-US.json +4 -1
- package/dist/locale/zh-CN.json +4 -1
- package/package.json +2 -2
|
@@ -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 { Command, Flags } from '@oclif/core';
|
|
2
10
|
import { executeApiRequest } from './api-client.js';
|
|
3
11
|
import { applyPostProcessor } from './post-processors.js';
|
|
@@ -74,7 +82,7 @@ export function createGeneratedFlags(operation) {
|
|
|
74
82
|
exclusive: ['body'],
|
|
75
83
|
});
|
|
76
84
|
}
|
|
77
|
-
flags['base-url'] = Flags.string({
|
|
85
|
+
flags['api-base-url'] = Flags.string({
|
|
78
86
|
description: 'NocoBase API base URL, for example http://localhost:13000/api',
|
|
79
87
|
helpGroup: 'Global',
|
|
80
88
|
});
|
|
@@ -114,7 +122,7 @@ export class GeneratedApiCommand extends Command {
|
|
|
114
122
|
const { flags } = await this.parse(ctor);
|
|
115
123
|
const response = await executeApiRequest({
|
|
116
124
|
envName: flags.env,
|
|
117
|
-
baseUrl: flags['base-url'],
|
|
125
|
+
baseUrl: flags['api-base-url'],
|
|
118
126
|
role: flags.role,
|
|
119
127
|
token: flags.token,
|
|
120
128
|
flags,
|
|
@@ -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;
|
package/dist/lib/self-manager.js
CHANGED
|
@@ -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
|
|
14
|
-
export const
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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, '
|
|
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
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
79
|
+
async function readPublishedSkillsVersion(options = {}) {
|
|
80
|
+
const globalRoot = resolveSkillsRoot(options);
|
|
81
|
+
await ensureSkillsWorkspaceRoot(globalRoot);
|
|
69
82
|
try {
|
|
70
|
-
const output = await (options.commandOutputFn ?? commandOutput)('
|
|
71
|
-
cwd:
|
|
72
|
-
errorName: '
|
|
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
|
|
75
|
-
|
|
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,140 @@ 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
|
|
85
|
-
const stateFile = getManagedSkillsStateFile(
|
|
139
|
+
const globalRoot = resolveSkillsRoot(options);
|
|
140
|
+
const stateFile = getManagedSkillsStateFile(globalRoot);
|
|
86
141
|
const [installedSkills, managedState] = await Promise.all([
|
|
87
|
-
|
|
88
|
-
|
|
142
|
+
listGlobalSkills({
|
|
143
|
+
globalRoot,
|
|
89
144
|
commandOutputFn: options.commandOutputFn,
|
|
90
145
|
}),
|
|
91
|
-
readManagedSkillsState(
|
|
146
|
+
readManagedSkillsState(globalRoot),
|
|
92
147
|
]);
|
|
93
148
|
const installedSkillNames = pickInstalledNocoBaseSkillNames(installedSkills, managedState);
|
|
94
|
-
const managedByNb = managedState?.packageName ===
|
|
95
|
-
let
|
|
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
|
|
100
|
-
|
|
154
|
+
const published = await readPublishedSkillsVersion({
|
|
155
|
+
globalRoot,
|
|
101
156
|
commandOutputFn: options.commandOutputFn,
|
|
102
157
|
});
|
|
103
|
-
|
|
104
|
-
registryError =
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
167
|
+
globalRoot,
|
|
168
|
+
workspaceRoot: globalRoot,
|
|
111
169
|
stateFile,
|
|
112
170
|
installed: installedSkillNames.length > 0,
|
|
113
171
|
managedByNb,
|
|
114
|
-
sourcePackage:
|
|
172
|
+
sourcePackage: managedState?.sourcePackage ?? NOCOBASE_SKILLS_SOURCE,
|
|
173
|
+
npmPackageName: managedState?.packageName ?? NOCOBASE_SKILLS_PACKAGE_NAME,
|
|
115
174
|
installedSkillNames,
|
|
116
|
-
|
|
117
|
-
|
|
175
|
+
latestVersion,
|
|
176
|
+
installedVersion,
|
|
177
|
+
latestRef: latestVersion,
|
|
178
|
+
installedRef: installedVersion,
|
|
118
179
|
updateAvailable,
|
|
119
180
|
registryError,
|
|
120
181
|
};
|
|
121
182
|
}
|
|
122
183
|
function formatSkillsNotInstalledMessage() {
|
|
123
184
|
return [
|
|
124
|
-
'NocoBase AI coding skills are not installed
|
|
185
|
+
'NocoBase AI coding skills are not installed globally.',
|
|
125
186
|
'Run `nb skills install` first.',
|
|
126
187
|
].join('\n');
|
|
127
188
|
}
|
|
128
|
-
async function persistManagedSkillsState(
|
|
129
|
-
const installedSkills = await
|
|
130
|
-
|
|
189
|
+
async function persistManagedSkillsState(globalRoot, options = {}) {
|
|
190
|
+
const installedSkills = await listGlobalSkills({
|
|
191
|
+
globalRoot,
|
|
131
192
|
commandOutputFn: options.commandOutputFn,
|
|
132
193
|
});
|
|
133
|
-
const managedState = await readManagedSkillsState(
|
|
194
|
+
const managedState = await readManagedSkillsState(globalRoot);
|
|
134
195
|
const installedSkillNames = pickInstalledNocoBaseSkillNames(installedSkills, managedState);
|
|
135
|
-
const
|
|
136
|
-
|
|
196
|
+
const published = await readPublishedSkillsVersion({
|
|
197
|
+
globalRoot,
|
|
137
198
|
commandOutputFn: options.commandOutputFn,
|
|
138
199
|
});
|
|
139
200
|
const now = new Date().toISOString();
|
|
140
|
-
await writeManagedSkillsState(
|
|
141
|
-
packageName:
|
|
142
|
-
|
|
201
|
+
await writeManagedSkillsState(globalRoot, {
|
|
202
|
+
packageName: NOCOBASE_SKILLS_PACKAGE_NAME,
|
|
203
|
+
sourcePackage: NOCOBASE_SKILLS_SOURCE,
|
|
143
204
|
installedAt: managedState?.installedAt ?? now,
|
|
144
205
|
updatedAt: now,
|
|
145
|
-
|
|
206
|
+
installedVersion: published.version,
|
|
146
207
|
skillNames: installedSkillNames,
|
|
147
208
|
});
|
|
148
209
|
return await inspectSkillsStatus({
|
|
149
|
-
|
|
210
|
+
globalRoot,
|
|
150
211
|
commandOutputFn: options.commandOutputFn,
|
|
151
212
|
});
|
|
152
213
|
}
|
|
214
|
+
async function reinstallManagedSkills(globalRoot, options = {}, targetVersion) {
|
|
215
|
+
const prepared = await prepareLocalSkillsPackage(globalRoot, options, targetVersion);
|
|
216
|
+
try {
|
|
217
|
+
await (options.runFn ?? run)('npx', ['-y', 'skills', 'add', prepared.packageDir, '-g', '-y'], {
|
|
218
|
+
cwd: globalRoot,
|
|
219
|
+
stdio: options.verbose ? 'inherit' : 'ignore',
|
|
220
|
+
errorName: 'skills add',
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
await prepared.cleanup();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
153
227
|
export async function installNocoBaseSkills(options = {}) {
|
|
154
|
-
const
|
|
228
|
+
const globalRoot = resolveSkillsRoot(options);
|
|
155
229
|
const status = await inspectSkillsStatus({
|
|
156
|
-
|
|
230
|
+
globalRoot,
|
|
157
231
|
commandOutputFn: options.commandOutputFn,
|
|
158
232
|
});
|
|
159
233
|
if (status.installed) {
|
|
@@ -162,41 +236,34 @@ export async function installNocoBaseSkills(options = {}) {
|
|
|
162
236
|
status,
|
|
163
237
|
};
|
|
164
238
|
}
|
|
165
|
-
await (
|
|
166
|
-
|
|
167
|
-
stdio: 'inherit',
|
|
168
|
-
errorName: 'skills add',
|
|
169
|
-
});
|
|
239
|
+
await ensureSkillsWorkspaceRoot(globalRoot);
|
|
240
|
+
await reinstallManagedSkills(globalRoot, options, status.latestVersion);
|
|
170
241
|
return {
|
|
171
242
|
action: 'installed',
|
|
172
|
-
status: await persistManagedSkillsState(
|
|
243
|
+
status: await persistManagedSkillsState(globalRoot, options),
|
|
173
244
|
};
|
|
174
245
|
}
|
|
175
246
|
export async function updateNocoBaseSkills(options = {}) {
|
|
176
|
-
const
|
|
247
|
+
const globalRoot = resolveSkillsRoot(options);
|
|
177
248
|
const status = await inspectSkillsStatus({
|
|
178
|
-
|
|
249
|
+
globalRoot,
|
|
179
250
|
commandOutputFn: options.commandOutputFn,
|
|
180
251
|
});
|
|
181
252
|
if (!status.installed) {
|
|
182
253
|
throw new Error(formatSkillsNotInstalledMessage());
|
|
183
254
|
}
|
|
184
255
|
if (status.managedByNb
|
|
185
|
-
&& status.
|
|
186
|
-
&& status.
|
|
187
|
-
&& status.
|
|
256
|
+
&& status.latestVersion
|
|
257
|
+
&& status.installedVersion
|
|
258
|
+
&& compareVersions(status.latestVersion, status.installedVersion) <= 0) {
|
|
188
259
|
return {
|
|
189
260
|
action: 'noop',
|
|
190
261
|
status,
|
|
191
262
|
};
|
|
192
263
|
}
|
|
193
|
-
await (
|
|
194
|
-
cwd: workspaceRoot,
|
|
195
|
-
stdio: 'inherit',
|
|
196
|
-
errorName: 'skills update',
|
|
197
|
-
});
|
|
264
|
+
await reinstallManagedSkills(globalRoot, options, status.latestVersion);
|
|
198
265
|
return {
|
|
199
266
|
action: 'updated',
|
|
200
|
-
status: await persistManagedSkillsState(
|
|
267
|
+
status: await persistManagedSkillsState(globalRoot, options),
|
|
201
268
|
};
|
|
202
269
|
}
|
|
@@ -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
|
+
}
|