@nocobase/cli 2.1.0-alpha.25 → 2.1.0-alpha.27
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 +61 -49
- package/README.zh-CN.md +40 -47
- package/dist/commands/app/down.js +259 -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 +579 -0
- package/dist/commands/build.js +3 -48
- package/dist/commands/config/delete.js +30 -0
- package/dist/commands/config/get.js +29 -0
- package/dist/commands/config/index.js +20 -0
- package/dist/commands/config/list.js +29 -0
- package/dist/commands/config/set.js +35 -0
- package/dist/commands/db/check.js +230 -0
- package/dist/commands/db/shared.js +1 -1
- 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 +83 -6
- package/dist/commands/install.js +361 -82
- package/dist/commands/license/activate.js +357 -0
- package/dist/commands/license/env.js +94 -0
- package/dist/commands/license/generate-id.js +107 -0
- package/dist/commands/license/id.js +52 -0
- package/dist/commands/license/index.js +20 -0
- package/dist/commands/license/plugins/clean.js +98 -0
- package/dist/commands/license/plugins/index.js +20 -0
- package/dist/commands/license/plugins/list.js +50 -0
- package/dist/commands/license/plugins/shared.js +325 -0
- package/dist/commands/license/plugins/sync.js +267 -0
- package/dist/commands/license/shared.js +411 -0
- package/dist/commands/license/status.js +50 -0
- 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 +94 -9
- package/dist/lib/app-health.js +126 -0
- package/dist/lib/app-managed-resources.js +264 -0
- package/dist/lib/app-runtime.js +26 -10
- package/dist/lib/auth-store.js +29 -63
- package/dist/lib/build-config.js +8 -0
- package/dist/lib/cli-config.js +176 -0
- package/dist/lib/cli-home.js +12 -26
- package/dist/lib/cli-locale.js +15 -1
- package/dist/lib/db-connection-check.js +178 -0
- package/dist/lib/env-config.js +80 -0
- package/dist/lib/generated-command.js +23 -3
- package/dist/lib/plugin-storage.js +127 -0
- package/dist/lib/prompt-validators.js +4 -4
- package/dist/lib/prompt-web-ui.js +13 -6
- package/dist/lib/runtime-generator.js +89 -10
- package/dist/lib/self-manager.js +57 -2
- package/dist/lib/skills-manager.js +34 -7
- package/dist/lib/startup-update.js +85 -7
- package/dist/locale/en-US.json +16 -13
- package/dist/locale/zh-CN.json +16 -13
- package/nocobase-ctl.config.json +82 -0
- package/package.json +41 -6
- package/dist/commands/ps.js +0 -119
|
@@ -0,0 +1,579 @@
|
|
|
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 { upsertEnv } from '../../lib/auth-store.js';
|
|
11
|
+
import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, startDockerContainer, stopDockerContainer, } from '../../lib/app-runtime.js';
|
|
12
|
+
import { resolveConfiguredEnvPath } from '../../lib/cli-home.js';
|
|
13
|
+
import { commandSucceeds, run } from '../../lib/run-npm.js';
|
|
14
|
+
import { failTask, printInfo, startTask, stopTask, succeedTask, updateTask } from '../../lib/ui.js';
|
|
15
|
+
const DEFAULT_DOCKER_REGISTRY = 'nocobase/nocobase';
|
|
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 normalizeDockerPlatform(value) {
|
|
96
|
+
const text = String(value ?? '').trim();
|
|
97
|
+
if (!text || text === 'auto') {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
if (text === 'linux/amd64' || text === 'linux/arm64') {
|
|
101
|
+
return text;
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
function readEnvValue(env, key) {
|
|
106
|
+
if (key === 'appRootPath' || key === 'storagePath') {
|
|
107
|
+
return trimValue(resolveConfiguredEnvPath(env.config[key]));
|
|
108
|
+
}
|
|
109
|
+
return trimValue(env.config[key]);
|
|
110
|
+
}
|
|
111
|
+
async function sleep(ms) {
|
|
112
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
113
|
+
}
|
|
114
|
+
async function requestAppHealthCheck(params) {
|
|
115
|
+
const controller = new AbortController();
|
|
116
|
+
const timeout = setTimeout(() => {
|
|
117
|
+
controller.abort();
|
|
118
|
+
}, params.requestTimeoutMs ?? APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS);
|
|
119
|
+
try {
|
|
120
|
+
const response = await (params.fetchImpl ?? fetch)(params.healthCheckUrl, {
|
|
121
|
+
method: 'GET',
|
|
122
|
+
signal: controller.signal,
|
|
123
|
+
});
|
|
124
|
+
const text = await response.text().catch(() => '');
|
|
125
|
+
const body = text.replace(/\s+/g, ' ').trim() || 'No response yet';
|
|
126
|
+
return {
|
|
127
|
+
ok: response.ok && text.trim().toLowerCase() === 'ok',
|
|
128
|
+
message: `HTTP ${response.status}: ${body}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
message: `No response within ${Math.ceil((params.requestTimeoutMs ?? APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS) / 1000)}s`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
message: error instanceof Error ? error.message : String(error),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
clearTimeout(timeout);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function waitForAppHealthCheck(params) {
|
|
148
|
+
if (!params.apiBaseUrl) {
|
|
149
|
+
printInfo(`Skipping health check for "${params.envName}" because no local API URL is saved for this env.`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const healthCheckUrl = buildHealthCheckUrl(params.apiBaseUrl);
|
|
153
|
+
const startedAt = Date.now();
|
|
154
|
+
let lastMessage = 'No response yet';
|
|
155
|
+
let spinnerActive = true;
|
|
156
|
+
startTask(`Waiting for NocoBase to become ready for "${params.envName}"...`);
|
|
157
|
+
try {
|
|
158
|
+
while (Date.now() - startedAt < APP_HEALTH_CHECK_TIMEOUT_MS) {
|
|
159
|
+
const result = await requestAppHealthCheck({
|
|
160
|
+
healthCheckUrl,
|
|
161
|
+
fetchImpl: params.fetchImpl,
|
|
162
|
+
});
|
|
163
|
+
if (result.ok) {
|
|
164
|
+
stopTask();
|
|
165
|
+
spinnerActive = false;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
lastMessage = result.message;
|
|
169
|
+
const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
|
|
170
|
+
updateTask(`Waiting for NocoBase to become ready for "${params.envName}"... (${elapsedSeconds}s elapsed, last status: ${lastMessage})`);
|
|
171
|
+
await sleep(APP_HEALTH_CHECK_INTERVAL_MS);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
if (spinnerActive) {
|
|
176
|
+
stopTask();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const logHint = params.containerName
|
|
180
|
+
? ` You can inspect startup logs with: docker logs ${params.containerName}`
|
|
181
|
+
: '';
|
|
182
|
+
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}`);
|
|
183
|
+
}
|
|
184
|
+
async function dockerContainerExists(containerName) {
|
|
185
|
+
return await commandSucceeds('docker', ['container', 'inspect', containerName]);
|
|
186
|
+
}
|
|
187
|
+
async function ensureDockerNetwork(name) {
|
|
188
|
+
const exists = await commandSucceeds('docker', ['network', 'inspect', name]);
|
|
189
|
+
if (exists) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
await run('docker', ['network', 'create', name], {
|
|
193
|
+
errorName: 'docker network create',
|
|
194
|
+
stdio: 'ignore',
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
export default class AppUpgrade extends Command {
|
|
198
|
+
static hidden = false;
|
|
199
|
+
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. Use --version to upgrade to a specific saved source version or image tag.';
|
|
200
|
+
static examples = [
|
|
201
|
+
'<%= config.bin %> <%= command.id %>',
|
|
202
|
+
'<%= config.bin %> <%= command.id %> --env local',
|
|
203
|
+
'<%= config.bin %> <%= command.id %> --env local -s',
|
|
204
|
+
'<%= config.bin %> <%= command.id %> --env local --version beta',
|
|
205
|
+
'<%= config.bin %> <%= command.id %> --env local --verbose',
|
|
206
|
+
'<%= config.bin %> <%= command.id %> --env local-docker -s',
|
|
207
|
+
];
|
|
208
|
+
static flags = {
|
|
209
|
+
env: Flags.string({
|
|
210
|
+
char: 'e',
|
|
211
|
+
description: 'CLI env name to upgrade. Defaults to the current env when omitted',
|
|
212
|
+
}),
|
|
213
|
+
'skip-code-update': Flags.boolean({
|
|
214
|
+
char: 's',
|
|
215
|
+
description: 'Restart with the saved local code or Docker image without downloading updates first',
|
|
216
|
+
required: false,
|
|
217
|
+
}),
|
|
218
|
+
version: Flags.string({
|
|
219
|
+
description: 'Override the saved downloadVersion for this upgrade. When the upgrade succeeds, the new version is saved back to the env config.',
|
|
220
|
+
required: false,
|
|
221
|
+
}),
|
|
222
|
+
verbose: Flags.boolean({
|
|
223
|
+
description: 'Show raw output from the underlying local or Docker commands',
|
|
224
|
+
default: false,
|
|
225
|
+
}),
|
|
226
|
+
};
|
|
227
|
+
static resolveUpgradeVersion(runtime, flags) {
|
|
228
|
+
const requestedVersion = trimValue(flags.version);
|
|
229
|
+
if (requestedVersion && flags['skip-code-update']) {
|
|
230
|
+
throw new Error('`--version` and `--skip-code-update` cannot be used together. Use `--version` to download a specific upgrade target, or `--skip-code-update` to restart the saved code/image as-is.');
|
|
231
|
+
}
|
|
232
|
+
if (runtime.kind === 'local' && runtime.source === 'local') {
|
|
233
|
+
if (requestedVersion) {
|
|
234
|
+
throw new Error([
|
|
235
|
+
`Env "${runtime.envName}" is managed from an existing local app path.`,
|
|
236
|
+
'This source does not support `nb app upgrade --version` because the CLI does not manage that code checkout.',
|
|
237
|
+
'Update the local app path yourself, then run `nb app upgrade` to restart it.',
|
|
238
|
+
].join('\n'));
|
|
239
|
+
}
|
|
240
|
+
return {};
|
|
241
|
+
}
|
|
242
|
+
const savedVersion = readEnvValue(runtime.env, 'downloadVersion');
|
|
243
|
+
const downloadVersion = requestedVersion || savedVersion;
|
|
244
|
+
if (!downloadVersion) {
|
|
245
|
+
throw new Error([
|
|
246
|
+
`Env "${runtime.envName}" does not have a saved \`downloadVersion\`.`,
|
|
247
|
+
'This env cannot be upgraded until a source version is explicit.',
|
|
248
|
+
'Re-run `nb init` or `nb env add` for this env, or pass `--version` to `nb app upgrade`.',
|
|
249
|
+
].join('\n'));
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
downloadVersion,
|
|
253
|
+
persistDownloadVersion: requestedVersion || undefined,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
static buildLocalDownloadArgv(runtime, downloadVersion) {
|
|
257
|
+
const argv = ['-y', '--no-intro', '--source', runtime.source, '--replace'];
|
|
258
|
+
const gitUrl = readEnvValue(runtime.env, 'gitUrl');
|
|
259
|
+
const npmRegistry = readEnvValue(runtime.env, 'npmRegistry');
|
|
260
|
+
argv.push('--version', downloadVersion, '--output-dir', runtime.projectRoot);
|
|
261
|
+
if (gitUrl) {
|
|
262
|
+
argv.push('--git-url', gitUrl);
|
|
263
|
+
}
|
|
264
|
+
if (npmRegistry) {
|
|
265
|
+
argv.push('--npm-registry', npmRegistry);
|
|
266
|
+
}
|
|
267
|
+
if (runtime.env.config.devDependencies === true) {
|
|
268
|
+
argv.push('--dev-dependencies');
|
|
269
|
+
}
|
|
270
|
+
if (runtime.env.config.build === false) {
|
|
271
|
+
argv.push('--no-build');
|
|
272
|
+
}
|
|
273
|
+
if (runtime.env.config.buildDts === true) {
|
|
274
|
+
argv.push('--build-dts');
|
|
275
|
+
}
|
|
276
|
+
return argv;
|
|
277
|
+
}
|
|
278
|
+
static buildDockerDownloadArgv(runtime, plan) {
|
|
279
|
+
const argv = [
|
|
280
|
+
'-y',
|
|
281
|
+
'--no-intro',
|
|
282
|
+
'--source',
|
|
283
|
+
'docker',
|
|
284
|
+
'--replace',
|
|
285
|
+
'--docker-registry',
|
|
286
|
+
plan.dockerRegistry,
|
|
287
|
+
'--version',
|
|
288
|
+
plan.downloadVersion,
|
|
289
|
+
];
|
|
290
|
+
const dockerPlatform = normalizeDockerPlatform(runtime.env.config.dockerPlatform);
|
|
291
|
+
if (dockerPlatform) {
|
|
292
|
+
argv.push('--docker-platform', dockerPlatform);
|
|
293
|
+
}
|
|
294
|
+
return argv;
|
|
295
|
+
}
|
|
296
|
+
static buildLocalStartArgv(runtime) {
|
|
297
|
+
const argv = ['start', '--quickstart'];
|
|
298
|
+
const appPort = runtime.env.appPort === undefined || runtime.env.appPort === null
|
|
299
|
+
? ''
|
|
300
|
+
: trimValue(runtime.env.appPort);
|
|
301
|
+
if (appPort) {
|
|
302
|
+
argv.push('--port', appPort);
|
|
303
|
+
}
|
|
304
|
+
argv.push('--daemon');
|
|
305
|
+
return argv;
|
|
306
|
+
}
|
|
307
|
+
static buildDockerUpgradePlan(runtime, downloadVersion) {
|
|
308
|
+
const dockerRegistry = readEnvValue(runtime.env, 'dockerRegistry') || DEFAULT_DOCKER_REGISTRY;
|
|
309
|
+
const appPort = runtime.env.appPort === undefined || runtime.env.appPort === null
|
|
310
|
+
? ''
|
|
311
|
+
: trimValue(runtime.env.appPort);
|
|
312
|
+
const storagePath = readEnvValue(runtime.env, 'storagePath');
|
|
313
|
+
const appKey = readEnvValue(runtime.env, 'appKey');
|
|
314
|
+
const timeZone = readEnvValue(runtime.env, 'timezone');
|
|
315
|
+
const dbDialect = readEnvValue(runtime.env, 'dbDialect');
|
|
316
|
+
const dbHost = readEnvValue(runtime.env, 'dbHost');
|
|
317
|
+
const dbPort = readEnvValue(runtime.env, 'dbPort');
|
|
318
|
+
const dbDatabase = readEnvValue(runtime.env, 'dbDatabase');
|
|
319
|
+
const dbUser = readEnvValue(runtime.env, 'dbUser');
|
|
320
|
+
const dbPassword = readEnvValue(runtime.env, 'dbPassword');
|
|
321
|
+
const networkName = trimValue(runtime.dockerNetworkName || runtime.workspaceName);
|
|
322
|
+
const missing = [];
|
|
323
|
+
if (!networkName) {
|
|
324
|
+
missing.push('docker.network');
|
|
325
|
+
}
|
|
326
|
+
if (!storagePath) {
|
|
327
|
+
missing.push('storagePath');
|
|
328
|
+
}
|
|
329
|
+
if (!appKey) {
|
|
330
|
+
missing.push('appKey');
|
|
331
|
+
}
|
|
332
|
+
if (!timeZone) {
|
|
333
|
+
missing.push('timezone');
|
|
334
|
+
}
|
|
335
|
+
if (!dbDialect) {
|
|
336
|
+
missing.push('dbDialect');
|
|
337
|
+
}
|
|
338
|
+
if (!dbHost) {
|
|
339
|
+
missing.push('dbHost');
|
|
340
|
+
}
|
|
341
|
+
if (!dbPort) {
|
|
342
|
+
missing.push('dbPort');
|
|
343
|
+
}
|
|
344
|
+
if (!dbDatabase) {
|
|
345
|
+
missing.push('dbDatabase');
|
|
346
|
+
}
|
|
347
|
+
if (!dbUser) {
|
|
348
|
+
missing.push('dbUser');
|
|
349
|
+
}
|
|
350
|
+
if (!dbPassword) {
|
|
351
|
+
missing.push('dbPassword');
|
|
352
|
+
}
|
|
353
|
+
if (missing.length > 0) {
|
|
354
|
+
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.`);
|
|
355
|
+
}
|
|
356
|
+
const imageRef = `${dockerRegistry}:${downloadVersion}`;
|
|
357
|
+
const args = [
|
|
358
|
+
'run',
|
|
359
|
+
'-d',
|
|
360
|
+
'--name',
|
|
361
|
+
runtime.containerName,
|
|
362
|
+
'--restart',
|
|
363
|
+
'always',
|
|
364
|
+
'--network',
|
|
365
|
+
networkName,
|
|
366
|
+
];
|
|
367
|
+
if (appPort) {
|
|
368
|
+
args.push('-p', `${appPort}:80`);
|
|
369
|
+
}
|
|
370
|
+
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);
|
|
371
|
+
return {
|
|
372
|
+
containerName: runtime.containerName,
|
|
373
|
+
networkName,
|
|
374
|
+
dockerRegistry,
|
|
375
|
+
downloadVersion,
|
|
376
|
+
imageRef,
|
|
377
|
+
appPort: appPort || undefined,
|
|
378
|
+
storagePath,
|
|
379
|
+
appKey,
|
|
380
|
+
timeZone,
|
|
381
|
+
dbDialect,
|
|
382
|
+
dbHost,
|
|
383
|
+
dbPort,
|
|
384
|
+
dbDatabase,
|
|
385
|
+
dbUser,
|
|
386
|
+
dbPassword,
|
|
387
|
+
args,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
static async upgradeLocal(runCommand, runtime, downloadVersion, flags, commandStdio) {
|
|
391
|
+
const displayUrl = formatDisplayUrl(resolveApiBaseUrl(runtime), trimValue(runtime.env.appPort));
|
|
392
|
+
startTask(`Stopping NocoBase for "${runtime.envName}" before upgrade...`);
|
|
393
|
+
try {
|
|
394
|
+
await runLocalNocoBaseCommand(runtime, ['pm2', 'kill'], {
|
|
395
|
+
stdio: commandStdio,
|
|
396
|
+
});
|
|
397
|
+
succeedTask(`Stopped the current NocoBase process for "${runtime.envName}".`);
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
stopTask();
|
|
401
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
402
|
+
printInfo(`No running background process was stopped for "${runtime.envName}". Continuing with the upgrade. (${message})`);
|
|
403
|
+
}
|
|
404
|
+
if (!flags['skip-code-update'] && (runtime.source === 'npm' || runtime.source === 'git')) {
|
|
405
|
+
startTask(`Refreshing NocoBase files for "${runtime.envName}" from the saved ${runtime.source} source...`);
|
|
406
|
+
try {
|
|
407
|
+
await runCommand('source:download', AppUpgrade.buildLocalDownloadArgv(runtime, downloadVersion));
|
|
408
|
+
succeedTask(`NocoBase files are up to date for "${runtime.envName}".`);
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
412
|
+
failTask(`Failed to refresh NocoBase files for "${runtime.envName}".`);
|
|
413
|
+
throw new Error(formatLocalDownloadFailure(runtime.envName, runtime.source, message));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else if (flags['skip-code-update']) {
|
|
417
|
+
printInfo(`Skipping code download for "${runtime.envName}" (--skip-code-update).`);
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
printInfo(`Skipping code download for "${runtime.envName}" because this env is managed from an existing local app path.`);
|
|
421
|
+
}
|
|
422
|
+
startTask(`Starting upgraded NocoBase for "${runtime.envName}"...`);
|
|
423
|
+
try {
|
|
424
|
+
await runLocalNocoBaseCommand(runtime, AppUpgrade.buildLocalStartArgv(runtime), {
|
|
425
|
+
stdio: commandStdio,
|
|
426
|
+
});
|
|
427
|
+
succeedTask(`Upgraded NocoBase is starting for "${runtime.envName}".`);
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
431
|
+
failTask(`Failed to start upgraded NocoBase for "${runtime.envName}".`);
|
|
432
|
+
throw new Error(formatLocalStartFailure(runtime.envName, runtime.source, trimValue(runtime.env.appPort), message));
|
|
433
|
+
}
|
|
434
|
+
await waitForAppHealthCheck({
|
|
435
|
+
envName: runtime.envName,
|
|
436
|
+
apiBaseUrl: resolveApiBaseUrl(runtime),
|
|
437
|
+
});
|
|
438
|
+
return displayUrl;
|
|
439
|
+
}
|
|
440
|
+
static async upgradeDocker(runCommand, runtime, downloadVersion, flags, commandStdio) {
|
|
441
|
+
const apiBaseUrl = resolveApiBaseUrl(runtime);
|
|
442
|
+
const containerExists = await dockerContainerExists(runtime.containerName);
|
|
443
|
+
if (!flags['skip-code-update']) {
|
|
444
|
+
const plan = AppUpgrade.buildDockerUpgradePlan(runtime, downloadVersion);
|
|
445
|
+
startTask(`Refreshing the Docker image for "${runtime.envName}"...`);
|
|
446
|
+
try {
|
|
447
|
+
await runCommand('source:download', AppUpgrade.buildDockerDownloadArgv(runtime, plan));
|
|
448
|
+
succeedTask(`Docker image is ready for "${runtime.envName}".`);
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
452
|
+
failTask(`Failed to refresh the Docker image for "${runtime.envName}".`);
|
|
453
|
+
throw new Error(formatDockerDownloadFailure(runtime.envName, message));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
printInfo(`Skipping image download for "${runtime.envName}" (--skip-code-update).`);
|
|
458
|
+
}
|
|
459
|
+
if (containerExists) {
|
|
460
|
+
startTask(`Stopping the current Docker app for "${runtime.envName}"...`);
|
|
461
|
+
try {
|
|
462
|
+
const state = await stopDockerContainer(runtime.containerName, {
|
|
463
|
+
stdio: commandStdio,
|
|
464
|
+
});
|
|
465
|
+
succeedTask(state === 'already-stopped'
|
|
466
|
+
? `The current Docker app was already stopped for "${runtime.envName}".`
|
|
467
|
+
: `Stopped the current Docker app for "${runtime.envName}".`);
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
stopTask();
|
|
471
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
472
|
+
printInfo(`Could not stop the existing Docker container for "${runtime.envName}" cleanly. Continuing with container recreation. (${message})`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (flags['skip-code-update'] && containerExists) {
|
|
476
|
+
startTask(`Starting NocoBase for "${runtime.envName}" with the saved Docker image...`);
|
|
477
|
+
try {
|
|
478
|
+
const state = await startDockerContainer(runtime.containerName, {
|
|
479
|
+
stdio: commandStdio,
|
|
480
|
+
});
|
|
481
|
+
succeedTask(state === 'already-running'
|
|
482
|
+
? `NocoBase is already running for "${runtime.envName}".`
|
|
483
|
+
: `NocoBase is starting for "${runtime.envName}".`);
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
487
|
+
failTask(`Failed to start the Docker app for "${runtime.envName}".`);
|
|
488
|
+
throw new Error(formatDockerStartFailure(runtime.envName, message));
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
const plan = AppUpgrade.buildDockerUpgradePlan(runtime, downloadVersion);
|
|
493
|
+
const displayUrl = formatDisplayUrl(apiBaseUrl, plan.appPort);
|
|
494
|
+
startTask(`Recreating the Docker app container for "${runtime.envName}"...`);
|
|
495
|
+
try {
|
|
496
|
+
if (containerExists) {
|
|
497
|
+
await run('docker', ['rm', '-f', runtime.containerName], {
|
|
498
|
+
errorName: 'docker rm',
|
|
499
|
+
stdio: commandStdio,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
await ensureDockerNetwork(plan.networkName);
|
|
503
|
+
await run('docker', plan.args, {
|
|
504
|
+
errorName: 'docker run',
|
|
505
|
+
stdio: commandStdio,
|
|
506
|
+
});
|
|
507
|
+
succeedTask(`Docker app container is ready for "${runtime.envName}".`);
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
511
|
+
failTask(`Failed to recreate the Docker app for "${runtime.envName}".`);
|
|
512
|
+
throw new Error(formatDockerStartFailure(runtime.envName, message));
|
|
513
|
+
}
|
|
514
|
+
await waitForAppHealthCheck({
|
|
515
|
+
envName: runtime.envName,
|
|
516
|
+
apiBaseUrl,
|
|
517
|
+
containerName: runtime.containerName,
|
|
518
|
+
});
|
|
519
|
+
return displayUrl;
|
|
520
|
+
}
|
|
521
|
+
await waitForAppHealthCheck({
|
|
522
|
+
envName: runtime.envName,
|
|
523
|
+
apiBaseUrl,
|
|
524
|
+
containerName: runtime.containerName,
|
|
525
|
+
});
|
|
526
|
+
return formatDisplayUrl(apiBaseUrl, trimValue(runtime.env.appPort));
|
|
527
|
+
}
|
|
528
|
+
static async persistDownloadVersion(runtime, downloadVersion) {
|
|
529
|
+
const { name: _name, ...envConfig } = runtime.env.config;
|
|
530
|
+
try {
|
|
531
|
+
await upsertEnv(runtime.envName, {
|
|
532
|
+
...envConfig,
|
|
533
|
+
downloadVersion,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
538
|
+
throw new Error(`NocoBase was upgraded for "${runtime.envName}", but the CLI could not save \`downloadVersion=${downloadVersion}\`. Details: ${message}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async run() {
|
|
542
|
+
const { flags } = await this.parse(AppUpgrade);
|
|
543
|
+
const parsed = flags;
|
|
544
|
+
const requestedEnv = parsed.env?.trim() || undefined;
|
|
545
|
+
const commandStdio = parsed.verbose ? 'inherit' : 'ignore';
|
|
546
|
+
const runtime = await resolveManagedAppRuntime(requestedEnv);
|
|
547
|
+
if (!runtime) {
|
|
548
|
+
this.error(formatMissingManagedAppEnvMessage(requestedEnv));
|
|
549
|
+
}
|
|
550
|
+
if (runtime.kind === 'http') {
|
|
551
|
+
this.error([
|
|
552
|
+
`Can't upgrade "${runtime.envName}" from this machine.`,
|
|
553
|
+
'This env only has an API connection, so there is no saved local app or Docker runtime to upgrade here.',
|
|
554
|
+
'If you want a local NocoBase AI environment that the CLI can upgrade, run `nb init` first.',
|
|
555
|
+
].join('\n'));
|
|
556
|
+
}
|
|
557
|
+
if (runtime.kind === 'ssh') {
|
|
558
|
+
this.error([
|
|
559
|
+
`Can't upgrade "${runtime.envName}" yet.`,
|
|
560
|
+
'SSH env support is reserved but not implemented yet.',
|
|
561
|
+
'Use a local or Docker env if you need CLI-managed upgrades right now.',
|
|
562
|
+
].join('\n'));
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
const resolvedVersion = AppUpgrade.resolveUpgradeVersion(runtime, parsed);
|
|
566
|
+
const runCommand = this.config.runCommand.bind(this.config);
|
|
567
|
+
const displayUrl = runtime.kind === 'docker'
|
|
568
|
+
? await AppUpgrade.upgradeDocker(runCommand, runtime, resolvedVersion.downloadVersion, parsed, commandStdio)
|
|
569
|
+
: await AppUpgrade.upgradeLocal(runCommand, runtime, resolvedVersion.downloadVersion, parsed, commandStdio);
|
|
570
|
+
if (resolvedVersion.persistDownloadVersion) {
|
|
571
|
+
await AppUpgrade.persistDownloadVersion(runtime, resolvedVersion.persistDownloadVersion);
|
|
572
|
+
}
|
|
573
|
+
succeedTask(`NocoBase has been upgraded for "${runtime.envName}"${displayUrl ? ` at ${displayUrl}` : ''}.`);
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
this.error(error instanceof Error ? error.message : String(error));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
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
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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 { Args, Command } from '@oclif/core';
|
|
10
|
+
import { assertSupportedCliConfigKey, deleteCliConfigValue } from '../../lib/cli-config.js';
|
|
11
|
+
export default class ConfigDelete extends Command {
|
|
12
|
+
static summary = 'Delete an explicitly configured CLI setting';
|
|
13
|
+
static examples = [
|
|
14
|
+
'<%= config.bin %> <%= command.id %> license.pkg-url',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> docker.network',
|
|
16
|
+
'<%= config.bin %> <%= command.id %> docker.container-prefix',
|
|
17
|
+
];
|
|
18
|
+
static args = {
|
|
19
|
+
key: Args.string({
|
|
20
|
+
description: 'Configuration key',
|
|
21
|
+
required: true,
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
async run() {
|
|
25
|
+
const { args } = await this.parse(ConfigDelete);
|
|
26
|
+
const key = assertSupportedCliConfigKey(args.key);
|
|
27
|
+
const removed = await deleteCliConfigValue(key);
|
|
28
|
+
this.log(removed ? `Deleted ${key}` : `${key} was not set`);
|
|
29
|
+
}
|
|
30
|
+
}
|