@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.
Files changed (56) hide show
  1. package/README.md +20 -8
  2. package/README.zh-CN.md +20 -8
  3. package/bin/run.js +9 -37
  4. package/dist/commands/app/destroy.js +225 -0
  5. package/dist/commands/app/down.js +24 -254
  6. package/dist/commands/app/restart.js +12 -5
  7. package/dist/commands/app/shared.js +122 -0
  8. package/dist/commands/app/start.js +41 -59
  9. package/dist/commands/app/stop.js +72 -26
  10. package/dist/commands/app/upgrade.js +312 -429
  11. package/dist/commands/config/delete.js +8 -1
  12. package/dist/commands/config/get.js +5 -0
  13. package/dist/commands/config/set.js +10 -1
  14. package/dist/commands/db/ps.js +5 -18
  15. package/dist/commands/db/start.js +23 -8
  16. package/dist/commands/env/add.js +66 -6
  17. package/dist/commands/env/auth.js +86 -27
  18. package/dist/commands/env/info.js +52 -8
  19. package/dist/commands/env/list.js +6 -7
  20. package/dist/commands/env/shared.js +41 -3
  21. package/dist/commands/env/status.js +10 -7
  22. package/dist/commands/init.js +214 -136
  23. package/dist/commands/install.js +364 -294
  24. package/dist/commands/license/plugins/shared.js +9 -3
  25. package/dist/commands/license/plugins/sync.js +54 -25
  26. package/dist/commands/self/update.js +58 -1
  27. package/dist/commands/source/dev.js +5 -5
  28. package/dist/commands/source/download.js +29 -25
  29. package/dist/generated/command-registry.js +3 -2
  30. package/dist/lib/api-client.js +6 -0
  31. package/dist/lib/api-command-compat.js +641 -0
  32. package/dist/lib/app-health.js +31 -18
  33. package/dist/lib/app-managed-resources.js +1 -1
  34. package/dist/lib/app-runtime.js +2 -2
  35. package/dist/lib/auth-store.js +56 -19
  36. package/dist/lib/backup.js +3 -3
  37. package/dist/lib/bootstrap.js +14 -8
  38. package/dist/lib/cli-config.js +137 -4
  39. package/dist/lib/cli-entry-error.js +44 -0
  40. package/dist/lib/cli-locale.js +19 -7
  41. package/dist/lib/db-connection-check.js +61 -0
  42. package/dist/lib/docker-log-stream.js +45 -0
  43. package/dist/lib/env-auth.js +98 -11
  44. package/dist/lib/env-config.js +3 -2
  45. package/dist/lib/env-guard.js +1 -1
  46. package/dist/lib/generated-command.js +17 -0
  47. package/dist/lib/prompt-validators.js +23 -5
  48. package/dist/lib/prompt-web-ui.js +143 -19
  49. package/dist/lib/run-npm.js +133 -23
  50. package/dist/lib/skills-manager.js +80 -4
  51. package/dist/lib/source-publish.js +4 -1
  52. package/dist/lib/startup-update.js +74 -50
  53. package/dist/lib/ui.js +4 -1
  54. package/dist/locale/en-US.json +92 -5
  55. package/dist/locale/zh-CN.json +92 -5
  56. 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
 
@@ -68,49 +69,20 @@ const { ensureRuntimeFromArgv } = await import(pathToFileURL(bootstrapPath).href
68
69
  const startupUpdatePath = isDev
69
70
  ? path.join(root, 'src/lib/startup-update.ts')
70
71
  : path.join(root, 'dist/lib/startup-update.js');
71
- const { maybeRunStartupUpdatePrompt } = await import(pathToFileURL(startupUpdatePath).href);
72
+ const { maybeRunStartupUpdate } = await import(pathToFileURL(startupUpdatePath).href);
73
+ const cliEntryErrorPath = isDev
74
+ ? path.join(root, 'src/lib/cli-entry-error.ts')
75
+ : path.join(root, 'dist/lib/cli-entry-error.js');
76
+ const { formatCliEntryError } = await import(pathToFileURL(cliEntryErrorPath).href);
72
77
  const { flush, run, settings } = await import('@oclif/core');
73
78
 
74
79
  if (isDev) {
75
80
  settings.debug = true;
76
81
  }
77
82
 
78
- function getCommandToken(argv) {
79
- const tokens = [];
80
-
81
- for (const token of argv) {
82
- if (!token || token.startsWith('-')) {
83
- continue;
84
- }
85
-
86
- tokens.push(token);
87
- }
88
-
89
- if (tokens[0] === 'api') {
90
- return tokens[1] ?? tokens[0];
91
- }
92
-
93
- return tokens[0];
94
- }
95
-
96
- function formatCliEntryError(error, argv) {
97
- const message = error instanceof Error ? error.message : String(error);
98
- const missingCommandMatch = message.match(/^Command (.+) not found\.$/);
99
- if (missingCommandMatch) {
100
- const commandToken = getCommandToken(argv) ?? missingCommandMatch[1];
101
- return [
102
- `Unknown command: \`${commandToken}\`.`,
103
- 'If this is a built-in command or a typo, run `nb --help` to inspect available commands.',
104
- `If \`${commandToken}\` should be a runtime command from your NocoBase app, run \`nb env update\` and try again.`,
105
- ].join('\n');
106
- }
107
-
108
- return message;
109
- }
110
-
111
83
  try {
112
84
  const argv = process.argv.slice(2);
113
- const startupUpdate = await maybeRunStartupUpdatePrompt(argv);
85
+ const startupUpdate = await maybeRunStartupUpdate(argv);
114
86
  if (startupUpdate.kind === 'updated') {
115
87
  const result = spawnSync(process.execPath, process.argv.slice(1), {
116
88
  stdio: 'inherit',
@@ -130,6 +102,6 @@ try {
130
102
  flush();
131
103
  } catch (error) {
132
104
  const message = formatCliEntryError(error, process.argv.slice(2));
133
- console.error(message);
134
- process.exitCode = 1;
105
+ console.error(pc.red(message));
106
+ process.exit(1);
135
107
  }
@@ -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
+ }