@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
package/dist/lib/api-client.js
CHANGED
|
@@ -6,7 +6,19 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* This file is part of the NocoBase (R) project.
|
|
11
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
12
|
+
* Authors: NocoBase Team.
|
|
13
|
+
*
|
|
14
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
15
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
16
|
+
*/
|
|
17
|
+
import { createWriteStream } from 'node:fs';
|
|
9
18
|
import { promises as fs } from 'node:fs';
|
|
19
|
+
import { basename, dirname } from 'node:path';
|
|
20
|
+
import { Readable } from 'node:stream';
|
|
21
|
+
import { pipeline } from 'node:stream/promises';
|
|
10
22
|
import { resolveServerRequestTarget } from './env-auth.js';
|
|
11
23
|
import { fetchWithPreservedAuthRedirect } from './http-request.js';
|
|
12
24
|
const CLI_REQUEST_SOURCE_HEADER = 'x-request-source';
|
|
@@ -43,6 +55,20 @@ async function parseResponse(response) {
|
|
|
43
55
|
data,
|
|
44
56
|
};
|
|
45
57
|
}
|
|
58
|
+
async function parseBinaryResponse(response, outputPath) {
|
|
59
|
+
if (response.ok && response.body) {
|
|
60
|
+
await fs.mkdir(dirname(outputPath), { recursive: true }).catch(() => undefined);
|
|
61
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(outputPath));
|
|
62
|
+
return {
|
|
63
|
+
ok: response.ok,
|
|
64
|
+
status: response.status,
|
|
65
|
+
data: {
|
|
66
|
+
output: outputPath,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return parseResponse(response);
|
|
71
|
+
}
|
|
46
72
|
function parseScalarValue(value, type) {
|
|
47
73
|
if (value === undefined) {
|
|
48
74
|
return undefined;
|
|
@@ -105,6 +131,9 @@ function parseBodyFieldValue(rawValue, parameter) {
|
|
|
105
131
|
return parseScalarValue(rawValue, parameter.type);
|
|
106
132
|
}
|
|
107
133
|
export async function parseBody(flags, operation) {
|
|
134
|
+
if (operation.requestContentType === 'multipart/form-data') {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
108
137
|
const inlineBody = flags.body;
|
|
109
138
|
const bodyFile = flags['body-file'];
|
|
110
139
|
const bodyParameters = operation.parameters.filter((parameter) => parameter.in === 'body');
|
|
@@ -143,6 +172,39 @@ export async function parseBody(flags, operation) {
|
|
|
143
172
|
}
|
|
144
173
|
return undefined;
|
|
145
174
|
}
|
|
175
|
+
async function createMultipartBody(flags, operation) {
|
|
176
|
+
const bodyParameters = operation.parameters.filter((parameter) => parameter.in === 'body');
|
|
177
|
+
const formData = new FormData();
|
|
178
|
+
let hasValues = false;
|
|
179
|
+
for (const parameter of bodyParameters) {
|
|
180
|
+
const rawValue = flags[parameter.flagName];
|
|
181
|
+
const hasValue = hasParameterValue(flags, parameter);
|
|
182
|
+
if (parameter.required && !hasValue) {
|
|
183
|
+
throw new Error(`Missing required body field --${parameter.flagName}`);
|
|
184
|
+
}
|
|
185
|
+
if (!hasValue) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (parameter.isFile) {
|
|
189
|
+
const filePath = String(rawValue);
|
|
190
|
+
const content = await fs.readFile(filePath);
|
|
191
|
+
const arrayBuffer = content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);
|
|
192
|
+
formData.append(parameter.name, new Blob([arrayBuffer]), basename(filePath));
|
|
193
|
+
hasValues = true;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const value = parseBodyFieldValue(rawValue, parameter);
|
|
197
|
+
if (value === undefined) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
formData.append(parameter.name, typeof value === 'object' ? JSON.stringify(value) : String(value));
|
|
201
|
+
hasValues = true;
|
|
202
|
+
}
|
|
203
|
+
if (!hasValues && operation.bodyRequired) {
|
|
204
|
+
throw new Error('Missing multipart request body.');
|
|
205
|
+
}
|
|
206
|
+
return hasValues ? formData : undefined;
|
|
207
|
+
}
|
|
146
208
|
export async function executeApiRequest(options) {
|
|
147
209
|
const { baseUrl, token } = await resolveServerRequestTarget(options);
|
|
148
210
|
const headers = new Headers();
|
|
@@ -190,8 +252,10 @@ export async function executeApiRequest(options) {
|
|
|
190
252
|
continue;
|
|
191
253
|
}
|
|
192
254
|
}
|
|
193
|
-
const body =
|
|
194
|
-
|
|
255
|
+
const body = options.operation.requestContentType === 'multipart/form-data'
|
|
256
|
+
? await createMultipartBody(options.flags, options.operation)
|
|
257
|
+
: await parseBody(options.flags, options.operation);
|
|
258
|
+
if (body !== undefined && options.operation.requestContentType !== 'multipart/form-data') {
|
|
195
259
|
headers.set('content-type', 'application/json');
|
|
196
260
|
}
|
|
197
261
|
const url = new URL(`${normalizeBaseUrl(baseUrl)}${requestPath}`);
|
|
@@ -199,8 +263,15 @@ export async function executeApiRequest(options) {
|
|
|
199
263
|
const response = await fetchWithPreservedAuthRedirect(url.toString(), {
|
|
200
264
|
method: options.operation.method.toUpperCase(),
|
|
201
265
|
headers,
|
|
202
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
266
|
+
body: body === undefined ? undefined : body instanceof FormData ? body : JSON.stringify(body),
|
|
203
267
|
});
|
|
268
|
+
if (options.operation.responseType === 'binary') {
|
|
269
|
+
const outputPath = options.flags.output;
|
|
270
|
+
if (!outputPath) {
|
|
271
|
+
throw new Error('Missing required output path --output');
|
|
272
|
+
}
|
|
273
|
+
return parseBinaryResponse(response, outputPath);
|
|
274
|
+
}
|
|
204
275
|
return parseResponse(response);
|
|
205
276
|
}
|
|
206
277
|
export async function executeRawApiRequest(options) {
|
|
@@ -235,10 +306,24 @@ export async function executeRawApiRequest(options) {
|
|
|
235
306
|
}
|
|
236
307
|
url.searchParams.set(key, typeof value === 'object' ? JSON.stringify(value) : String(value));
|
|
237
308
|
}
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
309
|
+
const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController() : undefined;
|
|
310
|
+
const timeout = controller
|
|
311
|
+
? setTimeout(() => {
|
|
312
|
+
controller.abort();
|
|
313
|
+
}, options.timeoutMs)
|
|
314
|
+
: undefined;
|
|
315
|
+
try {
|
|
316
|
+
const response = await fetchWithPreservedAuthRedirect(url.toString(), {
|
|
317
|
+
method: options.method.toUpperCase(),
|
|
318
|
+
headers,
|
|
319
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
320
|
+
signal: controller?.signal,
|
|
321
|
+
});
|
|
322
|
+
return parseResponse(response);
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
if (timeout) {
|
|
326
|
+
clearTimeout(timeout);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
244
329
|
}
|
|
@@ -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/app-runtime.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { resolveEnvKind } from './auth-store.js';
|
|
11
11
|
import { getEnv, loadAuthConfig } from './auth-store.js';
|
|
12
|
+
import { DEFAULT_DOCKER_CONTAINER_PREFIX, DEFAULT_DOCKER_NETWORK, getEffectiveCliConfigValue, } from './cli-config.js';
|
|
12
13
|
import { commandOutput, commandSucceeds, run, runNocoBaseCommand } from './run-npm.js';
|
|
13
14
|
const DOCKER_APP_WORKDIR = '/app/nocobase';
|
|
14
15
|
function sanitizeDockerResourceName(value) {
|
|
@@ -23,14 +24,24 @@ function sanitizeDockerResourceName(value) {
|
|
|
23
24
|
export function defaultWorkspaceName(cwd = process.cwd()) {
|
|
24
25
|
return sanitizeDockerResourceName(`nb-${path.basename(cwd)}`);
|
|
25
26
|
}
|
|
26
|
-
export function
|
|
27
|
-
const
|
|
28
|
-
|
|
27
|
+
export function defaultDockerContainerPrefix(cwd = process.cwd()) {
|
|
28
|
+
const configured = String(DEFAULT_DOCKER_CONTAINER_PREFIX ?? '').trim();
|
|
29
|
+
if (configured) {
|
|
30
|
+
return sanitizeDockerResourceName(configured);
|
|
31
|
+
}
|
|
32
|
+
return defaultWorkspaceName(cwd);
|
|
33
|
+
}
|
|
34
|
+
export function defaultDockerNetworkName() {
|
|
35
|
+
return sanitizeDockerResourceName(DEFAULT_DOCKER_NETWORK);
|
|
36
|
+
}
|
|
37
|
+
export function buildDockerAppContainerName(envName, containerPrefix) {
|
|
38
|
+
const prefix = containerPrefix?.trim() || defaultDockerContainerPrefix();
|
|
39
|
+
return sanitizeDockerResourceName(`${prefix}-${envName}-app`);
|
|
29
40
|
}
|
|
30
|
-
export function buildDockerDbContainerName(envName, dbDialect,
|
|
31
|
-
const
|
|
41
|
+
export function buildDockerDbContainerName(envName, dbDialect, containerPrefix) {
|
|
42
|
+
const prefix = containerPrefix?.trim() || defaultDockerContainerPrefix();
|
|
32
43
|
const dialect = dbDialect.trim() || 'postgres';
|
|
33
|
-
return sanitizeDockerResourceName(`${
|
|
44
|
+
return sanitizeDockerResourceName(`${prefix}-${envName}-${dialect}`);
|
|
34
45
|
}
|
|
35
46
|
function normalizeEnvSource(env) {
|
|
36
47
|
const source = String(env.config.source ?? '').trim();
|
|
@@ -51,7 +62,8 @@ export async function resolveManagedAppRuntime(envName) {
|
|
|
51
62
|
}
|
|
52
63
|
const resolvedName = env.name || envName?.trim() || config.currentEnv || 'default';
|
|
53
64
|
const source = normalizeEnvSource(env);
|
|
54
|
-
const
|
|
65
|
+
const dockerNetworkName = sanitizeDockerResourceName(getEffectiveCliConfigValue(config, 'docker.network') || defaultDockerNetworkName());
|
|
66
|
+
const dockerContainerPrefix = sanitizeDockerResourceName(getEffectiveCliConfigValue(config, 'docker.container-prefix') || defaultDockerContainerPrefix());
|
|
55
67
|
const kind = env.kind ?? resolveEnvKind(env.config);
|
|
56
68
|
if (kind === 'docker') {
|
|
57
69
|
return {
|
|
@@ -59,8 +71,10 @@ export async function resolveManagedAppRuntime(envName) {
|
|
|
59
71
|
env,
|
|
60
72
|
envName: resolvedName,
|
|
61
73
|
source: 'docker',
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
dockerNetworkName,
|
|
75
|
+
dockerContainerPrefix,
|
|
76
|
+
workspaceName: dockerNetworkName,
|
|
77
|
+
containerName: buildDockerAppContainerName(resolvedName, dockerContainerPrefix),
|
|
64
78
|
};
|
|
65
79
|
}
|
|
66
80
|
if (kind === 'local') {
|
|
@@ -70,7 +84,9 @@ export async function resolveManagedAppRuntime(envName) {
|
|
|
70
84
|
envName: resolvedName,
|
|
71
85
|
source: source === 'git' ? 'git' : source === 'npm' ? 'npm' : 'local',
|
|
72
86
|
projectRoot: env.appRootPath,
|
|
73
|
-
|
|
87
|
+
dockerNetworkName,
|
|
88
|
+
dockerContainerPrefix,
|
|
89
|
+
workspaceName: dockerNetworkName,
|
|
74
90
|
};
|
|
75
91
|
}
|
|
76
92
|
if (kind === 'ssh') {
|