@nocobase/cli 2.1.0-beta.36 → 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.
Files changed (43) hide show
  1. package/README.md +20 -8
  2. package/README.zh-CN.md +20 -8
  3. package/bin/run.js +3 -2
  4. package/dist/commands/app/destroy.js +225 -0
  5. package/dist/commands/app/down.js +24 -254
  6. package/dist/commands/app/shared.js +122 -0
  7. package/dist/commands/app/start.js +40 -59
  8. package/dist/commands/app/stop.js +72 -26
  9. package/dist/commands/app/upgrade.js +310 -427
  10. package/dist/commands/config/delete.js +4 -0
  11. package/dist/commands/config/get.js +4 -0
  12. package/dist/commands/config/set.js +5 -1
  13. package/dist/commands/db/start.js +23 -8
  14. package/dist/commands/env/add.js +66 -6
  15. package/dist/commands/env/auth.js +86 -27
  16. package/dist/commands/env/info.js +52 -8
  17. package/dist/commands/env/list.js +2 -2
  18. package/dist/commands/env/shared.js +41 -3
  19. package/dist/commands/init.js +196 -136
  20. package/dist/commands/install.js +311 -265
  21. package/dist/commands/license/plugins/shared.js +9 -3
  22. package/dist/commands/license/plugins/sync.js +54 -25
  23. package/dist/commands/source/download.js +29 -25
  24. package/dist/generated/command-registry.js +3 -2
  25. package/dist/lib/api-client.js +6 -0
  26. package/dist/lib/api-command-compat.js +641 -0
  27. package/dist/lib/app-health.js +27 -21
  28. package/dist/lib/auth-store.js +47 -19
  29. package/dist/lib/cli-config.js +99 -4
  30. package/dist/lib/cli-locale.js +19 -7
  31. package/dist/lib/db-connection-check.js +61 -0
  32. package/dist/lib/env-auth.js +79 -0
  33. package/dist/lib/env-config.js +3 -2
  34. package/dist/lib/env-guard.js +1 -1
  35. package/dist/lib/generated-command.js +17 -0
  36. package/dist/lib/prompt-validators.js +23 -5
  37. package/dist/lib/prompt-web-ui.js +143 -19
  38. package/dist/lib/run-npm.js +133 -23
  39. package/dist/lib/skills-manager.js +80 -4
  40. package/dist/lib/ui.js +4 -1
  41. package/dist/locale/en-US.json +86 -5
  42. package/dist/locale/zh-CN.json +86 -5
  43. package/package.json +34 -2
package/README.md CHANGED
@@ -61,7 +61,7 @@ When creating a new app, it can also install NocoBase AI coding skills
61
61
  (`nocobase/skills`) globally.
62
62
 
63
63
  Use `--skip-skills` if the skills are managed separately, or when running in CI
64
- or offline environments where `nb init` should not install or update them.
64
+ or offline environments where `nb init` should not install them.
65
65
 
66
66
  ### Non-Interactive Setup
67
67
 
@@ -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
- Bring down a local env:
275
+ Stop only the app runtime:
275
276
 
276
277
  ```bash
277
- nb app down --env app1
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
- By default, `nb app down` stops the app and removes app/database containers if they
281
- exist. For local envs, it also deletes the saved local app files. It keeps storage data and CLI env config.
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
- Use explicit flags for destructive cleanup:
291
+ Destroy the env's managed local resources:
284
292
 
285
293
  ```bash
286
- nb app down --env app1 --all --yes
294
+ nb app destroy --env app1
295
+ nb app destroy --env app1 --force
287
296
  ```
288
297
 
289
- - `--all`: delete everything for the env, including storage data and the saved env config. This requires `--yes`.
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
@@ -55,7 +55,7 @@ nb init --ui
55
55
 
56
56
  `nb init` 可以连接已有的 NocoBase 应用,也可以安装一个新的 NocoBase 应用。创建新应用时,还可以全局安装 NocoBase AI coding skills (`nocobase/skills`)。
57
57
 
58
- 如果已经自行管理 skills,或在 CI、离线环境中运行,不希望 `nb init` 安装或更新 skills,可以传入 `--skip-skills`。
58
+ 如果已经自行管理 skills,或在 CI、离线环境中运行,不希望 `nb init` 安装 skills,可以传入 `--skip-skills`。
59
59
 
60
60
  ### 非交互式初始化
61
61
 
@@ -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
- 关闭并清理某个本地 env:
235
+ 只停止应用运行态:
235
236
 
236
237
  ```bash
237
- nb app down --env app1
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
- 默认情况下,`nb app down` 会停止应用,并在存在时移除应用容器和数据库容器。它不会删除用户数据、源码文件和 CLI env 配置。
241
- 对于本地 env,它还会删除已保存的本地 app 目录。默认仍会保留 storage 数据和 CLI env 配置。
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 down --env app1 --all --yes
254
+ nb app destroy --env app1
255
+ nb app destroy --env app1 --force
247
256
  ```
248
257
 
249
- - `--all`:删除该 env 的所有内容,包括 storage 数据和已保存的 CLI env 配置。必须同时传 `--yes`。
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
 
package/bin/run.js CHANGED
@@ -4,6 +4,7 @@ import { spawnSync } from 'node:child_process';
4
4
  import fs from 'node:fs';
5
5
  import { createRequire } from 'node:module';
6
6
  import path from 'node:path';
7
+ import pc from 'picocolors';
7
8
  import { fileURLToPath, pathToFileURL } from 'node:url';
8
9
  import { normalizeNodeOptions, normalizeSessionEnv } from './session-env.js';
9
10
 
@@ -130,6 +131,6 @@ try {
130
131
  flush();
131
132
  } catch (error) {
132
133
  const message = formatCliEntryError(error, process.argv.slice(2));
133
- console.error(message);
134
- process.exitCode = 1;
134
+ console.error(pc.red(message));
135
+ process.exit(1);
135
136
  }
@@ -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 fsp from 'node:fs/promises';
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 = false;
144
- static description = 'Bring down the selected env by removing runtime containers and the saved local app files. Storage data and env config are kept unless explicitly requested.';
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: 'Confirm using --env when it targets a different env than the current env',
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: 'Force a one-off cross-env operation when --env targets a different env in non-interactive mode',
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 requestedEnv = flags.env?.trim() || undefined;
177
- if (requestedEnv && hasExplicitEnvSelection(this.argv)) {
178
- if (!isInteractiveTerminal()) {
179
- const currentEnv = await getCurrentEnvName();
180
- const normalizedCurrentEnv = String(currentEnv ?? '').trim() || undefined;
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
- const explicitEnv = Boolean(requestedEnv);
197
- const removeData = Boolean(flags.all);
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
- let confirmed = false;
219
- try {
220
- confirmed = await confirmDownAll(runtime.envName, flags.force, { explicitEnv });
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
- try {
230
- if (runtime.kind === 'docker') {
231
- startTask(`Removing Docker app container for "${runtime.envName}"...`);
232
- const state = await removeDockerContainerIfExists(runtime.containerName);
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
- catch (error) {
297
- failTask(`Failed to bring down NocoBase for "${runtime.envName}".`);
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
  }