@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,595 @@
|
|
|
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 { Command, Flags } from '@oclif/core';
|
|
10
|
+
import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, startDockerContainer, stopDockerContainer, } from '../../lib/app-runtime.js';
|
|
11
|
+
import { resolveConfiguredEnvPath } from '../../lib/cli-home.js';
|
|
12
|
+
import { commandOutput, commandSucceeds, run } from '../../lib/run-npm.js';
|
|
13
|
+
import { failTask, printInfo, startTask, stopTask, succeedTask, updateTask } from '../../lib/ui.js';
|
|
14
|
+
const DEFAULT_DOCKER_REGISTRY = 'nocobase/nocobase';
|
|
15
|
+
const DEFAULT_DOWNLOAD_VERSION = 'alpha';
|
|
16
|
+
const DOCKER_APP_STORAGE_DESTINATION = '/app/nocobase/storage';
|
|
17
|
+
const APP_HEALTH_CHECK_INTERVAL_MS = 2_000;
|
|
18
|
+
const APP_HEALTH_CHECK_TIMEOUT_MS = 600_000;
|
|
19
|
+
const APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS = 5_000;
|
|
20
|
+
function trimValue(value) {
|
|
21
|
+
return String(value ?? '').trim();
|
|
22
|
+
}
|
|
23
|
+
function formatAppUrl(port) {
|
|
24
|
+
const value = trimValue(port);
|
|
25
|
+
return value ? `http://127.0.0.1:${value}` : undefined;
|
|
26
|
+
}
|
|
27
|
+
function formatDisplayUrl(apiBaseUrl, appPort) {
|
|
28
|
+
const appUrl = formatAppUrl(appPort);
|
|
29
|
+
if (appUrl) {
|
|
30
|
+
return appUrl;
|
|
31
|
+
}
|
|
32
|
+
const value = trimValue(apiBaseUrl);
|
|
33
|
+
if (!value) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return value.replace(/\/api\/?$/, '');
|
|
37
|
+
}
|
|
38
|
+
function resolveApiBaseUrl(runtime) {
|
|
39
|
+
const baseUrl = trimValue(runtime.env.baseUrl);
|
|
40
|
+
if (baseUrl) {
|
|
41
|
+
return baseUrl.replace(/\/+$/, '');
|
|
42
|
+
}
|
|
43
|
+
const appPort = runtime.env.appPort === undefined || runtime.env.appPort === null
|
|
44
|
+
? ''
|
|
45
|
+
: trimValue(runtime.env.appPort);
|
|
46
|
+
return appPort ? `http://127.0.0.1:${appPort}/api` : undefined;
|
|
47
|
+
}
|
|
48
|
+
function buildHealthCheckUrl(apiBaseUrl) {
|
|
49
|
+
return `${apiBaseUrl.replace(/\/+$/, '')}/__health_check`;
|
|
50
|
+
}
|
|
51
|
+
function dockerRefLabel(source) {
|
|
52
|
+
if (source === 'git') {
|
|
53
|
+
return 'Git checkout';
|
|
54
|
+
}
|
|
55
|
+
if (source === 'npm') {
|
|
56
|
+
return 'npm app';
|
|
57
|
+
}
|
|
58
|
+
return 'local app';
|
|
59
|
+
}
|
|
60
|
+
function formatLocalDownloadFailure(envName, source, message) {
|
|
61
|
+
const sourceLabel = source === 'git' ? 'the saved Git checkout' : 'the saved npm app';
|
|
62
|
+
return [
|
|
63
|
+
`Couldn't refresh NocoBase for "${envName}".`,
|
|
64
|
+
`The CLI was not able to update ${sourceLabel} before restarting it.`,
|
|
65
|
+
'Check the saved source settings for this env, then try again.',
|
|
66
|
+
`Details: ${message}`,
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
69
|
+
function formatLocalStartFailure(envName, source, port, message) {
|
|
70
|
+
const sourceLabel = dockerRefLabel(source);
|
|
71
|
+
const portHint = trimValue(port) ? ` Expected app port: ${trimValue(port)}.` : '';
|
|
72
|
+
const details = trimValue(message) ? ` Details: ${trimValue(message)}` : '';
|
|
73
|
+
return [
|
|
74
|
+
`Couldn't finish the upgrade for "${envName}".`,
|
|
75
|
+
`The CLI updated ${sourceLabel}, but it could not start the upgraded app successfully.`,
|
|
76
|
+
`Check the local dependencies, database connection, and saved env settings, then try again.${portHint}${details}`,
|
|
77
|
+
].join('\n');
|
|
78
|
+
}
|
|
79
|
+
function formatDockerDownloadFailure(envName, message) {
|
|
80
|
+
return [
|
|
81
|
+
`Couldn't refresh the Docker image for "${envName}".`,
|
|
82
|
+
'The CLI was not able to pull the latest image for this env.',
|
|
83
|
+
'Check the saved Docker source settings and your Docker network access, then try again.',
|
|
84
|
+
`Details: ${message}`,
|
|
85
|
+
].join('\n');
|
|
86
|
+
}
|
|
87
|
+
function formatDockerStartFailure(envName, message) {
|
|
88
|
+
return [
|
|
89
|
+
`Couldn't finish the upgrade for "${envName}".`,
|
|
90
|
+
'The CLI was not able to start the upgraded Docker app successfully.',
|
|
91
|
+
'Check that the saved Docker image, container settings, and database connection are still valid, then try again.',
|
|
92
|
+
`Details: ${message}`,
|
|
93
|
+
].join('\n');
|
|
94
|
+
}
|
|
95
|
+
function parseDockerImageRef(imageRef) {
|
|
96
|
+
const cleaned = trimValue(imageRef).replace(/@.+$/, '');
|
|
97
|
+
if (!cleaned) {
|
|
98
|
+
return {
|
|
99
|
+
dockerRegistry: DEFAULT_DOCKER_REGISTRY,
|
|
100
|
+
version: DEFAULT_DOWNLOAD_VERSION,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const lastSlash = cleaned.lastIndexOf('/');
|
|
104
|
+
const lastColon = cleaned.lastIndexOf(':');
|
|
105
|
+
if (lastColon > lastSlash) {
|
|
106
|
+
return {
|
|
107
|
+
dockerRegistry: cleaned.slice(0, lastColon),
|
|
108
|
+
version: cleaned.slice(lastColon + 1) || DEFAULT_DOWNLOAD_VERSION,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
dockerRegistry: cleaned,
|
|
113
|
+
version: 'latest',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function normalizeDockerPlatform(value) {
|
|
117
|
+
const text = String(value ?? '').trim();
|
|
118
|
+
if (!text || text === 'auto') {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
if (text === 'linux/amd64' || text === 'linux/arm64') {
|
|
122
|
+
return text;
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
function readEnvValue(env, key) {
|
|
127
|
+
if (key === 'appRootPath' || key === 'storagePath') {
|
|
128
|
+
return trimValue(resolveConfiguredEnvPath(env.config[key]));
|
|
129
|
+
}
|
|
130
|
+
return trimValue(env.config[key]);
|
|
131
|
+
}
|
|
132
|
+
async function sleep(ms) {
|
|
133
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
134
|
+
}
|
|
135
|
+
async function requestAppHealthCheck(params) {
|
|
136
|
+
const controller = new AbortController();
|
|
137
|
+
const timeout = setTimeout(() => {
|
|
138
|
+
controller.abort();
|
|
139
|
+
}, params.requestTimeoutMs ?? APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS);
|
|
140
|
+
try {
|
|
141
|
+
const response = await (params.fetchImpl ?? fetch)(params.healthCheckUrl, {
|
|
142
|
+
method: 'GET',
|
|
143
|
+
signal: controller.signal,
|
|
144
|
+
});
|
|
145
|
+
const text = await response.text().catch(() => '');
|
|
146
|
+
const body = text.replace(/\s+/g, ' ').trim() || 'No response yet';
|
|
147
|
+
return {
|
|
148
|
+
ok: response.ok && text.trim().toLowerCase() === 'ok',
|
|
149
|
+
message: `HTTP ${response.status}: ${body}`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
message: `No response within ${Math.ceil((params.requestTimeoutMs ?? APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS) / 1000)}s`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
message: error instanceof Error ? error.message : String(error),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
clearTimeout(timeout);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function waitForAppHealthCheck(params) {
|
|
169
|
+
if (!params.apiBaseUrl) {
|
|
170
|
+
printInfo(`Skipping health check for "${params.envName}" because no local API URL is saved for this env.`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const healthCheckUrl = buildHealthCheckUrl(params.apiBaseUrl);
|
|
174
|
+
const startedAt = Date.now();
|
|
175
|
+
let lastMessage = 'No response yet';
|
|
176
|
+
let spinnerActive = true;
|
|
177
|
+
startTask(`Waiting for NocoBase to become ready for "${params.envName}"...`);
|
|
178
|
+
try {
|
|
179
|
+
while (Date.now() - startedAt < APP_HEALTH_CHECK_TIMEOUT_MS) {
|
|
180
|
+
const result = await requestAppHealthCheck({
|
|
181
|
+
healthCheckUrl,
|
|
182
|
+
fetchImpl: params.fetchImpl,
|
|
183
|
+
});
|
|
184
|
+
if (result.ok) {
|
|
185
|
+
stopTask();
|
|
186
|
+
spinnerActive = false;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
lastMessage = result.message;
|
|
190
|
+
const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
|
|
191
|
+
updateTask(`Waiting for NocoBase to become ready for "${params.envName}"... (${elapsedSeconds}s elapsed, last status: ${lastMessage})`);
|
|
192
|
+
await sleep(APP_HEALTH_CHECK_INTERVAL_MS);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
if (spinnerActive) {
|
|
197
|
+
stopTask();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const logHint = params.containerName
|
|
201
|
+
? ` You can inspect startup logs with: docker logs ${params.containerName}`
|
|
202
|
+
: '';
|
|
203
|
+
throw new Error(`The upgraded app for "${params.envName}" did not become ready in time. Expected \`${healthCheckUrl}\` to respond with \`ok\`, but the last status was: ${lastMessage}.${logHint}`);
|
|
204
|
+
}
|
|
205
|
+
async function dockerContainerExists(containerName) {
|
|
206
|
+
return await commandSucceeds('docker', ['container', 'inspect', containerName]);
|
|
207
|
+
}
|
|
208
|
+
async function ensureDockerNetwork(name) {
|
|
209
|
+
const exists = await commandSucceeds('docker', ['network', 'inspect', name]);
|
|
210
|
+
if (exists) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
await run('docker', ['network', 'create', name], {
|
|
214
|
+
errorName: 'docker network create',
|
|
215
|
+
stdio: 'ignore',
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
async function inspectDockerContainerEnv(name) {
|
|
219
|
+
const output = await commandOutput('docker', [
|
|
220
|
+
'inspect',
|
|
221
|
+
'--format',
|
|
222
|
+
'{{range .Config.Env}}{{println .}}{{end}}',
|
|
223
|
+
name,
|
|
224
|
+
], {
|
|
225
|
+
errorName: 'docker inspect',
|
|
226
|
+
});
|
|
227
|
+
const env = {};
|
|
228
|
+
for (const line of output.split(/\r?\n/)) {
|
|
229
|
+
const index = line.indexOf('=');
|
|
230
|
+
if (index <= 0) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
env[line.slice(0, index)] = line.slice(index + 1);
|
|
234
|
+
}
|
|
235
|
+
return env;
|
|
236
|
+
}
|
|
237
|
+
async function inspectDockerContainerImage(name) {
|
|
238
|
+
return await commandOutput('docker', [
|
|
239
|
+
'inspect',
|
|
240
|
+
'--format',
|
|
241
|
+
'{{.Config.Image}}',
|
|
242
|
+
name,
|
|
243
|
+
], {
|
|
244
|
+
errorName: 'docker inspect',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
async function inspectDockerStoragePath(name) {
|
|
248
|
+
return await commandOutput('docker', [
|
|
249
|
+
'inspect',
|
|
250
|
+
'--format',
|
|
251
|
+
`{{range .Mounts}}{{if eq .Destination "${DOCKER_APP_STORAGE_DESTINATION}"}}{{println .Source}}{{end}}{{end}}`,
|
|
252
|
+
name,
|
|
253
|
+
], {
|
|
254
|
+
errorName: 'docker inspect',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
export default class AppUpgrade extends Command {
|
|
258
|
+
static hidden = false;
|
|
259
|
+
static description = 'Upgrade the selected NocoBase app. Local npm/git installs refresh the saved source and restart with quickstart; Docker installs refresh the saved image and recreate the app container.';
|
|
260
|
+
static examples = [
|
|
261
|
+
'<%= config.bin %> <%= command.id %>',
|
|
262
|
+
'<%= config.bin %> <%= command.id %> --env local',
|
|
263
|
+
'<%= config.bin %> <%= command.id %> --env local -s',
|
|
264
|
+
'<%= config.bin %> <%= command.id %> --env local --verbose',
|
|
265
|
+
'<%= config.bin %> <%= command.id %> --env local-docker -s',
|
|
266
|
+
];
|
|
267
|
+
static flags = {
|
|
268
|
+
env: Flags.string({
|
|
269
|
+
char: 'e',
|
|
270
|
+
description: 'CLI env name to upgrade. Defaults to the current env when omitted',
|
|
271
|
+
}),
|
|
272
|
+
'skip-code-update': Flags.boolean({
|
|
273
|
+
char: 's',
|
|
274
|
+
description: 'Restart with the saved local code or Docker image without downloading updates first',
|
|
275
|
+
required: false,
|
|
276
|
+
}),
|
|
277
|
+
verbose: Flags.boolean({
|
|
278
|
+
description: 'Show raw output from the underlying local or Docker commands',
|
|
279
|
+
default: false,
|
|
280
|
+
}),
|
|
281
|
+
};
|
|
282
|
+
static buildLocalDownloadArgv(runtime) {
|
|
283
|
+
const argv = ['-y', '--no-intro', '--source', runtime.source, '--replace'];
|
|
284
|
+
const version = readEnvValue(runtime.env, 'downloadVersion');
|
|
285
|
+
const outputDir = readEnvValue(runtime.env, 'appRootPath');
|
|
286
|
+
const gitUrl = readEnvValue(runtime.env, 'gitUrl');
|
|
287
|
+
const npmRegistry = readEnvValue(runtime.env, 'npmRegistry');
|
|
288
|
+
if (version) {
|
|
289
|
+
argv.push('--version', version);
|
|
290
|
+
}
|
|
291
|
+
if (outputDir) {
|
|
292
|
+
argv.push('--output-dir', outputDir);
|
|
293
|
+
}
|
|
294
|
+
if (gitUrl) {
|
|
295
|
+
argv.push('--git-url', gitUrl);
|
|
296
|
+
}
|
|
297
|
+
if (npmRegistry) {
|
|
298
|
+
argv.push('--npm-registry', npmRegistry);
|
|
299
|
+
}
|
|
300
|
+
if (runtime.env.config.devDependencies === true) {
|
|
301
|
+
argv.push('--dev-dependencies');
|
|
302
|
+
}
|
|
303
|
+
if (runtime.env.config.build === false) {
|
|
304
|
+
argv.push('--no-build');
|
|
305
|
+
}
|
|
306
|
+
if (runtime.env.config.buildDts === true) {
|
|
307
|
+
argv.push('--build-dts');
|
|
308
|
+
}
|
|
309
|
+
return argv;
|
|
310
|
+
}
|
|
311
|
+
static buildLocalStartArgv(runtime) {
|
|
312
|
+
const argv = ['start', '--quickstart'];
|
|
313
|
+
const appPort = runtime.env.appPort === undefined || runtime.env.appPort === null
|
|
314
|
+
? ''
|
|
315
|
+
: trimValue(runtime.env.appPort);
|
|
316
|
+
if (appPort) {
|
|
317
|
+
argv.push('--port', appPort);
|
|
318
|
+
}
|
|
319
|
+
argv.push('--daemon');
|
|
320
|
+
return argv;
|
|
321
|
+
}
|
|
322
|
+
static async buildDockerUpgradePlan(runtime) {
|
|
323
|
+
const containerExists = await dockerContainerExists(runtime.containerName);
|
|
324
|
+
let inspectedEnv;
|
|
325
|
+
const readContainerEnv = async () => {
|
|
326
|
+
if (!containerExists) {
|
|
327
|
+
return {};
|
|
328
|
+
}
|
|
329
|
+
if (!inspectedEnv) {
|
|
330
|
+
inspectedEnv = await inspectDockerContainerEnv(runtime.containerName);
|
|
331
|
+
}
|
|
332
|
+
return inspectedEnv;
|
|
333
|
+
};
|
|
334
|
+
let dockerRegistry = readEnvValue(runtime.env, 'dockerRegistry');
|
|
335
|
+
let version = readEnvValue(runtime.env, 'downloadVersion');
|
|
336
|
+
if ((!dockerRegistry || !version) && containerExists) {
|
|
337
|
+
const imageRef = await inspectDockerContainerImage(runtime.containerName);
|
|
338
|
+
const parsed = parseDockerImageRef(imageRef);
|
|
339
|
+
dockerRegistry ||= parsed.dockerRegistry;
|
|
340
|
+
version ||= parsed.version;
|
|
341
|
+
}
|
|
342
|
+
const envVars = await readContainerEnv();
|
|
343
|
+
const appPort = runtime.env.appPort === undefined || runtime.env.appPort === null
|
|
344
|
+
? ''
|
|
345
|
+
: trimValue(runtime.env.appPort);
|
|
346
|
+
let storagePath = readEnvValue(runtime.env, 'storagePath');
|
|
347
|
+
if (!storagePath && containerExists) {
|
|
348
|
+
storagePath = trimValue(await inspectDockerStoragePath(runtime.containerName));
|
|
349
|
+
}
|
|
350
|
+
const appKey = readEnvValue(runtime.env, 'appKey') || trimValue(envVars.APP_KEY);
|
|
351
|
+
const timeZone = readEnvValue(runtime.env, 'timezone') || trimValue(envVars.TZ);
|
|
352
|
+
const dbDialect = readEnvValue(runtime.env, 'dbDialect') || trimValue(envVars.DB_DIALECT);
|
|
353
|
+
const dbHost = readEnvValue(runtime.env, 'dbHost') || trimValue(envVars.DB_HOST);
|
|
354
|
+
const dbPort = readEnvValue(runtime.env, 'dbPort') || trimValue(envVars.DB_PORT);
|
|
355
|
+
const dbDatabase = readEnvValue(runtime.env, 'dbDatabase') || trimValue(envVars.DB_DATABASE);
|
|
356
|
+
const dbUser = readEnvValue(runtime.env, 'dbUser') || trimValue(envVars.DB_USER);
|
|
357
|
+
const dbPassword = readEnvValue(runtime.env, 'dbPassword') || trimValue(envVars.DB_PASSWORD);
|
|
358
|
+
const missing = [];
|
|
359
|
+
if (!storagePath) {
|
|
360
|
+
missing.push('storagePath');
|
|
361
|
+
}
|
|
362
|
+
if (!appKey) {
|
|
363
|
+
missing.push('appKey');
|
|
364
|
+
}
|
|
365
|
+
if (!timeZone) {
|
|
366
|
+
missing.push('timezone');
|
|
367
|
+
}
|
|
368
|
+
if (!dbDialect) {
|
|
369
|
+
missing.push('dbDialect');
|
|
370
|
+
}
|
|
371
|
+
if (!dbHost) {
|
|
372
|
+
missing.push('dbHost');
|
|
373
|
+
}
|
|
374
|
+
if (!dbPort) {
|
|
375
|
+
missing.push('dbPort');
|
|
376
|
+
}
|
|
377
|
+
if (!dbDatabase) {
|
|
378
|
+
missing.push('dbDatabase');
|
|
379
|
+
}
|
|
380
|
+
if (!dbUser) {
|
|
381
|
+
missing.push('dbUser');
|
|
382
|
+
}
|
|
383
|
+
if (!dbPassword) {
|
|
384
|
+
missing.push('dbPassword');
|
|
385
|
+
}
|
|
386
|
+
if (missing.length > 0) {
|
|
387
|
+
throw new Error(`The saved Docker settings for "${runtime.envName}" are incomplete. Missing: ${missing.join(', ')}. Re-run \`nb init\` or \`nb env add\` to refresh this env config.`);
|
|
388
|
+
}
|
|
389
|
+
const resolvedRegistry = dockerRegistry || DEFAULT_DOCKER_REGISTRY;
|
|
390
|
+
const resolvedVersion = version || DEFAULT_DOWNLOAD_VERSION;
|
|
391
|
+
const imageRef = `${resolvedRegistry}:${resolvedVersion}`;
|
|
392
|
+
const args = [
|
|
393
|
+
'run',
|
|
394
|
+
'-d',
|
|
395
|
+
'--name',
|
|
396
|
+
runtime.containerName,
|
|
397
|
+
'--restart',
|
|
398
|
+
'always',
|
|
399
|
+
'--network',
|
|
400
|
+
runtime.workspaceName,
|
|
401
|
+
];
|
|
402
|
+
if (appPort) {
|
|
403
|
+
args.push('-p', `${appPort}:80`);
|
|
404
|
+
}
|
|
405
|
+
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);
|
|
406
|
+
return {
|
|
407
|
+
containerName: runtime.containerName,
|
|
408
|
+
networkName: runtime.workspaceName,
|
|
409
|
+
imageRef,
|
|
410
|
+
appPort: appPort || undefined,
|
|
411
|
+
storagePath,
|
|
412
|
+
appKey,
|
|
413
|
+
timeZone,
|
|
414
|
+
dbDialect,
|
|
415
|
+
dbHost,
|
|
416
|
+
dbPort,
|
|
417
|
+
dbDatabase,
|
|
418
|
+
dbUser,
|
|
419
|
+
dbPassword,
|
|
420
|
+
args,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
static async upgradeLocal(runCommand, runtime, flags, commandStdio) {
|
|
424
|
+
const displayUrl = formatDisplayUrl(resolveApiBaseUrl(runtime), trimValue(runtime.env.appPort));
|
|
425
|
+
startTask(`Stopping NocoBase for "${runtime.envName}" before upgrade...`);
|
|
426
|
+
try {
|
|
427
|
+
await runLocalNocoBaseCommand(runtime, ['pm2', 'kill'], {
|
|
428
|
+
stdio: commandStdio,
|
|
429
|
+
});
|
|
430
|
+
succeedTask(`Stopped the current NocoBase process for "${runtime.envName}".`);
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
stopTask();
|
|
434
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
435
|
+
printInfo(`No running background process was stopped for "${runtime.envName}". Continuing with the upgrade. (${message})`);
|
|
436
|
+
}
|
|
437
|
+
if (!flags['skip-code-update'] && (runtime.source === 'npm' || runtime.source === 'git')) {
|
|
438
|
+
startTask(`Refreshing NocoBase files for "${runtime.envName}" from the saved ${runtime.source} source...`);
|
|
439
|
+
try {
|
|
440
|
+
await runCommand('source:download', AppUpgrade.buildLocalDownloadArgv(runtime));
|
|
441
|
+
succeedTask(`NocoBase files are up to date for "${runtime.envName}".`);
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
445
|
+
failTask(`Failed to refresh NocoBase files for "${runtime.envName}".`);
|
|
446
|
+
throw new Error(formatLocalDownloadFailure(runtime.envName, runtime.source, message));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
else if (flags['skip-code-update']) {
|
|
450
|
+
printInfo(`Skipping code download for "${runtime.envName}" (--skip-code-update).`);
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
printInfo(`Skipping code download for "${runtime.envName}" because this env is managed from an existing local app path.`);
|
|
454
|
+
}
|
|
455
|
+
startTask(`Starting upgraded NocoBase for "${runtime.envName}"...`);
|
|
456
|
+
try {
|
|
457
|
+
await runLocalNocoBaseCommand(runtime, AppUpgrade.buildLocalStartArgv(runtime), {
|
|
458
|
+
stdio: commandStdio,
|
|
459
|
+
});
|
|
460
|
+
succeedTask(`Upgraded NocoBase is starting for "${runtime.envName}".`);
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
464
|
+
failTask(`Failed to start upgraded NocoBase for "${runtime.envName}".`);
|
|
465
|
+
throw new Error(formatLocalStartFailure(runtime.envName, runtime.source, trimValue(runtime.env.appPort), message));
|
|
466
|
+
}
|
|
467
|
+
await waitForAppHealthCheck({
|
|
468
|
+
envName: runtime.envName,
|
|
469
|
+
apiBaseUrl: resolveApiBaseUrl(runtime),
|
|
470
|
+
});
|
|
471
|
+
succeedTask(`NocoBase has been upgraded for "${runtime.envName}"${displayUrl ? ` at ${displayUrl}` : ''}.`);
|
|
472
|
+
}
|
|
473
|
+
static async upgradeDocker(runCommand, runtime, flags, commandStdio) {
|
|
474
|
+
const plan = await AppUpgrade.buildDockerUpgradePlan(runtime);
|
|
475
|
+
const apiBaseUrl = resolveApiBaseUrl(runtime);
|
|
476
|
+
const displayUrl = formatDisplayUrl(apiBaseUrl, plan.appPort);
|
|
477
|
+
const containerExists = await dockerContainerExists(runtime.containerName);
|
|
478
|
+
if (!flags['skip-code-update']) {
|
|
479
|
+
const argv = ['-y', '--no-intro', '--source', 'docker', '--replace', '--docker-registry', parseDockerImageRef(plan.imageRef).dockerRegistry, '--version', parseDockerImageRef(plan.imageRef).version];
|
|
480
|
+
const dockerPlatform = normalizeDockerPlatform(runtime.env.config.dockerPlatform);
|
|
481
|
+
if (dockerPlatform) {
|
|
482
|
+
argv.push('--docker-platform', dockerPlatform);
|
|
483
|
+
}
|
|
484
|
+
startTask(`Refreshing the Docker image for "${runtime.envName}"...`);
|
|
485
|
+
try {
|
|
486
|
+
await runCommand('source:download', argv);
|
|
487
|
+
succeedTask(`Docker image is ready for "${runtime.envName}".`);
|
|
488
|
+
}
|
|
489
|
+
catch (error) {
|
|
490
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
491
|
+
failTask(`Failed to refresh the Docker image for "${runtime.envName}".`);
|
|
492
|
+
throw new Error(formatDockerDownloadFailure(runtime.envName, message));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
printInfo(`Skipping image download for "${runtime.envName}" (--skip-code-update).`);
|
|
497
|
+
}
|
|
498
|
+
if (containerExists) {
|
|
499
|
+
startTask(`Stopping the current Docker app for "${runtime.envName}"...`);
|
|
500
|
+
try {
|
|
501
|
+
const state = await stopDockerContainer(runtime.containerName, {
|
|
502
|
+
stdio: commandStdio,
|
|
503
|
+
});
|
|
504
|
+
succeedTask(state === 'already-stopped'
|
|
505
|
+
? `The current Docker app was already stopped for "${runtime.envName}".`
|
|
506
|
+
: `Stopped the current Docker app for "${runtime.envName}".`);
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
stopTask();
|
|
510
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
511
|
+
printInfo(`Could not stop the existing Docker container for "${runtime.envName}" cleanly. Continuing with container recreation. (${message})`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (flags['skip-code-update'] && containerExists) {
|
|
515
|
+
startTask(`Starting NocoBase for "${runtime.envName}" with the saved Docker image...`);
|
|
516
|
+
try {
|
|
517
|
+
const state = await startDockerContainer(runtime.containerName, {
|
|
518
|
+
stdio: commandStdio,
|
|
519
|
+
});
|
|
520
|
+
succeedTask(state === 'already-running'
|
|
521
|
+
? `NocoBase is already running for "${runtime.envName}".`
|
|
522
|
+
: `NocoBase is starting for "${runtime.envName}".`);
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
526
|
+
failTask(`Failed to start the Docker app for "${runtime.envName}".`);
|
|
527
|
+
throw new Error(formatDockerStartFailure(runtime.envName, message));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
startTask(`Recreating the Docker app container for "${runtime.envName}"...`);
|
|
532
|
+
try {
|
|
533
|
+
if (containerExists) {
|
|
534
|
+
await run('docker', ['rm', '-f', runtime.containerName], {
|
|
535
|
+
errorName: 'docker rm',
|
|
536
|
+
stdio: commandStdio,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
await ensureDockerNetwork(plan.networkName);
|
|
540
|
+
await run('docker', plan.args, {
|
|
541
|
+
errorName: 'docker run',
|
|
542
|
+
stdio: commandStdio,
|
|
543
|
+
});
|
|
544
|
+
succeedTask(`Docker app container is ready for "${runtime.envName}".`);
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
548
|
+
failTask(`Failed to recreate the Docker app for "${runtime.envName}".`);
|
|
549
|
+
throw new Error(formatDockerStartFailure(runtime.envName, message));
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
await waitForAppHealthCheck({
|
|
553
|
+
envName: runtime.envName,
|
|
554
|
+
apiBaseUrl,
|
|
555
|
+
containerName: runtime.containerName,
|
|
556
|
+
});
|
|
557
|
+
succeedTask(`NocoBase has been upgraded for "${runtime.envName}"${displayUrl ? ` at ${displayUrl}` : ''}.`);
|
|
558
|
+
}
|
|
559
|
+
async run() {
|
|
560
|
+
const { flags } = await this.parse(AppUpgrade);
|
|
561
|
+
const parsed = flags;
|
|
562
|
+
const requestedEnv = parsed.env?.trim() || undefined;
|
|
563
|
+
const commandStdio = parsed.verbose ? 'inherit' : 'ignore';
|
|
564
|
+
const runtime = await resolveManagedAppRuntime(requestedEnv);
|
|
565
|
+
if (!runtime) {
|
|
566
|
+
this.error(formatMissingManagedAppEnvMessage(requestedEnv));
|
|
567
|
+
}
|
|
568
|
+
if (runtime.kind === 'http') {
|
|
569
|
+
this.error([
|
|
570
|
+
`Can't upgrade "${runtime.envName}" from this machine.`,
|
|
571
|
+
'This env only has an API connection, so there is no saved local app or Docker runtime to upgrade here.',
|
|
572
|
+
'If you want a local NocoBase AI environment that the CLI can upgrade, run `nb init` first.',
|
|
573
|
+
].join('\n'));
|
|
574
|
+
}
|
|
575
|
+
if (runtime.kind === 'ssh') {
|
|
576
|
+
this.error([
|
|
577
|
+
`Can't upgrade "${runtime.envName}" yet.`,
|
|
578
|
+
'SSH env support is reserved but not implemented yet.',
|
|
579
|
+
'Use a local or Docker env if you need CLI-managed upgrades right now.',
|
|
580
|
+
].join('\n'));
|
|
581
|
+
}
|
|
582
|
+
try {
|
|
583
|
+
const runCommand = this.config.runCommand.bind(this.config);
|
|
584
|
+
if (runtime.kind === 'docker') {
|
|
585
|
+
await AppUpgrade.upgradeDocker(runCommand, runtime, parsed, commandStdio);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
await AppUpgrade.upgradeLocal(runCommand, runtime, parsed, commandStdio);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
this.error(error instanceof Error ? error.message : String(error));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
package/dist/commands/build.js
CHANGED
|
@@ -6,52 +6,7 @@
|
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export default class Build extends Command {
|
|
13
|
-
static args = {
|
|
14
|
-
/** Matches `nb build @nocobase/acl @nocobase/actions` — zero or more package names. */
|
|
15
|
-
packages: Args.string({
|
|
16
|
-
description: 'package names to build',
|
|
17
|
-
multiple: true,
|
|
18
|
-
required: false,
|
|
19
|
-
}),
|
|
20
|
-
};
|
|
21
|
-
static description = 'Run the legacy NocoBase build (forwards to `npm run build` in the repo root)';
|
|
22
|
-
static examples = [
|
|
23
|
-
'<%= config.bin %> <%= command.id %>',
|
|
24
|
-
'<%= config.bin %> <%= command.id %> --no-dts',
|
|
25
|
-
'<%= config.bin %> <%= command.id %> --sourcemap',
|
|
26
|
-
'<%= config.bin %> <%= command.id %> @nocobase/acl',
|
|
27
|
-
'<%= config.bin %> <%= command.id %> @nocobase/acl @nocobase/actions',
|
|
28
|
-
];
|
|
29
|
-
static flags = {
|
|
30
|
-
'cwd': Flags.string({ description: 'Current working directory', char: 'c', required: false }),
|
|
31
|
-
'no-dts': Flags.boolean({ description: 'not generate dts' }),
|
|
32
|
-
sourcemap: Flags.boolean({ description: 'generate sourcemap' }),
|
|
33
|
-
verbose: Flags.boolean({ description: 'Show detailed command output', default: false }),
|
|
34
|
-
};
|
|
35
|
-
async run() {
|
|
36
|
-
const { args, flags } = await this.parse(Build);
|
|
37
|
-
setVerboseMode(flags.verbose);
|
|
38
|
-
const packages = args.packages ?? [];
|
|
39
|
-
const npmArgs = ['build', ...packages];
|
|
40
|
-
if (flags['no-dts']) {
|
|
41
|
-
npmArgs.push('--no-dts');
|
|
42
|
-
}
|
|
43
|
-
if (flags.sourcemap) {
|
|
44
|
-
npmArgs.push('--sourcemap');
|
|
45
|
-
}
|
|
46
|
-
try {
|
|
47
|
-
await runNocoBaseCommand(npmArgs, {
|
|
48
|
-
cwd: flags['cwd'],
|
|
49
|
-
stdio: flags.verbose ? 'inherit' : 'ignore',
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
catch (error) {
|
|
53
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
-
this.error(message);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
9
|
+
import SourceBuild from './source/build.js';
|
|
10
|
+
export default class Build extends SourceBuild {
|
|
11
|
+
static hidden = true;
|
|
57
12
|
}
|