@nocobase/cli 2.1.0-alpha.40 → 2.1.0-alpha.46
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 +20 -8
- package/README.zh-CN.md +20 -8
- package/bin/run.js +9 -37
- package/dist/commands/app/destroy.js +225 -0
- package/dist/commands/app/down.js +24 -254
- package/dist/commands/app/restart.js +12 -5
- package/dist/commands/app/shared.js +122 -0
- package/dist/commands/app/start.js +41 -59
- package/dist/commands/app/stop.js +72 -26
- package/dist/commands/app/upgrade.js +312 -429
- package/dist/commands/config/delete.js +8 -1
- package/dist/commands/config/get.js +5 -0
- package/dist/commands/config/set.js +10 -1
- package/dist/commands/db/ps.js +5 -18
- package/dist/commands/db/start.js +23 -8
- package/dist/commands/env/add.js +66 -6
- package/dist/commands/env/auth.js +86 -27
- package/dist/commands/env/info.js +52 -8
- package/dist/commands/env/list.js +6 -7
- package/dist/commands/env/shared.js +41 -3
- package/dist/commands/env/status.js +10 -7
- package/dist/commands/init.js +214 -136
- package/dist/commands/install.js +364 -294
- package/dist/commands/license/plugins/shared.js +9 -3
- package/dist/commands/license/plugins/sync.js +54 -25
- package/dist/commands/self/update.js +58 -1
- package/dist/commands/source/dev.js +5 -5
- package/dist/commands/source/download.js +29 -25
- package/dist/generated/command-registry.js +3 -2
- package/dist/lib/api-client.js +6 -0
- package/dist/lib/api-command-compat.js +641 -0
- package/dist/lib/app-health.js +31 -18
- package/dist/lib/app-managed-resources.js +1 -1
- package/dist/lib/app-runtime.js +2 -2
- package/dist/lib/auth-store.js +56 -19
- package/dist/lib/backup.js +3 -3
- package/dist/lib/bootstrap.js +14 -8
- package/dist/lib/cli-config.js +137 -4
- package/dist/lib/cli-entry-error.js +44 -0
- package/dist/lib/cli-locale.js +19 -7
- package/dist/lib/db-connection-check.js +61 -0
- package/dist/lib/docker-log-stream.js +45 -0
- package/dist/lib/env-auth.js +98 -11
- package/dist/lib/env-config.js +3 -2
- package/dist/lib/env-guard.js +1 -1
- package/dist/lib/generated-command.js +17 -0
- package/dist/lib/prompt-validators.js +23 -5
- package/dist/lib/prompt-web-ui.js +143 -19
- package/dist/lib/run-npm.js +133 -23
- package/dist/lib/skills-manager.js +80 -4
- package/dist/lib/source-publish.js +4 -1
- package/dist/lib/startup-update.js +74 -50
- package/dist/lib/ui.js +4 -1
- package/dist/locale/en-US.json +92 -5
- package/dist/locale/zh-CN.json +92 -5
- package/package.json +34 -2
|
@@ -7,145 +7,13 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
import { Command, Flags } from '@oclif/core';
|
|
10
|
-
import
|
|
11
|
-
import os from 'node:os';
|
|
12
|
-
import path from 'node:path';
|
|
13
|
-
import { buildDockerDbContainerName, formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
|
|
14
|
-
import { getCurrentEnvName, removeEnv } from '../../lib/auth-store.js';
|
|
15
|
-
import { resolveConfiguredEnvPath } from '../../lib/cli-home.js';
|
|
16
|
-
import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
|
|
17
|
-
import { confirm } from "../../lib/inquirer.js";
|
|
18
|
-
import { commandOutput, commandSucceeds, run } from '../../lib/run-npm.js';
|
|
19
|
-
import { failTask, isInteractiveTerminal, printInfo, startTask, succeedTask, } from '../../lib/ui.js';
|
|
20
|
-
function resolveConfiguredPath(value) {
|
|
21
|
-
return resolveConfiguredEnvPath(value);
|
|
22
|
-
}
|
|
23
|
-
function assertSafeRemovalPath(target, label) {
|
|
24
|
-
const resolved = path.resolve(target);
|
|
25
|
-
const cwd = path.resolve(process.cwd());
|
|
26
|
-
const home = path.resolve(os.homedir());
|
|
27
|
-
const root = path.parse(resolved).root;
|
|
28
|
-
if (resolved === root || resolved === cwd || resolved === home) {
|
|
29
|
-
throw new Error(`Refusing to remove ${label} at "${resolved}" because it is too broad.`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
async function removePathIfExists(target, label) {
|
|
33
|
-
const resolved = path.resolve(target);
|
|
34
|
-
assertSafeRemovalPath(resolved, label);
|
|
35
|
-
await fsp.rm(resolved, { recursive: true, force: true });
|
|
36
|
-
}
|
|
37
|
-
async function dockerContainerExists(containerName) {
|
|
38
|
-
return await commandSucceeds('docker', ['container', 'inspect', containerName]);
|
|
39
|
-
}
|
|
40
|
-
async function removeDockerContainerIfExists(containerName) {
|
|
41
|
-
if (!(await dockerContainerExists(containerName))) {
|
|
42
|
-
return 'missing';
|
|
43
|
-
}
|
|
44
|
-
await run('docker', ['rm', '-f', containerName], {
|
|
45
|
-
errorName: 'docker rm',
|
|
46
|
-
stdio: 'ignore',
|
|
47
|
-
});
|
|
48
|
-
return 'removed';
|
|
49
|
-
}
|
|
50
|
-
async function dockerNetworkExists(networkName) {
|
|
51
|
-
return await commandSucceeds('docker', ['network', 'inspect', networkName]);
|
|
52
|
-
}
|
|
53
|
-
async function dockerNetworkHasActiveEndpoints(networkName) {
|
|
54
|
-
try {
|
|
55
|
-
const output = await commandOutput('docker', [
|
|
56
|
-
'network',
|
|
57
|
-
'inspect',
|
|
58
|
-
networkName,
|
|
59
|
-
'--format',
|
|
60
|
-
'{{json .Containers}}',
|
|
61
|
-
], {
|
|
62
|
-
errorName: 'docker network inspect',
|
|
63
|
-
});
|
|
64
|
-
const containers = JSON.parse(output || '{}');
|
|
65
|
-
return Boolean(containers && typeof containers === 'object' && Object.keys(containers).length > 0);
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
async function removeDockerNetworkIfUnused(networkName) {
|
|
72
|
-
if (!(await dockerNetworkExists(networkName))) {
|
|
73
|
-
return 'missing';
|
|
74
|
-
}
|
|
75
|
-
try {
|
|
76
|
-
await run('docker', ['network', 'rm', networkName], {
|
|
77
|
-
errorName: 'docker network rm',
|
|
78
|
-
stdio: 'ignore',
|
|
79
|
-
});
|
|
80
|
-
return 'removed';
|
|
81
|
-
}
|
|
82
|
-
catch (error) {
|
|
83
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
84
|
-
if (/has active endpoints|is in use|active endpoints/i.test(message)
|
|
85
|
-
|| (await dockerNetworkExists(networkName) && await dockerNetworkHasActiveEndpoints(networkName))) {
|
|
86
|
-
return 'in-use';
|
|
87
|
-
}
|
|
88
|
-
throw error;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
function builtinDbContainerName(runtime) {
|
|
92
|
-
if (!runtime.env.config.builtinDb) {
|
|
93
|
-
return undefined;
|
|
94
|
-
}
|
|
95
|
-
const dbDialect = String(runtime.env.config.dbDialect ?? 'postgres').trim() || 'postgres';
|
|
96
|
-
return buildDockerDbContainerName(runtime.envName, dbDialect, runtime.dockerContainerPrefix || runtime.workspaceName);
|
|
97
|
-
}
|
|
98
|
-
function managedDockerNetworkName(runtime) {
|
|
99
|
-
return runtime.dockerNetworkName?.trim() || runtime.workspaceName?.trim() || undefined;
|
|
100
|
-
}
|
|
101
|
-
async function confirmDownAll(envName, force, options) {
|
|
102
|
-
if (force) {
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
const usedCurrentEnv = options?.explicitEnv === false;
|
|
106
|
-
if (!isInteractiveTerminal()) {
|
|
107
|
-
if (usedCurrentEnv) {
|
|
108
|
-
throw new Error(`\`nb app down --all\` is using the current env "${envName}". Re-run with --env ${envName} --force to delete everything for that env in non-interactive mode.`);
|
|
109
|
-
}
|
|
110
|
-
throw new Error(`\`nb app down --all\` needs confirmation. Re-run with --force to delete everything for "${envName}" in non-interactive mode.`);
|
|
111
|
-
}
|
|
112
|
-
try {
|
|
113
|
-
return await confirm({
|
|
114
|
-
message: usedCurrentEnv
|
|
115
|
-
? `Delete everything for current env "${envName}"? This removes the app, managed containers, storage data, and the saved CLI env config.`
|
|
116
|
-
: `Delete everything for "${envName}"? This removes the app, managed containers, storage data, and the saved CLI env config.`,
|
|
117
|
-
default: false,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
function formatDownCrossEnvForceRequiredMessage(currentEnv, requestedEnv) {
|
|
125
|
-
return [
|
|
126
|
-
`Refusing to run against env "${requestedEnv}" because the current env is "${currentEnv}" and interactive confirmation is unavailable in the current agent session.`,
|
|
127
|
-
'',
|
|
128
|
-
'For safety, the agent will not switch envs automatically and will not add --force on your behalf.',
|
|
129
|
-
'',
|
|
130
|
-
'To continue:',
|
|
131
|
-
`- run \`nb env use ${requestedEnv}\` yourself and then re-run the command, or`,
|
|
132
|
-
`- re-run the same command with \`--env ${requestedEnv} --force\` to confirm this one-off cross-env operation.`,
|
|
133
|
-
].join('\n');
|
|
134
|
-
}
|
|
135
|
-
function formatDownFailure(envName, message) {
|
|
136
|
-
return [
|
|
137
|
-
`Couldn't bring down NocoBase for "${envName}".`,
|
|
138
|
-
'Some local runtime resources may still exist. Check Docker or the local app process, then try again.',
|
|
139
|
-
`Details: ${message}`,
|
|
140
|
-
].join('\n');
|
|
141
|
-
}
|
|
10
|
+
import { printWarning } from '../../lib/ui.js';
|
|
142
11
|
export default class AppDown extends Command {
|
|
143
|
-
static hidden =
|
|
144
|
-
static description = '
|
|
12
|
+
static hidden = true;
|
|
13
|
+
static description = 'Deprecated compatibility alias for `nb app stop --with-db` and `nb app destroy`.';
|
|
145
14
|
static examples = [
|
|
146
15
|
'<%= config.bin %> <%= command.id %> --env app1',
|
|
147
16
|
'<%= config.bin %> <%= command.id %> --env app1 --all --force',
|
|
148
|
-
'<%= config.bin %> <%= command.id %> --env app1 --force',
|
|
149
17
|
];
|
|
150
18
|
static flags = {
|
|
151
19
|
env: Flags.string({
|
|
@@ -158,12 +26,12 @@ export default class AppDown extends Command {
|
|
|
158
26
|
}),
|
|
159
27
|
yes: Flags.boolean({
|
|
160
28
|
char: 'y',
|
|
161
|
-
description: '
|
|
29
|
+
description: 'Compatibility alias for confirmation flags on the replacement command',
|
|
162
30
|
default: false,
|
|
163
31
|
}),
|
|
164
32
|
force: Flags.boolean({
|
|
165
33
|
char: 'f',
|
|
166
|
-
description: '
|
|
34
|
+
description: 'Compatibility alias for confirmation flags on the replacement command',
|
|
167
35
|
default: false,
|
|
168
36
|
}),
|
|
169
37
|
verbose: Flags.boolean({
|
|
@@ -173,129 +41,31 @@ export default class AppDown extends Command {
|
|
|
173
41
|
};
|
|
174
42
|
async run() {
|
|
175
43
|
const { flags } = await this.parse(AppDown);
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (normalizedCurrentEnv && normalizedCurrentEnv !== requestedEnv && !flags.force) {
|
|
182
|
-
this.error(formatDownCrossEnvForceRequiredMessage(normalizedCurrentEnv, requestedEnv));
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
const confirmed = await ensureCrossEnvConfirmed({
|
|
187
|
-
command: this,
|
|
188
|
-
requestedEnv,
|
|
189
|
-
yes: flags.yes,
|
|
190
|
-
});
|
|
191
|
-
if (!confirmed) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
44
|
+
const runCommand = this.config.runCommand.bind(this.config);
|
|
45
|
+
const envName = flags.env?.trim();
|
|
46
|
+
const argv = [];
|
|
47
|
+
if (envName) {
|
|
48
|
+
argv.push('--env', envName);
|
|
195
49
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const removeEnvConfig = Boolean(flags.all);
|
|
199
|
-
const runtime = await resolveManagedAppRuntime(requestedEnv);
|
|
200
|
-
if (!runtime) {
|
|
201
|
-
this.error(formatMissingManagedAppEnvMessage(requestedEnv));
|
|
202
|
-
}
|
|
203
|
-
if (runtime.kind === 'http') {
|
|
204
|
-
this.error([
|
|
205
|
-
`Can't bring down "${runtime.envName}" from this machine.`,
|
|
206
|
-
'This env only has an API connection, so there is no saved local app, Docker app, or managed database to remove here.',
|
|
207
|
-
'Use `nb env remove` if you only want to remove the CLI connection config.',
|
|
208
|
-
].join('\n'));
|
|
209
|
-
}
|
|
210
|
-
if (runtime.kind === 'ssh') {
|
|
211
|
-
this.error([
|
|
212
|
-
`Can't bring down "${runtime.envName}" yet.`,
|
|
213
|
-
'SSH env support is reserved but not implemented yet.',
|
|
214
|
-
'Use `nb env remove` if you only want to remove the saved CLI config for now.',
|
|
215
|
-
].join('\n'));
|
|
50
|
+
if (flags.verbose) {
|
|
51
|
+
argv.push('--verbose');
|
|
216
52
|
}
|
|
217
53
|
if (flags.all) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
catch (error) {
|
|
223
|
-
this.error(error instanceof Error ? error.message : String(error));
|
|
224
|
-
}
|
|
225
|
-
if (!confirmed) {
|
|
226
|
-
return;
|
|
54
|
+
printWarning('`nb app down --all` is deprecated. Use `nb app destroy` instead.');
|
|
55
|
+
if (flags.force || flags.yes) {
|
|
56
|
+
argv.push('--force');
|
|
227
57
|
}
|
|
58
|
+
await runCommand('app:destroy', argv);
|
|
59
|
+
return;
|
|
228
60
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
succeedTask(state === 'removed'
|
|
234
|
-
? `Docker app container removed for "${runtime.envName}".`
|
|
235
|
-
: `No Docker app container found for "${runtime.envName}".`);
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
startTask(`Stopping local NocoBase app for "${runtime.envName}"...`);
|
|
239
|
-
await runLocalNocoBaseCommand(runtime, ['pm2', 'kill'], {
|
|
240
|
-
stdio: flags.verbose ? 'inherit' : 'ignore',
|
|
241
|
-
});
|
|
242
|
-
succeedTask(`Local NocoBase app stopped for "${runtime.envName}".`);
|
|
243
|
-
}
|
|
244
|
-
if (runtime.kind === 'local' || runtime.kind === 'docker') {
|
|
245
|
-
const dbContainerName = builtinDbContainerName(runtime);
|
|
246
|
-
if (dbContainerName) {
|
|
247
|
-
startTask(`Removing built-in database container for "${runtime.envName}"...`);
|
|
248
|
-
const state = await removeDockerContainerIfExists(dbContainerName);
|
|
249
|
-
succeedTask(state === 'removed'
|
|
250
|
-
? `Built-in database container removed for "${runtime.envName}".`
|
|
251
|
-
: `No built-in database container found for "${runtime.envName}".`);
|
|
252
|
-
}
|
|
253
|
-
const networkName = managedDockerNetworkName(runtime);
|
|
254
|
-
if (networkName) {
|
|
255
|
-
startTask(`Removing Docker network for "${runtime.envName}" if unused...`);
|
|
256
|
-
const state = await removeDockerNetworkIfUnused(networkName);
|
|
257
|
-
if (state === 'removed') {
|
|
258
|
-
succeedTask(`Docker network removed for "${runtime.envName}".`);
|
|
259
|
-
}
|
|
260
|
-
else if (state === 'missing') {
|
|
261
|
-
succeedTask(`No Docker network found for "${runtime.envName}".`);
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
succeedTask(`Docker network is still in use for "${runtime.envName}". Keeping it.`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
if (runtime.kind === 'local') {
|
|
269
|
-
const localAppPath = resolveConfiguredPath(runtime.env.config.appRootPath) || runtime.projectRoot;
|
|
270
|
-
if (localAppPath) {
|
|
271
|
-
startTask(`Removing local app files for "${runtime.envName}"...`);
|
|
272
|
-
await removePathIfExists(localAppPath, `app files for "${runtime.envName}"`);
|
|
273
|
-
succeedTask(`Local app files removed for "${runtime.envName}".`);
|
|
274
|
-
}
|
|
275
|
-
else {
|
|
276
|
-
printInfo(`No saved local app path found for "${runtime.envName}".`);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
if (removeData) {
|
|
280
|
-
const configuredStoragePath = resolveConfiguredPath(runtime.env.config.storagePath);
|
|
281
|
-
if (configuredStoragePath) {
|
|
282
|
-
startTask(`Removing storage data for "${runtime.envName}"...`);
|
|
283
|
-
await removePathIfExists(configuredStoragePath, `storage data for "${runtime.envName}"`);
|
|
284
|
-
succeedTask(`Storage data removed for "${runtime.envName}".`);
|
|
285
|
-
}
|
|
286
|
-
else {
|
|
287
|
-
printInfo(`No saved storage path found for "${runtime.envName}".`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
if (removeEnvConfig) {
|
|
291
|
-
startTask(`Removing saved CLI env config for "${runtime.envName}"...`);
|
|
292
|
-
const result = await removeEnv(runtime.envName);
|
|
293
|
-
succeedTask(`Saved CLI env config removed for "${runtime.envName}"${result.lastEnv ? ` (last env: ${result.lastEnv})` : ''}.`);
|
|
294
|
-
}
|
|
61
|
+
printWarning('`nb app down` is deprecated. Use `nb app stop --with-db` instead.');
|
|
62
|
+
argv.push('--with-db');
|
|
63
|
+
if (flags.yes) {
|
|
64
|
+
argv.push('--yes');
|
|
295
65
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
this.error(formatDownFailure(runtime.envName, error instanceof Error ? error.message : String(error)));
|
|
66
|
+
if (flags.force) {
|
|
67
|
+
argv.push('--force');
|
|
299
68
|
}
|
|
69
|
+
await runCommand('app:stop', argv);
|
|
300
70
|
}
|
|
301
71
|
}
|
|
@@ -55,7 +55,11 @@ export default class AppRestart extends Command {
|
|
|
55
55
|
default: false,
|
|
56
56
|
}),
|
|
57
57
|
quickstart: Flags.boolean({ description: 'Quickstart the application after stopping it', required: false }),
|
|
58
|
-
port: Flags.string({
|
|
58
|
+
port: Flags.string({
|
|
59
|
+
description: 'Port (overrides appPort from env config when set)',
|
|
60
|
+
char: 'p',
|
|
61
|
+
required: false,
|
|
62
|
+
}),
|
|
59
63
|
daemon: Flags.boolean({
|
|
60
64
|
description: 'Run the application as a daemon after stopping it (default: true; use --no-daemon to stay in the foreground)',
|
|
61
65
|
char: 'd',
|
|
@@ -63,7 +67,11 @@ export default class AppRestart extends Command {
|
|
|
63
67
|
default: true,
|
|
64
68
|
allowNo: true,
|
|
65
69
|
}),
|
|
66
|
-
instances: Flags.integer({
|
|
70
|
+
instances: Flags.integer({
|
|
71
|
+
description: 'Number of instances to run after stopping it',
|
|
72
|
+
char: 'i',
|
|
73
|
+
required: false,
|
|
74
|
+
}),
|
|
67
75
|
'launch-mode': Flags.string({ description: 'Launch Mode', required: false, options: ['pm2', 'node'] }),
|
|
68
76
|
verbose: Flags.boolean({
|
|
69
77
|
description: 'Show raw shutdown/startup output from the underlying local or Docker command',
|
|
@@ -121,14 +129,13 @@ export default class AppRestart extends Command {
|
|
|
121
129
|
failTask(`Failed to recreate NocoBase for "${runtime.envName}".`);
|
|
122
130
|
this.error(formatDockerRestartFailure(runtime.envName, message));
|
|
123
131
|
}
|
|
124
|
-
const appUrl = formatAppUrl(runtime.env.appPort === undefined || runtime.env.appPort === null
|
|
125
|
-
? undefined
|
|
126
|
-
: String(runtime.env.appPort));
|
|
132
|
+
const appUrl = formatAppUrl(runtime.env.appPort === undefined || runtime.env.appPort === null ? undefined : String(runtime.env.appPort));
|
|
127
133
|
await waitForAppReady({
|
|
128
134
|
envName: runtime.envName,
|
|
129
135
|
apiBaseUrl: resolveManagedAppApiBaseUrl(runtime),
|
|
130
136
|
containerName: runtime.containerName,
|
|
131
137
|
logHint: `You can inspect startup logs with \`nb app logs --env ${runtime.envName}\`.`,
|
|
138
|
+
...(flags.verbose ? { verbose: true } : {}),
|
|
132
139
|
});
|
|
133
140
|
succeedTask(`NocoBase is running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
|
|
134
141
|
return;
|
|
@@ -0,0 +1,122 @@
|
|
|
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 fsp from 'node:fs/promises';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { buildDockerDbContainerName } from '../../lib/app-runtime.js';
|
|
13
|
+
import { resolveConfiguredEnvPath } from '../../lib/cli-home.js';
|
|
14
|
+
import { commandOutput, run } from '../../lib/run-npm.js';
|
|
15
|
+
export function resolveConfiguredPath(value) {
|
|
16
|
+
return resolveConfiguredEnvPath(value);
|
|
17
|
+
}
|
|
18
|
+
function assertSafeRemovalPath(target, label) {
|
|
19
|
+
const resolved = path.resolve(target);
|
|
20
|
+
const cwd = path.resolve(process.cwd());
|
|
21
|
+
const home = path.resolve(os.homedir());
|
|
22
|
+
const root = path.parse(resolved).root;
|
|
23
|
+
if (resolved === root || resolved === cwd || resolved === home) {
|
|
24
|
+
throw new Error(`Refusing to remove ${label} at "${resolved}" because it is too broad.`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function removePathIfExists(target, label) {
|
|
28
|
+
const resolved = path.resolve(target);
|
|
29
|
+
assertSafeRemovalPath(resolved, label);
|
|
30
|
+
await fsp.rm(resolved, { recursive: true, force: true });
|
|
31
|
+
}
|
|
32
|
+
function isMissingDockerContainerError(error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
return /No such (container|object)/i.test(message);
|
|
35
|
+
}
|
|
36
|
+
function isMissingDockerNetworkError(error) {
|
|
37
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
38
|
+
return /No such network|network .* not found/i.test(message);
|
|
39
|
+
}
|
|
40
|
+
export async function removeDockerContainerIfExists(containerName, options) {
|
|
41
|
+
try {
|
|
42
|
+
await commandOutput('docker', ['container', 'inspect', containerName], {
|
|
43
|
+
errorName: 'docker container inspect',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (isMissingDockerContainerError(error)) {
|
|
48
|
+
return 'missing';
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
await run('docker', ['rm', '-f', containerName], {
|
|
53
|
+
errorName: 'docker rm',
|
|
54
|
+
stdio: options?.stdio ?? 'ignore',
|
|
55
|
+
});
|
|
56
|
+
return 'removed';
|
|
57
|
+
}
|
|
58
|
+
async function dockerNetworkExists(networkName) {
|
|
59
|
+
try {
|
|
60
|
+
await commandOutput('docker', ['network', 'inspect', networkName], {
|
|
61
|
+
errorName: 'docker network inspect',
|
|
62
|
+
});
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (isMissingDockerNetworkError(error)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function dockerNetworkHasActiveEndpoints(networkName) {
|
|
73
|
+
try {
|
|
74
|
+
const output = await commandOutput('docker', ['network', 'inspect', networkName, '--format', '{{json .Containers}}'], {
|
|
75
|
+
errorName: 'docker network inspect',
|
|
76
|
+
});
|
|
77
|
+
const containers = JSON.parse(output || '{}');
|
|
78
|
+
return Boolean(containers && typeof containers === 'object' && Object.keys(containers).length > 0);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export async function removeDockerNetworkIfUnused(networkName) {
|
|
85
|
+
if (!(await dockerNetworkExists(networkName))) {
|
|
86
|
+
return 'missing';
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
await run('docker', ['network', 'rm', networkName], {
|
|
90
|
+
errorName: 'docker network rm',
|
|
91
|
+
stdio: 'ignore',
|
|
92
|
+
});
|
|
93
|
+
return 'removed';
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
97
|
+
if (/has active endpoints|is in use|active endpoints/i.test(message) ||
|
|
98
|
+
((await dockerNetworkExists(networkName)) && (await dockerNetworkHasActiveEndpoints(networkName)))) {
|
|
99
|
+
return 'in-use';
|
|
100
|
+
}
|
|
101
|
+
if (isMissingDockerNetworkError(error)) {
|
|
102
|
+
return 'missing';
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function builtinDbContainerName(runtime) {
|
|
108
|
+
if (!runtime.env.config.builtinDb) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const dbDialect = String(runtime.env.config.dbDialect ?? 'postgres').trim() || 'postgres';
|
|
112
|
+
return buildDockerDbContainerName(runtime.envName, dbDialect, runtime.dockerContainerPrefix || runtime.workspaceName);
|
|
113
|
+
}
|
|
114
|
+
export function managedDockerNetworkName(runtime) {
|
|
115
|
+
return runtime.dockerNetworkName?.trim() || runtime.workspaceName?.trim() || undefined;
|
|
116
|
+
}
|
|
117
|
+
export function resolveManagedLocalAppPath(runtime) {
|
|
118
|
+
return resolveConfiguredPath(runtime.env.config.appRootPath) || runtime.projectRoot;
|
|
119
|
+
}
|
|
120
|
+
export function shouldRemoveManagedLocalAppFiles(runtime) {
|
|
121
|
+
return runtime.source === 'npm' || runtime.source === 'git';
|
|
122
|
+
}
|
|
@@ -8,11 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Command, Flags } from '@oclif/core';
|
|
10
10
|
import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
|
|
11
|
-
import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand,
|
|
11
|
+
import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
|
|
12
12
|
import { AppHealthCheckError, formatAppUrl, isAppReady, resolveManagedAppApiBaseUrl, waitForAppReady, } from '../../lib/app-health.js';
|
|
13
13
|
import { ensureBuiltinDbReady, ensureLocalPostinstall, ensureSavedLocalSource, recreateSavedDockerApp, } from '../../lib/app-managed-resources.js';
|
|
14
14
|
import { run } from '../../lib/run-npm.js';
|
|
15
15
|
import { announceTargetEnv, failTask, printInfo, startTask, succeedTask } from '../../lib/ui.js';
|
|
16
|
+
function shouldPrintStartSuccess() {
|
|
17
|
+
return process.env.NB_SKIP_APP_START_SUCCESS_LOG !== '1';
|
|
18
|
+
}
|
|
16
19
|
function argvHasToken(argv, tokens) {
|
|
17
20
|
return tokens.some((token) => argv.includes(token));
|
|
18
21
|
}
|
|
@@ -52,7 +55,7 @@ function formatLocalReadyFailure(envName, message, options) {
|
|
|
52
55
|
}
|
|
53
56
|
export default class AppStart extends Command {
|
|
54
57
|
static hidden = false;
|
|
55
|
-
static description = 'Start NocoBase for the selected env. Local npm/git installs run the app command, and Docker installs
|
|
58
|
+
static description = 'Start NocoBase for the selected env. Local npm/git installs run the app command, and Docker installs recreate the saved app container.';
|
|
56
59
|
static examples = [
|
|
57
60
|
'<%= config.bin %> <%= command.id %>',
|
|
58
61
|
'<%= config.bin %> <%= command.id %> --env local',
|
|
@@ -76,7 +79,11 @@ export default class AppStart extends Command {
|
|
|
76
79
|
default: false,
|
|
77
80
|
}),
|
|
78
81
|
quickstart: Flags.boolean({ description: 'Quickstart the application', required: false }),
|
|
79
|
-
port: Flags.string({
|
|
82
|
+
port: Flags.string({
|
|
83
|
+
description: 'Port (overrides appPort from env config when set)',
|
|
84
|
+
char: 'p',
|
|
85
|
+
required: false,
|
|
86
|
+
}),
|
|
80
87
|
daemon: Flags.boolean({
|
|
81
88
|
description: 'Run the application as a daemon (default: true; use --no-daemon to stay in the foreground)',
|
|
82
89
|
char: 'd',
|
|
@@ -90,10 +97,6 @@ export default class AppStart extends Command {
|
|
|
90
97
|
description: 'Show raw startup output from the underlying local or Docker command',
|
|
91
98
|
default: false,
|
|
92
99
|
}),
|
|
93
|
-
recreate: Flags.boolean({
|
|
94
|
-
description: 'Recreate the saved Docker app container before starting it',
|
|
95
|
-
default: false,
|
|
96
|
-
}),
|
|
97
100
|
};
|
|
98
101
|
async run() {
|
|
99
102
|
const { flags } = await this.parse(AppStart);
|
|
@@ -131,7 +134,6 @@ export default class AppStart extends Command {
|
|
|
131
134
|
announceTargetEnv(runtime.envName);
|
|
132
135
|
if (runtime.kind === 'docker') {
|
|
133
136
|
const unsupportedFlags = [
|
|
134
|
-
flags.quickstart ? '--quickstart' : undefined,
|
|
135
137
|
flags.port ? '--port' : undefined,
|
|
136
138
|
daemonFlagWasProvided ? (flags.daemon ? '--daemon' : '--no-daemon') : undefined,
|
|
137
139
|
flags.instances !== undefined ? '--instances' : undefined,
|
|
@@ -141,7 +143,7 @@ export default class AppStart extends Command {
|
|
|
141
143
|
this.error([
|
|
142
144
|
`Can't apply ${unsupportedFlags.join(', ')} to "${runtime.envName}".`,
|
|
143
145
|
'This env is managed by Docker, so those options are only available for local npm/git installs.',
|
|
144
|
-
`Run \`nb app start --env ${runtime.envName}\` to
|
|
146
|
+
`Run \`nb app start --env ${runtime.envName}\` to recreate the saved container, or recreate the env if you need different runtime settings.`,
|
|
145
147
|
].join('\n'));
|
|
146
148
|
}
|
|
147
149
|
await ensureBuiltinDbReady(runtime, {
|
|
@@ -150,60 +152,34 @@ export default class AppStart extends Command {
|
|
|
150
152
|
onSucceedTask: succeedTask,
|
|
151
153
|
onFailTask: failTask,
|
|
152
154
|
});
|
|
153
|
-
const appUrl = formatAppUrl(runtime.env.appPort === undefined || runtime.env.appPort === null
|
|
154
|
-
? undefined
|
|
155
|
-
: String(runtime.env.appPort));
|
|
155
|
+
const appUrl = formatAppUrl(runtime.env.appPort === undefined || runtime.env.appPort === null ? undefined : String(runtime.env.appPort));
|
|
156
156
|
const apiBaseUrl = resolveManagedAppApiBaseUrl(runtime);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
succeedTask(`Docker app container is ready for "${runtime.envName}".`);
|
|
168
|
-
}
|
|
169
|
-
catch (error) {
|
|
170
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
171
|
-
failTask(`Failed to recreate NocoBase for "${runtime.envName}".`);
|
|
172
|
-
this.error(formatDockerStartFailure(runtime.envName, message));
|
|
173
|
-
}
|
|
157
|
+
startTask(`Recreating the Docker app container for "${runtime.envName}"...`);
|
|
158
|
+
try {
|
|
159
|
+
await run('docker', ['rm', '-f', runtime.containerName], {
|
|
160
|
+
errorName: 'docker rm',
|
|
161
|
+
stdio: commandStdio,
|
|
162
|
+
}).catch(() => undefined);
|
|
163
|
+
await recreateSavedDockerApp(runtime, {
|
|
164
|
+
verbose: flags.verbose,
|
|
165
|
+
});
|
|
166
|
+
succeedTask(`Docker app container is ready for "${runtime.envName}".`);
|
|
174
167
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
stdio: commandStdio,
|
|
180
|
-
});
|
|
181
|
-
if (state === 'already-running' && await isAppReady(apiBaseUrl)) {
|
|
182
|
-
succeedTask(`NocoBase is already running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
188
|
-
if (/does not exist/i.test(message)) {
|
|
189
|
-
printInfo(`The saved Docker app container for "${runtime.envName}" is missing. Recreating it from the saved Docker env settings...`);
|
|
190
|
-
await recreateSavedDockerApp(runtime, {
|
|
191
|
-
verbose: flags.verbose,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
failTask(`Failed to start NocoBase for "${runtime.envName}".`);
|
|
196
|
-
this.error(formatDockerStartFailure(runtime.envName, message));
|
|
197
|
-
}
|
|
198
|
-
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
170
|
+
failTask(`Failed to recreate NocoBase for "${runtime.envName}".`);
|
|
171
|
+
this.error(formatDockerStartFailure(runtime.envName, message));
|
|
199
172
|
}
|
|
200
173
|
await waitForAppReady({
|
|
201
174
|
envName: runtime.envName,
|
|
202
175
|
apiBaseUrl,
|
|
203
176
|
containerName: runtime.containerName,
|
|
204
177
|
logHint: `You can inspect startup logs with \`nb app logs --env ${runtime.envName}\`.`,
|
|
178
|
+
...(flags.verbose ? { verbose: true } : {}),
|
|
205
179
|
});
|
|
206
|
-
|
|
180
|
+
if (shouldPrintStartSuccess()) {
|
|
181
|
+
succeedTask(`NocoBase is running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
|
|
182
|
+
}
|
|
207
183
|
return;
|
|
208
184
|
}
|
|
209
185
|
await ensureBuiltinDbReady(runtime, {
|
|
@@ -229,7 +205,9 @@ export default class AppStart extends Command {
|
|
|
229
205
|
if (flags.port) {
|
|
230
206
|
npmArgs.push('--port', flags.port);
|
|
231
207
|
}
|
|
232
|
-
else if (runtime.env.appPort !== undefined &&
|
|
208
|
+
else if (runtime.env.appPort !== undefined &&
|
|
209
|
+
runtime.env.appPort !== null &&
|
|
210
|
+
String(runtime.env.appPort).trim() !== '') {
|
|
233
211
|
npmArgs.push('--port', String(runtime.env.appPort));
|
|
234
212
|
}
|
|
235
213
|
if (flags.daemon !== false) {
|
|
@@ -241,8 +219,8 @@ export default class AppStart extends Command {
|
|
|
241
219
|
if (flags['launch-mode']) {
|
|
242
220
|
npmArgs.push('--launch-mode', flags['launch-mode']);
|
|
243
221
|
}
|
|
244
|
-
const effectivePort = flags.port
|
|
245
|
-
|
|
222
|
+
const effectivePort = flags.port ||
|
|
223
|
+
(runtime.env.appPort !== undefined && runtime.env.appPort !== null
|
|
246
224
|
? String(runtime.env.appPort).trim()
|
|
247
225
|
: undefined);
|
|
248
226
|
const appUrl = formatAppUrl(effectivePort);
|
|
@@ -254,7 +232,9 @@ export default class AppStart extends Command {
|
|
|
254
232
|
printInfo(`NocoBase is already running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}. Use \`nb app stop --env ${runtime.envName}\` before starting it again in the foreground.`);
|
|
255
233
|
}
|
|
256
234
|
else {
|
|
257
|
-
|
|
235
|
+
if (shouldPrintStartSuccess()) {
|
|
236
|
+
succeedTask(`NocoBase is already running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
|
|
237
|
+
}
|
|
258
238
|
}
|
|
259
239
|
return;
|
|
260
240
|
}
|
|
@@ -285,7 +265,9 @@ export default class AppStart extends Command {
|
|
|
285
265
|
apiBaseUrl,
|
|
286
266
|
logHint: `You can inspect startup logs with \`nb app logs --env ${runtime.envName}\`.`,
|
|
287
267
|
});
|
|
288
|
-
|
|
268
|
+
if (shouldPrintStartSuccess()) {
|
|
269
|
+
succeedTask(`NocoBase is running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
|
|
270
|
+
}
|
|
289
271
|
}
|
|
290
272
|
}
|
|
291
273
|
catch (error) {
|