@nocobase/cli 2.1.0-beta.37 → 2.1.0-beta.38
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 +19 -7
- package/README.zh-CN.md +19 -7
- package/dist/commands/app/destroy.js +225 -0
- package/dist/commands/app/down.js +24 -254
- package/dist/commands/app/shared.js +122 -0
- package/dist/commands/app/start.js +40 -59
- package/dist/commands/app/stop.js +72 -26
- package/dist/commands/app/upgrade.js +310 -427
- package/dist/commands/db/start.js +23 -8
- package/dist/commands/license/plugins/shared.js +9 -3
- package/dist/commands/license/plugins/sync.js +54 -25
- 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 +27 -21
- package/dist/lib/env-guard.js +1 -1
- package/dist/lib/generated-command.js +17 -0
- package/dist/lib/skills-manager.js +6 -0
- package/dist/lib/ui.js +4 -1
- package/dist/locale/en-US.json +51 -1
- package/dist/locale/zh-CN.json +51 -1
- package/package.json +34 -2
package/README.md
CHANGED
|
@@ -265,28 +265,40 @@ nb db logs --env app1
|
|
|
265
265
|
|
|
266
266
|
Notes:
|
|
267
267
|
|
|
268
|
+
- `nb db start` can also recreate the saved built-in database container when it has been removed.
|
|
268
269
|
- `nb db start` and `nb db stop` only work for envs created with the built-in database option enabled.
|
|
269
270
|
- `nb db logs` only works for envs created with the built-in database option enabled.
|
|
270
271
|
- `nb db ps` can also show `external` or `remote` status for envs that do not have a CLI-managed database container.
|
|
271
272
|
|
|
272
273
|
## Cleanup
|
|
273
274
|
|
|
274
|
-
|
|
275
|
+
Stop only the app runtime:
|
|
275
276
|
|
|
276
277
|
```bash
|
|
277
|
-
nb app
|
|
278
|
+
nb app stop --env app1
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Stop the app runtime and also remove the CLI-managed built-in database runtime when present:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
nb app stop --env app1 --with-db
|
|
278
285
|
```
|
|
279
286
|
|
|
280
|
-
|
|
281
|
-
|
|
287
|
+
- `nb app stop` keeps storage data and the saved CLI env config.
|
|
288
|
+
- Docker envs remove the saved app container when stopped.
|
|
289
|
+
- `--with-db` only affects CLI-managed built-in databases. External databases are not touched.
|
|
282
290
|
|
|
283
|
-
|
|
291
|
+
Destroy the env's managed local resources:
|
|
284
292
|
|
|
285
293
|
```bash
|
|
286
|
-
nb app
|
|
294
|
+
nb app destroy --env app1
|
|
295
|
+
nb app destroy --env app1 --force
|
|
287
296
|
```
|
|
288
297
|
|
|
289
|
-
-
|
|
298
|
+
- `nb app destroy` removes managed runtime resources, storage data, and the saved CLI env config.
|
|
299
|
+
- For downloaded npm/Git envs, `nb app destroy` also removes the saved local app files. Custom local app directories are kept.
|
|
300
|
+
- In interactive terminals, `nb app destroy` requires a strong confirmation prompt. In non-interactive mode, re-run with `--env <name> --force`.
|
|
301
|
+
- `nb app down` is deprecated. Use `nb app stop --with-db` for runtime cleanup, or `nb app destroy` for destructive cleanup.
|
|
290
302
|
|
|
291
303
|
## Environment Management
|
|
292
304
|
|
package/README.zh-CN.md
CHANGED
|
@@ -225,28 +225,40 @@ nb db logs --env app1
|
|
|
225
225
|
|
|
226
226
|
说明:
|
|
227
227
|
|
|
228
|
+
- `nb db start` 在内置数据库容器已被删除时,也可以根据已保存的 env 配置自动恢复它。
|
|
228
229
|
- `nb db start` 和 `nb db stop` 只适用于启用了内置数据库的 env。
|
|
229
230
|
- `nb db logs` 只适用于启用了内置数据库的 env。
|
|
230
231
|
- 对于没有 CLI 托管数据库容器的 env,`nb db ps` 也会显示 `external` 或 `remote` 状态。
|
|
231
232
|
|
|
232
233
|
## 清理
|
|
233
234
|
|
|
234
|
-
|
|
235
|
+
只停止应用运行态:
|
|
235
236
|
|
|
236
237
|
```bash
|
|
237
|
-
nb app
|
|
238
|
+
nb app stop --env app1
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
如果还要一并移除 CLI 托管的内置数据库运行态,可以使用:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
nb app stop --env app1 --with-db
|
|
238
245
|
```
|
|
239
246
|
|
|
240
|
-
|
|
241
|
-
|
|
247
|
+
- `nb app stop` 会保留 storage 数据和已保存的 CLI env 配置。
|
|
248
|
+
- Docker env 停止时会移除已保存的 app container。
|
|
249
|
+
- `--with-db` 只会影响 CLI 托管的内置数据库,external db 不会被处理。
|
|
242
250
|
|
|
243
|
-
|
|
251
|
+
销毁该 env 的本地托管资源:
|
|
244
252
|
|
|
245
253
|
```bash
|
|
246
|
-
nb app
|
|
254
|
+
nb app destroy --env app1
|
|
255
|
+
nb app destroy --env app1 --force
|
|
247
256
|
```
|
|
248
257
|
|
|
249
|
-
-
|
|
258
|
+
- `nb app destroy` 会删除托管的运行时资源、storage 数据以及已保存的 CLI env 配置。
|
|
259
|
+
- 对于通过 npm/Git 下载的 env,`nb app destroy` 还会删除已保存的本地 app 文件。自定义的本地源码目录会被保留。
|
|
260
|
+
- 在交互终端中,`nb app destroy` 需要强确认;在非交互模式下,需要显式传入 `--env <name> --force`。
|
|
261
|
+
- `nb app down` 已废弃。运行态清理请使用 `nb app stop --with-db`,彻底销毁请使用 `nb app destroy`。
|
|
250
262
|
|
|
251
263
|
## Env 管理
|
|
252
264
|
|
|
@@ -0,0 +1,225 @@
|
|
|
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 { removeEnv } from '../../lib/auth-store.js';
|
|
11
|
+
import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
|
|
12
|
+
import { hasExplicitEnvSelection } from '../../lib/env-guard.js';
|
|
13
|
+
import { input } from "../../lib/inquirer.js";
|
|
14
|
+
import { announceTargetEnv, failTask, isInteractiveTerminal, printInfo, startTask, succeedTask } from '../../lib/ui.js';
|
|
15
|
+
import { builtinDbContainerName, managedDockerNetworkName, removeDockerContainerIfExists, removeDockerNetworkIfUnused, removePathIfExists, resolveConfiguredPath, resolveManagedLocalAppPath, shouldRemoveManagedLocalAppFiles, } from './shared.js';
|
|
16
|
+
function formatDestroyFailure(envName, message) {
|
|
17
|
+
return [
|
|
18
|
+
`Couldn't destroy env "${envName}".`,
|
|
19
|
+
'Some managed local resources may still exist. Check Docker, local app files, and storage data, then try again.',
|
|
20
|
+
`Details: ${message}`,
|
|
21
|
+
].join('\n');
|
|
22
|
+
}
|
|
23
|
+
function formatDestroyForceRequiredMessage(envName, explicitEnv) {
|
|
24
|
+
if (!explicitEnv) {
|
|
25
|
+
return [
|
|
26
|
+
`Refusing to destroy current env "${envName}" without explicit selection in non-interactive mode.`,
|
|
27
|
+
`Re-run with \`--env ${envName} --force\` to destroy this env.`,
|
|
28
|
+
].join('\n');
|
|
29
|
+
}
|
|
30
|
+
return [
|
|
31
|
+
`Refusing to destroy env "${envName}" without confirmation in non-interactive mode.`,
|
|
32
|
+
`Re-run with \`--env ${envName} --force\` to destroy this env.`,
|
|
33
|
+
].join('\n');
|
|
34
|
+
}
|
|
35
|
+
function buildDestroyPrompt(runtime, options) {
|
|
36
|
+
const subject = options.explicitEnv ? `env "${runtime.envName}"` : `current env "${runtime.envName}"`;
|
|
37
|
+
const lines = [`Destroy ${subject}?`];
|
|
38
|
+
if (runtime.kind === 'http' || runtime.kind === 'ssh') {
|
|
39
|
+
lines.push('This env has no CLI-managed local app or database runtime on this machine.');
|
|
40
|
+
lines.push('Only the saved CLI env config will be removed. External services are not touched.');
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
lines.push('This removes managed app runtime resources.');
|
|
44
|
+
if (runtime.env.config.builtinDb) {
|
|
45
|
+
lines.push('CLI-managed built-in database runtime will also be removed.');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
lines.push('External database resources are not managed by the CLI and will be left untouched.');
|
|
49
|
+
}
|
|
50
|
+
if (options.removesManagedLocalAppFiles) {
|
|
51
|
+
lines.push('CLI-managed local app files will also be removed.');
|
|
52
|
+
}
|
|
53
|
+
else if (runtime.kind === 'local') {
|
|
54
|
+
lines.push('Custom local app source files will be kept.');
|
|
55
|
+
}
|
|
56
|
+
if (options.removesStorageData) {
|
|
57
|
+
lines.push('Storage data will be removed.');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
lines.push('No saved storage path was found for this env.');
|
|
61
|
+
}
|
|
62
|
+
lines.push('The saved CLI env config will be removed.');
|
|
63
|
+
}
|
|
64
|
+
lines.push(`Type "${runtime.envName}" to confirm:`);
|
|
65
|
+
return lines.join('\n');
|
|
66
|
+
}
|
|
67
|
+
async function confirmDestroy(runtime, options) {
|
|
68
|
+
if (!isInteractiveTerminal()) {
|
|
69
|
+
if (!options.explicitEnv) {
|
|
70
|
+
throw new Error(formatDestroyForceRequiredMessage(runtime.envName, false));
|
|
71
|
+
}
|
|
72
|
+
if (options.force) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
throw new Error(formatDestroyForceRequiredMessage(runtime.envName, options.explicitEnv));
|
|
76
|
+
}
|
|
77
|
+
if (options.force) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
await input({
|
|
82
|
+
message: buildDestroyPrompt(runtime, options),
|
|
83
|
+
required: true,
|
|
84
|
+
validate: (value) => (value.trim() === runtime.envName ? true : `Type "${runtime.envName}" to confirm.`),
|
|
85
|
+
placeholder: runtime.envName,
|
|
86
|
+
});
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export default class AppDestroy extends Command {
|
|
94
|
+
static hidden = false;
|
|
95
|
+
static description = 'Destroy the selected env by removing managed runtime resources, storage data, and the saved CLI env config.';
|
|
96
|
+
static examples = [
|
|
97
|
+
'<%= config.bin %> <%= command.id %> --env app1',
|
|
98
|
+
'<%= config.bin %> <%= command.id %> --env app1 --force',
|
|
99
|
+
];
|
|
100
|
+
static flags = {
|
|
101
|
+
env: Flags.string({
|
|
102
|
+
char: 'e',
|
|
103
|
+
description: 'CLI env name to destroy. Defaults to the current env when omitted in interactive mode',
|
|
104
|
+
}),
|
|
105
|
+
force: Flags.boolean({
|
|
106
|
+
char: 'f',
|
|
107
|
+
description: 'Skip confirmation and destroy the selected env immediately',
|
|
108
|
+
default: false,
|
|
109
|
+
}),
|
|
110
|
+
verbose: Flags.boolean({
|
|
111
|
+
description: 'Show raw output from destruction commands',
|
|
112
|
+
default: false,
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
async run() {
|
|
116
|
+
const { flags } = await this.parse(AppDestroy);
|
|
117
|
+
const requestedEnv = flags.env?.trim() || undefined;
|
|
118
|
+
const explicitEnv = Boolean(requestedEnv && hasExplicitEnvSelection(this.argv));
|
|
119
|
+
const runtime = await resolveManagedAppRuntime(requestedEnv);
|
|
120
|
+
if (!runtime) {
|
|
121
|
+
this.error(formatMissingManagedAppEnvMessage(requestedEnv));
|
|
122
|
+
}
|
|
123
|
+
const removesManagedLocalAppFiles = runtime.kind === 'local' &&
|
|
124
|
+
Boolean(resolveManagedLocalAppPath(runtime)) &&
|
|
125
|
+
shouldRemoveManagedLocalAppFiles(runtime);
|
|
126
|
+
const removesStorageData = (runtime.kind === 'local' || runtime.kind === 'docker') &&
|
|
127
|
+
Boolean(resolveConfiguredPath(runtime.env.config.storagePath));
|
|
128
|
+
let confirmed = false;
|
|
129
|
+
try {
|
|
130
|
+
confirmed = await confirmDestroy(runtime, {
|
|
131
|
+
explicitEnv,
|
|
132
|
+
force: flags.force,
|
|
133
|
+
removesManagedLocalAppFiles,
|
|
134
|
+
removesStorageData,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
this.error(error instanceof Error ? error.message : String(error));
|
|
139
|
+
}
|
|
140
|
+
if (!confirmed) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
announceTargetEnv(runtime.envName);
|
|
144
|
+
try {
|
|
145
|
+
if (runtime.kind === 'docker') {
|
|
146
|
+
startTask(`Removing Docker app container for "${runtime.envName}"...`);
|
|
147
|
+
const state = await removeDockerContainerIfExists(runtime.containerName, {
|
|
148
|
+
stdio: flags.verbose ? 'inherit' : 'ignore',
|
|
149
|
+
});
|
|
150
|
+
succeedTask(state === 'removed'
|
|
151
|
+
? `Docker app container removed for "${runtime.envName}".`
|
|
152
|
+
: `No Docker app container found for "${runtime.envName}".`);
|
|
153
|
+
}
|
|
154
|
+
else if (runtime.kind === 'local') {
|
|
155
|
+
startTask(`Stopping local NocoBase app for "${runtime.envName}"...`);
|
|
156
|
+
await runLocalNocoBaseCommand(runtime, ['pm2', 'kill'], {
|
|
157
|
+
stdio: flags.verbose ? 'inherit' : 'ignore',
|
|
158
|
+
});
|
|
159
|
+
succeedTask(`Local NocoBase app stopped for "${runtime.envName}".`);
|
|
160
|
+
}
|
|
161
|
+
if (runtime.kind === 'local' || runtime.kind === 'docker') {
|
|
162
|
+
const dbContainer = builtinDbContainerName(runtime);
|
|
163
|
+
if (dbContainer) {
|
|
164
|
+
startTask(`Removing built-in database container for "${runtime.envName}"...`);
|
|
165
|
+
const state = await removeDockerContainerIfExists(dbContainer, {
|
|
166
|
+
stdio: flags.verbose ? 'inherit' : 'ignore',
|
|
167
|
+
});
|
|
168
|
+
succeedTask(state === 'removed'
|
|
169
|
+
? `Built-in database container removed for "${runtime.envName}".`
|
|
170
|
+
: `No built-in database container found for "${runtime.envName}".`);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
printInfo(`External database resources for "${runtime.envName}" were left untouched.`);
|
|
174
|
+
}
|
|
175
|
+
const networkName = managedDockerNetworkName(runtime);
|
|
176
|
+
if (networkName) {
|
|
177
|
+
startTask(`Removing Docker network for "${runtime.envName}" if unused...`);
|
|
178
|
+
const state = await removeDockerNetworkIfUnused(networkName);
|
|
179
|
+
if (state === 'removed') {
|
|
180
|
+
succeedTask(`Docker network removed for "${runtime.envName}".`);
|
|
181
|
+
}
|
|
182
|
+
else if (state === 'missing') {
|
|
183
|
+
succeedTask(`No Docker network found for "${runtime.envName}".`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
succeedTask(`Docker network is still in use for "${runtime.envName}". Keeping it.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (runtime.kind === 'local') {
|
|
190
|
+
const localAppPath = resolveManagedLocalAppPath(runtime);
|
|
191
|
+
if (localAppPath && removesManagedLocalAppFiles) {
|
|
192
|
+
startTask(`Removing managed local app files for "${runtime.envName}"...`);
|
|
193
|
+
await removePathIfExists(localAppPath, `managed app files for "${runtime.envName}"`);
|
|
194
|
+
succeedTask(`Managed local app files removed for "${runtime.envName}".`);
|
|
195
|
+
}
|
|
196
|
+
else if (localAppPath) {
|
|
197
|
+
printInfo(`Keeping custom local app files for "${runtime.envName}" at "${localAppPath}".`);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
printInfo(`No saved local app path found for "${runtime.envName}".`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const configuredStoragePath = resolveConfiguredPath(runtime.env.config.storagePath);
|
|
204
|
+
if (configuredStoragePath) {
|
|
205
|
+
startTask(`Removing storage data for "${runtime.envName}"...`);
|
|
206
|
+
await removePathIfExists(configuredStoragePath, `storage data for "${runtime.envName}"`);
|
|
207
|
+
succeedTask(`Storage data removed for "${runtime.envName}".`);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
printInfo(`No saved storage path found for "${runtime.envName}".`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
printInfo(`No CLI-managed local app or database runtime exists for "${runtime.envName}" on this machine.`);
|
|
215
|
+
}
|
|
216
|
+
startTask(`Removing saved CLI env config for "${runtime.envName}"...`);
|
|
217
|
+
const result = await removeEnv(runtime.envName);
|
|
218
|
+
succeedTask(`Saved CLI env config removed for "${runtime.envName}"${result.lastEnv ? ` (last env: ${result.lastEnv})` : ''}.`);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
failTask(`Failed to destroy env "${runtime.envName}".`);
|
|
222
|
+
this.error(formatDestroyFailure(runtime.envName, error instanceof Error ? error.message : String(error)));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -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
|
}
|