@nocobase/cli 2.1.0-alpha.25 → 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.
- package/README.md +37 -49
- package/README.zh-CN.md +36 -47
- package/dist/commands/app/down.js +260 -0
- package/dist/commands/app/logs.js +98 -0
- package/dist/commands/app/restart.js +75 -0
- package/dist/commands/app/start.js +252 -0
- package/dist/commands/app/stop.js +98 -0
- package/dist/commands/app/upgrade.js +595 -0
- package/dist/commands/build.js +3 -48
- package/dist/commands/dev.js +3 -147
- package/dist/commands/down.js +3 -188
- package/dist/commands/download.js +4 -856
- package/dist/commands/env/add.js +28 -23
- package/dist/commands/env/info.js +152 -0
- package/dist/commands/env/list.js +23 -9
- package/dist/commands/env/shared.js +158 -0
- package/dist/commands/{prompts-stages.js → examples/prompts-stages.js} +3 -3
- package/dist/commands/{prompts-test.js → examples/prompts-test.js} +3 -3
- package/dist/commands/init.js +84 -6
- package/dist/commands/install.js +288 -61
- package/dist/commands/logs.js +3 -88
- package/dist/commands/plugin/disable.js +64 -0
- package/dist/commands/plugin/enable.js +64 -0
- package/dist/commands/plugin/list.js +62 -0
- package/dist/commands/pm/disable.js +3 -54
- package/dist/commands/pm/enable.js +3 -54
- package/dist/commands/pm/list.js +3 -52
- package/dist/commands/restart.js +3 -65
- package/dist/commands/scaffold/migration.js +1 -1
- package/dist/commands/scaffold/plugin.js +1 -1
- package/dist/commands/skills/remove.js +71 -0
- package/dist/commands/skills/update.js +7 -0
- package/dist/commands/source/build.js +58 -0
- package/dist/commands/source/dev.js +157 -0
- package/dist/commands/source/download.js +866 -0
- package/dist/commands/source/test.js +467 -0
- package/dist/commands/start.js +3 -209
- package/dist/commands/stop.js +3 -88
- package/dist/commands/test.js +3 -457
- package/dist/commands/upgrade.js +3 -585
- package/dist/help/runtime-help.js +3 -0
- package/dist/lib/api-client.js +20 -6
- package/dist/lib/app-health.js +126 -0
- package/dist/lib/app-managed-resources.js +264 -0
- package/dist/lib/auth-store.js +5 -2
- package/dist/lib/cli-home.js +7 -6
- package/dist/lib/cli-locale.js +15 -1
- package/dist/lib/env-config.js +80 -0
- package/dist/lib/prompt-web-ui.js +13 -6
- package/dist/lib/skills-manager.js +34 -7
- package/package.json +27 -4
- package/dist/commands/ps.js +0 -119
|
@@ -0,0 +1,126 @@
|
|
|
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 { printInfo, startTask, stopTask, updateTask } from './ui.js';
|
|
10
|
+
const APP_HEALTH_CHECK_INTERVAL_MS = 2_000;
|
|
11
|
+
const APP_HEALTH_CHECK_TIMEOUT_MS = 600_000;
|
|
12
|
+
const APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS = 5_000;
|
|
13
|
+
function trimValue(value) {
|
|
14
|
+
return String(value ?? '').trim();
|
|
15
|
+
}
|
|
16
|
+
function buildHealthCheckUrl(apiBaseUrl) {
|
|
17
|
+
return `${apiBaseUrl.replace(/\/+$/, '')}/__health_check`;
|
|
18
|
+
}
|
|
19
|
+
async function sleep(ms) {
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
async function requestAppHealthCheck(params) {
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const timeout = setTimeout(() => {
|
|
25
|
+
controller.abort();
|
|
26
|
+
}, params.requestTimeoutMs ?? APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS);
|
|
27
|
+
try {
|
|
28
|
+
const response = await (params.fetchImpl ?? fetch)(params.healthCheckUrl, {
|
|
29
|
+
method: 'GET',
|
|
30
|
+
signal: controller.signal,
|
|
31
|
+
});
|
|
32
|
+
const text = await response.text().catch(() => '');
|
|
33
|
+
const body = text.replace(/\s+/g, ' ').trim() || 'No response yet';
|
|
34
|
+
return {
|
|
35
|
+
ok: response.ok && text.trim().toLowerCase() === 'ok',
|
|
36
|
+
message: `HTTP ${response.status}: ${body}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
message: `No response within ${Math.ceil((params.requestTimeoutMs ?? APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS) / 1000)}s`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
message: error instanceof Error ? error.message : String(error),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
clearTimeout(timeout);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export class AppHealthCheckError extends Error {
|
|
56
|
+
}
|
|
57
|
+
export function formatAppUrl(port) {
|
|
58
|
+
const value = trimValue(port);
|
|
59
|
+
return value ? `http://127.0.0.1:${value}` : undefined;
|
|
60
|
+
}
|
|
61
|
+
export function resolveManagedAppApiBaseUrl(runtime, options) {
|
|
62
|
+
const override = trimValue(options?.portOverride);
|
|
63
|
+
if (override) {
|
|
64
|
+
return `http://127.0.0.1:${override}/api`;
|
|
65
|
+
}
|
|
66
|
+
const baseUrl = trimValue(runtime.env.baseUrl);
|
|
67
|
+
if (baseUrl) {
|
|
68
|
+
return baseUrl.replace(/\/+$/, '');
|
|
69
|
+
}
|
|
70
|
+
const appPort = runtime.env.appPort === undefined || runtime.env.appPort === null
|
|
71
|
+
? ''
|
|
72
|
+
: trimValue(runtime.env.appPort);
|
|
73
|
+
return appPort ? `http://127.0.0.1:${appPort}/api` : undefined;
|
|
74
|
+
}
|
|
75
|
+
export async function isAppReady(apiBaseUrl, options) {
|
|
76
|
+
const baseUrl = trimValue(apiBaseUrl);
|
|
77
|
+
if (!baseUrl) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const result = await requestAppHealthCheck({
|
|
81
|
+
healthCheckUrl: buildHealthCheckUrl(baseUrl),
|
|
82
|
+
fetchImpl: options?.fetchImpl,
|
|
83
|
+
requestTimeoutMs: options?.requestTimeoutMs,
|
|
84
|
+
});
|
|
85
|
+
return result.ok;
|
|
86
|
+
}
|
|
87
|
+
export async function waitForAppReady(params) {
|
|
88
|
+
const apiBaseUrl = trimValue(params.apiBaseUrl);
|
|
89
|
+
if (!apiBaseUrl) {
|
|
90
|
+
printInfo(`Skipping health check for "${params.envName}" because no local API URL is saved for this env.`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const healthCheckUrl = buildHealthCheckUrl(apiBaseUrl);
|
|
94
|
+
const startedAt = Date.now();
|
|
95
|
+
let lastMessage = 'No response yet';
|
|
96
|
+
let spinnerActive = true;
|
|
97
|
+
startTask(`Waiting for NocoBase to become ready for "${params.envName}"...`);
|
|
98
|
+
try {
|
|
99
|
+
while (Date.now() - startedAt < APP_HEALTH_CHECK_TIMEOUT_MS) {
|
|
100
|
+
const result = await requestAppHealthCheck({
|
|
101
|
+
healthCheckUrl,
|
|
102
|
+
fetchImpl: params.fetchImpl,
|
|
103
|
+
});
|
|
104
|
+
if (result.ok) {
|
|
105
|
+
stopTask();
|
|
106
|
+
spinnerActive = false;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
lastMessage = result.message;
|
|
110
|
+
const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
|
|
111
|
+
updateTask(`Waiting for NocoBase to become ready for "${params.envName}"... (${elapsedSeconds}s elapsed, last status: ${lastMessage})`);
|
|
112
|
+
await sleep(APP_HEALTH_CHECK_INTERVAL_MS);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
if (spinnerActive) {
|
|
117
|
+
stopTask();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const hints = [
|
|
121
|
+
params.logHint,
|
|
122
|
+
params.containerName ? `docker logs ${params.containerName}` : undefined,
|
|
123
|
+
].filter(Boolean);
|
|
124
|
+
const hintText = hints.length > 0 ? ` ${hints.join(' ')}` : '';
|
|
125
|
+
throw new AppHealthCheckError(`NocoBase did not become ready in time for "${params.envName}". Expected \`${healthCheckUrl}\` to respond with \`ok\`, but the last status was: ${lastMessage}.${hintText}`);
|
|
126
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
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 { mkdir, readdir } from 'node:fs/promises';
|
|
10
|
+
import { dockerContainerExists, startDockerContainer } from './app-runtime.js';
|
|
11
|
+
import { resolveConfiguredEnvPath } from './cli-home.js';
|
|
12
|
+
import { commandSucceeds, run } from './run-npm.js';
|
|
13
|
+
import Install from '../commands/install.js';
|
|
14
|
+
const DEFAULT_DOCKER_REGISTRY = 'nocobase/nocobase';
|
|
15
|
+
const DEFAULT_DOCKER_VERSION = 'alpha';
|
|
16
|
+
const DOCKER_APP_STORAGE_DESTINATION = '/app/nocobase/storage';
|
|
17
|
+
function commandStdio(verbose) {
|
|
18
|
+
return verbose ? 'inherit' : 'ignore';
|
|
19
|
+
}
|
|
20
|
+
async function ensureDockerNetwork(networkName) {
|
|
21
|
+
if (await commandSucceeds('docker', ['network', 'inspect', networkName])) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
await run('docker', ['network', 'create', networkName], {
|
|
25
|
+
errorName: 'docker network create',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function localSourceLabel(source) {
|
|
29
|
+
return source === 'git' ? 'Git checkout' : 'npm app';
|
|
30
|
+
}
|
|
31
|
+
function trimValue(value) {
|
|
32
|
+
return String(value ?? '').trim();
|
|
33
|
+
}
|
|
34
|
+
function normalizeDockerPlatform(value) {
|
|
35
|
+
const text = trimValue(value);
|
|
36
|
+
if (!text || text === 'auto') {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
if (text === 'linux/amd64' || text === 'linux/arm64') {
|
|
40
|
+
return text;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
function formatBuiltinDbFailure(envName, message) {
|
|
45
|
+
return [
|
|
46
|
+
`Couldn't restore the built-in database for "${envName}".`,
|
|
47
|
+
'Check the saved database settings, local storage path, and Docker runtime, then try again.',
|
|
48
|
+
`Details: ${message}`,
|
|
49
|
+
].join('\n');
|
|
50
|
+
}
|
|
51
|
+
function formatLocalSourceRestoreFailure(envName, source, message) {
|
|
52
|
+
const sourceLabel = source === 'git' ? 'the saved Git checkout' : 'the saved npm app';
|
|
53
|
+
return [
|
|
54
|
+
`Couldn't restore NocoBase files for "${envName}".`,
|
|
55
|
+
`The CLI was not able to download ${sourceLabel} before starting the app again.`,
|
|
56
|
+
'Check the saved source settings for this env, then try again.',
|
|
57
|
+
`Details: ${message}`,
|
|
58
|
+
].join('\n');
|
|
59
|
+
}
|
|
60
|
+
function formatSavedDockerSettingsIncomplete(envName, missing) {
|
|
61
|
+
return [
|
|
62
|
+
`Can't start NocoBase for "${envName}" yet.`,
|
|
63
|
+
`The saved Docker settings for this env are incomplete. Missing: ${missing.join(', ')}.`,
|
|
64
|
+
'Re-run `nb init` or `nb env add` to refresh this env config, then try again.',
|
|
65
|
+
].join('\n');
|
|
66
|
+
}
|
|
67
|
+
function formatDockerAppRecreateFailure(envName, message) {
|
|
68
|
+
return [
|
|
69
|
+
`Couldn't start NocoBase for "${envName}".`,
|
|
70
|
+
'The CLI was not able to recreate the saved Docker app container successfully.',
|
|
71
|
+
'Check the saved Docker image, container settings, and database connection, then try again.',
|
|
72
|
+
`Details: ${message}`,
|
|
73
|
+
].join('\n');
|
|
74
|
+
}
|
|
75
|
+
async function localProjectHasFiles(projectRoot) {
|
|
76
|
+
try {
|
|
77
|
+
const entries = await readdir(projectRoot);
|
|
78
|
+
return entries.length > 0;
|
|
79
|
+
}
|
|
80
|
+
catch (_error) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function buildSavedDockerRunArgs(runtime) {
|
|
85
|
+
const config = runtime.env.config ?? {};
|
|
86
|
+
const configuredStoragePath = trimValue(config.storagePath);
|
|
87
|
+
const storagePath = configuredStoragePath
|
|
88
|
+
? trimValue(resolveConfiguredEnvPath(configuredStoragePath))
|
|
89
|
+
: '';
|
|
90
|
+
const appPort = runtime.env.appPort === undefined || runtime.env.appPort === null
|
|
91
|
+
? ''
|
|
92
|
+
: trimValue(runtime.env.appPort);
|
|
93
|
+
const appKey = trimValue(config.appKey);
|
|
94
|
+
const timeZone = trimValue(config.timezone) || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
|
95
|
+
const dbDialect = trimValue(config.dbDialect);
|
|
96
|
+
const dbHost = trimValue(config.dbHost);
|
|
97
|
+
const dbPort = trimValue(config.dbPort);
|
|
98
|
+
const dbDatabase = trimValue(config.dbDatabase);
|
|
99
|
+
const dbUser = trimValue(config.dbUser);
|
|
100
|
+
const dbPassword = trimValue(config.dbPassword);
|
|
101
|
+
const dockerRegistry = trimValue(config.dockerRegistry) || DEFAULT_DOCKER_REGISTRY;
|
|
102
|
+
const version = trimValue(config.downloadVersion) || DEFAULT_DOCKER_VERSION;
|
|
103
|
+
const imageRef = `${dockerRegistry}:${version}`;
|
|
104
|
+
const missing = [];
|
|
105
|
+
if (!storagePath) {
|
|
106
|
+
missing.push('storagePath');
|
|
107
|
+
}
|
|
108
|
+
if (!appKey) {
|
|
109
|
+
missing.push('appKey');
|
|
110
|
+
}
|
|
111
|
+
if (!dbDialect) {
|
|
112
|
+
missing.push('dbDialect');
|
|
113
|
+
}
|
|
114
|
+
if (!dbHost) {
|
|
115
|
+
missing.push('dbHost');
|
|
116
|
+
}
|
|
117
|
+
if (!dbPort) {
|
|
118
|
+
missing.push('dbPort');
|
|
119
|
+
}
|
|
120
|
+
if (!dbDatabase) {
|
|
121
|
+
missing.push('dbDatabase');
|
|
122
|
+
}
|
|
123
|
+
if (!dbUser) {
|
|
124
|
+
missing.push('dbUser');
|
|
125
|
+
}
|
|
126
|
+
if (!dbPassword) {
|
|
127
|
+
missing.push('dbPassword');
|
|
128
|
+
}
|
|
129
|
+
if (missing.length > 0) {
|
|
130
|
+
throw new Error(formatSavedDockerSettingsIncomplete(runtime.envName, missing));
|
|
131
|
+
}
|
|
132
|
+
const args = [
|
|
133
|
+
'run',
|
|
134
|
+
'-d',
|
|
135
|
+
'--name',
|
|
136
|
+
runtime.containerName,
|
|
137
|
+
'--restart',
|
|
138
|
+
'always',
|
|
139
|
+
'--network',
|
|
140
|
+
runtime.workspaceName,
|
|
141
|
+
];
|
|
142
|
+
const dockerPlatform = normalizeDockerPlatform(config.dockerPlatform);
|
|
143
|
+
if (dockerPlatform) {
|
|
144
|
+
args.push('--platform', dockerPlatform);
|
|
145
|
+
}
|
|
146
|
+
if (appPort) {
|
|
147
|
+
args.push('-p', `${appPort}:80`);
|
|
148
|
+
}
|
|
149
|
+
args.push('-e', `APP_KEY=${appKey}`, '-e', `DB_DIALECT=${dbDialect}`, '-e', `DB_HOST=${dbHost}`, '-e', `DB_PORT=${dbPort}`, '-e', `DB_DATABASE=${dbDatabase}`, '-e', `DB_USER=${dbUser}`, '-e', `DB_PASSWORD=${dbPassword}`, '-e', `TZ=${timeZone}`, '-v', `${storagePath}:${DOCKER_APP_STORAGE_DESTINATION}`, imageRef);
|
|
150
|
+
return {
|
|
151
|
+
appPort: appPort || undefined,
|
|
152
|
+
storagePath,
|
|
153
|
+
imageRef,
|
|
154
|
+
args,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
export async function recreateSavedDockerApp(runtime, options) {
|
|
158
|
+
const plan = buildSavedDockerRunArgs(runtime);
|
|
159
|
+
try {
|
|
160
|
+
await ensureDockerNetwork(runtime.workspaceName);
|
|
161
|
+
await mkdir(plan.storagePath, { recursive: true });
|
|
162
|
+
await run('docker', plan.args, {
|
|
163
|
+
errorName: 'docker run',
|
|
164
|
+
stdio: commandStdio(options?.verbose),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
169
|
+
if (message.includes(`Can't start NocoBase for "${runtime.envName}" yet.`)
|
|
170
|
+
|| message.includes(`Couldn't start NocoBase for "${runtime.envName}".`)) {
|
|
171
|
+
throw error instanceof Error ? error : new Error(message);
|
|
172
|
+
}
|
|
173
|
+
throw new Error(formatDockerAppRecreateFailure(runtime.envName, message));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export async function ensureBuiltinDbReady(runtime, options) {
|
|
177
|
+
const config = runtime.env.config ?? {};
|
|
178
|
+
if (!config.builtinDb) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const plan = Install.buildBuiltinDbPlan({
|
|
182
|
+
envName: runtime.envName,
|
|
183
|
+
workspaceName: runtime.workspaceName,
|
|
184
|
+
storagePath: config.storagePath,
|
|
185
|
+
source: runtime.source,
|
|
186
|
+
dbDialect: config.dbDialect,
|
|
187
|
+
dbHost: config.dbHost,
|
|
188
|
+
dbPort: config.dbPort,
|
|
189
|
+
dbDatabase: config.dbDatabase,
|
|
190
|
+
dbUser: config.dbUser,
|
|
191
|
+
dbPassword: config.dbPassword,
|
|
192
|
+
builtinDbImage: config.builtinDbImage,
|
|
193
|
+
});
|
|
194
|
+
options?.onStartTask?.(`Restoring the built-in ${plan.dbDialect} database for "${runtime.envName}"...`);
|
|
195
|
+
try {
|
|
196
|
+
await ensureDockerNetwork(plan.networkName);
|
|
197
|
+
if (await dockerContainerExists(plan.containerName)) {
|
|
198
|
+
const state = await startDockerContainer(plan.containerName, {
|
|
199
|
+
stdio: commandStdio(options?.verbose),
|
|
200
|
+
});
|
|
201
|
+
options?.onSucceedTask?.(state === 'already-running'
|
|
202
|
+
? `The built-in ${plan.dbDialect} database is already running for "${runtime.envName}" at ${plan.dbHost}:${plan.dbPort}.`
|
|
203
|
+
: `The built-in ${plan.dbDialect} database is running for "${runtime.envName}" at ${plan.dbHost}:${plan.dbPort}.`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
await mkdir(plan.dataDir, { recursive: true });
|
|
207
|
+
await run('docker', plan.args, {
|
|
208
|
+
errorName: 'docker run',
|
|
209
|
+
stdio: commandStdio(options?.verbose),
|
|
210
|
+
});
|
|
211
|
+
options?.onSucceedTask?.(`The built-in ${plan.dbDialect} database is running for "${runtime.envName}" at ${plan.dbHost}:${plan.dbPort}.`);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
options?.onFailTask?.(`Failed to restore the built-in database for "${runtime.envName}".`);
|
|
215
|
+
throw new Error(formatBuiltinDbFailure(runtime.envName, error instanceof Error ? error.message : String(error)));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
export function buildSavedLocalDownloadArgv(runtime, options) {
|
|
219
|
+
const config = runtime.env.config ?? {};
|
|
220
|
+
const argv = ['-y', '--no-intro'];
|
|
221
|
+
if (options?.verbose) {
|
|
222
|
+
argv.push('--verbose');
|
|
223
|
+
}
|
|
224
|
+
argv.push('--source', runtime.source, '--replace', '--output-dir', runtime.projectRoot);
|
|
225
|
+
const version = String(config.downloadVersion ?? '').trim();
|
|
226
|
+
const gitUrl = String(config.gitUrl ?? '').trim();
|
|
227
|
+
const npmRegistry = String(config.npmRegistry ?? '').trim();
|
|
228
|
+
if (version) {
|
|
229
|
+
argv.push('--version', version);
|
|
230
|
+
}
|
|
231
|
+
if (gitUrl) {
|
|
232
|
+
argv.push('--git-url', gitUrl);
|
|
233
|
+
}
|
|
234
|
+
if (npmRegistry) {
|
|
235
|
+
argv.push('--npm-registry', npmRegistry);
|
|
236
|
+
}
|
|
237
|
+
if (config.devDependencies === true) {
|
|
238
|
+
argv.push('--dev-dependencies');
|
|
239
|
+
}
|
|
240
|
+
if (config.build === false) {
|
|
241
|
+
argv.push('--no-build');
|
|
242
|
+
}
|
|
243
|
+
if (config.buildDts === true) {
|
|
244
|
+
argv.push('--build-dts');
|
|
245
|
+
}
|
|
246
|
+
return argv;
|
|
247
|
+
}
|
|
248
|
+
export async function ensureSavedLocalSource(runtime, runCommand, options) {
|
|
249
|
+
if (await localProjectHasFiles(runtime.projectRoot)) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const sourceLabel = localSourceLabel(runtime.source);
|
|
253
|
+
options?.onStartTask?.(`Restoring the saved ${sourceLabel} for "${runtime.envName}"...`);
|
|
254
|
+
try {
|
|
255
|
+
await runCommand('source:download', buildSavedLocalDownloadArgv(runtime, {
|
|
256
|
+
verbose: options?.verbose,
|
|
257
|
+
}));
|
|
258
|
+
options?.onSucceedTask?.(`NocoBase files are ready for "${runtime.envName}".`);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
options?.onFailTask?.(`Failed to restore NocoBase files for "${runtime.envName}".`);
|
|
262
|
+
throw new Error(formatLocalSourceRestoreFailure(runtime.envName, runtime.source, error instanceof Error ? error.message : String(error)));
|
|
263
|
+
}
|
|
264
|
+
}
|
package/dist/lib/auth-store.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { promises as fs } from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
import { resolveCliHomeDir, resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRelativePath, } from './cli-home.js';
|
|
11
|
+
import { NB_CLI_ROOT_ENV, resolveCliHomeDir, resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRelativePath, } from './cli-home.js';
|
|
12
12
|
function normalizeStoredEnvKind(value) {
|
|
13
13
|
const kind = String(value ?? '').trim();
|
|
14
14
|
if (kind === 'remote') {
|
|
@@ -88,7 +88,10 @@ function hasConfiguredEnvs(config) {
|
|
|
88
88
|
}
|
|
89
89
|
function shouldFallbackToLegacyProjectScope(options = {}) {
|
|
90
90
|
const requestedScope = options.scope ?? resolveDefaultConfigScope();
|
|
91
|
-
|
|
91
|
+
if (requestedScope !== 'global') {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return !process.env[NB_CLI_ROOT_ENV];
|
|
92
95
|
}
|
|
93
96
|
async function loadExactAuthConfig(options = {}) {
|
|
94
97
|
try {
|
package/dist/lib/cli-home.js
CHANGED
|
@@ -11,16 +11,17 @@ import os from 'node:os';
|
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
export const CLI_HOME_DIRNAME = '.nocobase';
|
|
13
13
|
export const NB_CONFIG_SCOPE_ENV = 'NB_CONFIG_SCOPE';
|
|
14
|
-
export const
|
|
14
|
+
export const NB_CLI_ROOT_ENV = 'NB_CLI_ROOT';
|
|
15
15
|
export function resolveDefaultConfigScope() {
|
|
16
16
|
const raw = String(process.env[NB_CONFIG_SCOPE_ENV] ?? '').trim().toLowerCase();
|
|
17
17
|
return raw === 'project' ? 'project' : 'global';
|
|
18
18
|
}
|
|
19
|
+
function readConfiguredPath(name) {
|
|
20
|
+
const value = String(process.env[name] ?? '').trim();
|
|
21
|
+
return value || undefined;
|
|
22
|
+
}
|
|
19
23
|
function resolveGlobalCliHomeRoot() {
|
|
20
|
-
|
|
21
|
-
return process.env.NOCOBASE_CTL_HOME;
|
|
22
|
-
}
|
|
23
|
-
return os.homedir();
|
|
24
|
+
return readConfiguredPath(NB_CLI_ROOT_ENV) ?? os.homedir();
|
|
24
25
|
}
|
|
25
26
|
export function resolveCliHomeRoot(scope = resolveDefaultConfigScope()) {
|
|
26
27
|
const cwdRoot = process.cwd();
|
|
@@ -40,7 +41,7 @@ export function resolveCliHomeDir(scope = resolveDefaultConfigScope()) {
|
|
|
40
41
|
return path.join(resolveCliHomeRoot(scope), CLI_HOME_DIRNAME);
|
|
41
42
|
}
|
|
42
43
|
export function resolveEnvRoot(scope = resolveDefaultConfigScope()) {
|
|
43
|
-
const envRoot =
|
|
44
|
+
const envRoot = readConfiguredPath(NB_CLI_ROOT_ENV);
|
|
44
45
|
if (envRoot) {
|
|
45
46
|
return path.resolve(envRoot);
|
|
46
47
|
}
|
package/dist/lib/cli-locale.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
import { readFileSync } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
10
12
|
export const SUPPORTED_CLI_LOCALES = ['en-US', 'zh-CN'];
|
|
11
13
|
export const CLI_LOCALE_FLAG_OPTIONS = [...SUPPORTED_CLI_LOCALES];
|
|
12
14
|
export const CLI_LOCALE_FLAG_DESCRIPTION = 'Language for CLI prompts and the local setup UI.';
|
|
@@ -30,8 +32,20 @@ function loadLocaleMessages(locale) {
|
|
|
30
32
|
if (localeCache[locale]) {
|
|
31
33
|
return localeCache[locale];
|
|
32
34
|
}
|
|
35
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
const fallbackPath = path.resolve(moduleDir, '..', 'locale', `${locale}.json`);
|
|
33
37
|
const fileUrl = new URL(`../locale/${locale}.json`, import.meta.url);
|
|
34
|
-
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(readFileSync(fileUrl, 'utf8'));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
|
|
44
|
+
if (code !== 'ENOENT') {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
parsed = JSON.parse(readFileSync(fallbackPath, 'utf8'));
|
|
48
|
+
}
|
|
35
49
|
localeCache[locale] = parsed;
|
|
36
50
|
return parsed;
|
|
37
51
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
const STRING_ENV_CONFIG_KEYS = [
|
|
10
|
+
'source',
|
|
11
|
+
'downloadVersion',
|
|
12
|
+
'dockerRegistry',
|
|
13
|
+
'dockerPlatform',
|
|
14
|
+
'gitUrl',
|
|
15
|
+
'npmRegistry',
|
|
16
|
+
'appRootPath',
|
|
17
|
+
'storagePath',
|
|
18
|
+
'appPort',
|
|
19
|
+
'appKey',
|
|
20
|
+
'timezone',
|
|
21
|
+
'dbDialect',
|
|
22
|
+
'builtinDbImage',
|
|
23
|
+
'dbHost',
|
|
24
|
+
'dbPort',
|
|
25
|
+
'dbDatabase',
|
|
26
|
+
'dbUser',
|
|
27
|
+
'dbPassword',
|
|
28
|
+
'rootUsername',
|
|
29
|
+
'rootEmail',
|
|
30
|
+
'rootPassword',
|
|
31
|
+
'rootNickname',
|
|
32
|
+
];
|
|
33
|
+
const BOOLEAN_ENV_CONFIG_KEYS = [
|
|
34
|
+
'builtinDb',
|
|
35
|
+
'devDependencies',
|
|
36
|
+
'build',
|
|
37
|
+
'buildDts',
|
|
38
|
+
];
|
|
39
|
+
function trimConfigValue(value) {
|
|
40
|
+
const text = String(value ?? '').trim();
|
|
41
|
+
return text || undefined;
|
|
42
|
+
}
|
|
43
|
+
function resolveEnvKind(input) {
|
|
44
|
+
const source = trimConfigValue(input.source);
|
|
45
|
+
const appRootPath = trimConfigValue(input.appRootPath);
|
|
46
|
+
if (source === 'docker') {
|
|
47
|
+
return 'docker';
|
|
48
|
+
}
|
|
49
|
+
if (source === 'npm' || source === 'git' || source === 'local' || appRootPath) {
|
|
50
|
+
return 'local';
|
|
51
|
+
}
|
|
52
|
+
return 'http';
|
|
53
|
+
}
|
|
54
|
+
export function buildStoredEnvConfig(input) {
|
|
55
|
+
const envConfig = {
|
|
56
|
+
kind: resolveEnvKind(input),
|
|
57
|
+
apiBaseUrl: trimConfigValue(input.apiBaseUrl) ?? '',
|
|
58
|
+
};
|
|
59
|
+
for (const key of STRING_ENV_CONFIG_KEYS) {
|
|
60
|
+
const value = trimConfigValue(input[key]);
|
|
61
|
+
if (value) {
|
|
62
|
+
envConfig[key] = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const key of BOOLEAN_ENV_CONFIG_KEYS) {
|
|
66
|
+
const value = input[key];
|
|
67
|
+
if (typeof value === 'boolean') {
|
|
68
|
+
envConfig[key] = value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (input.builtinDb === false) {
|
|
72
|
+
envConfig.builtinDbImage = undefined;
|
|
73
|
+
}
|
|
74
|
+
const authType = trimConfigValue(input.authType);
|
|
75
|
+
const accessToken = trimConfigValue(input.accessToken);
|
|
76
|
+
if (authType === 'token' && accessToken) {
|
|
77
|
+
envConfig.accessToken = accessToken;
|
|
78
|
+
}
|
|
79
|
+
return envConfig;
|
|
80
|
+
}
|
|
@@ -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 })
|
|
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 })
|
|
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 })
|
|
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
|
-
|
|
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
|
});
|
|
@@ -180,12 +180,6 @@ export async function inspectSkillsStatus(options = {}) {
|
|
|
180
180
|
registryError,
|
|
181
181
|
};
|
|
182
182
|
}
|
|
183
|
-
function formatSkillsNotInstalledMessage() {
|
|
184
|
-
return [
|
|
185
|
-
'NocoBase AI coding skills are not installed globally.',
|
|
186
|
-
'Run `nb skills install` first.',
|
|
187
|
-
].join('\n');
|
|
188
|
-
}
|
|
189
183
|
async function persistManagedSkillsState(globalRoot, options = {}) {
|
|
190
184
|
const installedSkills = await listGlobalSkills({
|
|
191
185
|
globalRoot,
|
|
@@ -250,7 +244,11 @@ export async function updateNocoBaseSkills(options = {}) {
|
|
|
250
244
|
commandOutputFn: options.commandOutputFn,
|
|
251
245
|
});
|
|
252
246
|
if (!status.installed) {
|
|
253
|
-
|
|
247
|
+
return {
|
|
248
|
+
action: 'noop',
|
|
249
|
+
reason: 'not-installed',
|
|
250
|
+
status,
|
|
251
|
+
};
|
|
254
252
|
}
|
|
255
253
|
if (status.managedByNb
|
|
256
254
|
&& status.latestVersion
|
|
@@ -258,6 +256,7 @@ export async function updateNocoBaseSkills(options = {}) {
|
|
|
258
256
|
&& compareVersions(status.latestVersion, status.installedVersion) <= 0) {
|
|
259
257
|
return {
|
|
260
258
|
action: 'noop',
|
|
259
|
+
reason: 'up-to-date',
|
|
261
260
|
status,
|
|
262
261
|
};
|
|
263
262
|
}
|
|
@@ -267,3 +266,31 @@ export async function updateNocoBaseSkills(options = {}) {
|
|
|
267
266
|
status: await persistManagedSkillsState(globalRoot, options),
|
|
268
267
|
};
|
|
269
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
|
+
}),
|
|
295
|
+
};
|
|
296
|
+
}
|