@nocobase/cli 2.1.0-beta.22 → 2.1.0-beta.24

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 CHANGED
@@ -60,12 +60,16 @@ nb init --ui
60
60
  When creating a new app, it can also install NocoBase AI coding skills
61
61
  (`nocobase/skills`) globally.
62
62
 
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.
65
+
63
66
  ### Non-Interactive Setup
64
67
 
65
68
  When prompts are skipped, an app/env name is required:
66
69
 
67
70
  ```bash
68
71
  nb init --env app1 --yes
72
+ nb init --env app1 --yes --skip-skills
69
73
  ```
70
74
 
71
75
  Install with Docker:
@@ -111,11 +115,12 @@ If `nb init` was interrupted after the env config had already been saved, you ca
111
115
 
112
116
  ```bash
113
117
  nb init --env app1 --resume
118
+ nb init --env app1 --resume --skip-skills
114
119
  ```
115
120
 
116
121
  `--resume` reuses the saved workspace env config for app, source, database, and env connection settings. In interactive mode, it only asks for any missing setup-only values.
117
122
 
118
- In non-interactive mode, pass these setup-only flags again because they are not saved in env config:
123
+ In non-interactive resume mode, `nb init --resume --yes` uses default initialization values unless these flags are passed explicitly:
119
124
 
120
125
  - `--lang`
121
126
  - `--root-username`
@@ -128,7 +133,7 @@ In non-interactive mode, pass these setup-only flags again because they are not
128
133
  | Command | Description |
129
134
  | --- | --- |
130
135
  | `nb init` | Set up NocoBase and connect it as a CLI env for coding agents. |
131
- | `nb app` | Manage app runtimes: start, stop, restart, logs, status, cleanup, and upgrades. |
136
+ | `nb app` | Manage app runtimes: start, stop, restart, logs, cleanup, and upgrades. |
132
137
  | `nb source` | Manage the local source project: download, develop, build, and test. |
133
138
  | `nb db` | Inspect or manage built-in database runtime status for local envs. |
134
139
  | `nb env` | Manage saved CLI env connections. |
@@ -137,13 +142,13 @@ In non-interactive mode, pass these setup-only flags again because they are not
137
142
  | `nb self` | Check or update the installed NocoBase CLI. |
138
143
  | `nb skills` | Check, install, or update global NocoBase AI coding skills. |
139
144
 
140
- Recommended style: use `--env` explicitly for app/runtime commands. `-e` is the short form:
145
+ Recommended style: pass the env name explicitly when operating on a specific env. Runtime commands accept `--env`, and `nb env info` also accepts a positional env name:
141
146
 
142
147
  ```bash
143
148
  nb app start --env app1
144
149
  nb app restart --env app1
145
150
  nb app logs --env app1
146
- nb app ps --env app1
151
+ nb env info app1
147
152
  nb db ps --env app1
148
153
  ```
149
154
 
@@ -291,12 +296,18 @@ Show the current env:
291
296
  nb env
292
297
  ```
293
298
 
294
- List configured envs:
299
+ List configured envs with token-verified API status:
295
300
 
296
301
  ```bash
297
302
  nb env list
298
303
  ```
299
304
 
305
+ Show details for one env:
306
+
307
+ ```bash
308
+ nb env info app1
309
+ ```
310
+
300
311
  Switch the current env:
301
312
 
302
313
  ```bash
package/README.zh-CN.md CHANGED
@@ -55,12 +55,15 @@ 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`。
59
+
58
60
  ### 非交互式初始化
59
61
 
60
62
  跳过交互提示时,必须提供 app/env name:
61
63
 
62
64
  ```bash
63
65
  nb init --env app1 --yes
66
+ nb init --env app1 --yes --skip-skills
64
67
  ```
65
68
 
66
69
  使用 Docker 安装:
@@ -106,11 +109,12 @@ nb init --env app1 --yes --source git --version fix/cli-v2
106
109
 
107
110
  ```bash
108
111
  nb init --env app1 --resume
112
+ nb init --env app1 --resume --skip-skills
109
113
  ```
110
114
 
111
115
  `--resume` 会复用工作区里已保存的 env config,包括应用、source、数据库和 env 连接相关配置。在交互模式下,只会继续补齐缺失的初始化参数。
112
116
 
113
- 在非交互模式下,需要重新传这些只用于初始化、不会保存到 env config 的参数:
117
+ 在非交互恢复模式下,如果没有显式传入这些参数,`nb init --resume --yes` 会使用默认初始化值:
114
118
 
115
119
  - `--lang`
116
120
  - `--root-username`
@@ -130,13 +134,13 @@ nb init --env app1 --resume
130
134
  | `nb api` | 通过 CLI 调用 NocoBase API 资源。 |
131
135
  | `nb plugin` | 管理选中 NocoBase env 的插件。 |
132
136
 
133
- 推荐在应用和运行时相关命令里显式使用 `--env`;`-e` 是它的简写:
137
+ 推荐在操作指定 env 时显式传入 env 名称。运行时命令支持 `--env`,`nb env info` 也支持位置参数:
134
138
 
135
139
  ```bash
136
140
  nb app start --env app1
137
141
  nb app restart --env app1
138
142
  nb app logs --env app1
139
- nb app ps --env app1
143
+ nb env info app1
140
144
  nb db ps --env app1
141
145
  ```
142
146
 
@@ -252,12 +256,18 @@ nb app down --env app1 --all --yes
252
256
  nb env
253
257
  ```
254
258
 
255
- 查看已配置的 env
259
+ 查看已配置的 env 及 Token 验证后的 API 状态:
256
260
 
257
261
  ```bash
258
262
  nb env list
259
263
  ```
260
264
 
265
+ 查看某个 env 的详情:
266
+
267
+ ```bash
268
+ nb env info app1
269
+ ```
270
+
261
271
  切换当前 env:
262
272
 
263
273
  ```bash
@@ -0,0 +1,132 @@
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 { formatMissingManagedAppEnvMessage } from '../../lib/app-runtime.js';
11
+ import { getEnv } from '../../lib/auth-store.js';
12
+ import { checkExternalDbConnection, formatDbCheckAddress, readExternalDbConnectionConfig, } from "../../lib/db-connection-check.js";
13
+ import { validateTcpPort } from "../../lib/prompt-validators.js";
14
+ function trimValue(value) {
15
+ const text = String(value ?? '').trim();
16
+ return text || undefined;
17
+ }
18
+ function resolveRequiredDbField(flagValue, envValue) {
19
+ return trimValue(flagValue) ?? trimValue(envValue);
20
+ }
21
+ function formatMissingFieldsMessage(missing) {
22
+ return [
23
+ 'Missing database settings for connectivity check.',
24
+ `Required: ${missing.join(', ')}.`,
25
+ 'Pass `--env <name>` to reuse a saved env, or provide all `--db-*` flags explicitly.',
26
+ ].join('\n');
27
+ }
28
+ export default class DbCheck extends Command {
29
+ static description = 'Check whether the current machine can connect to a database using saved env config or explicit --db-* flags.';
30
+ static examples = [
31
+ '<%= config.bin %> <%= command.id %> --env app1',
32
+ '<%= config.bin %> <%= command.id %> --db-dialect postgres --db-host 127.0.0.1 --db-port 5432 --db-database nocobase --db-user nocobase --db-password secret',
33
+ '<%= config.bin %> <%= command.id %> --env app1 --db-password new-secret --json',
34
+ ];
35
+ static flags = {
36
+ env: Flags.string({
37
+ char: 'e',
38
+ description: 'CLI env name to read saved database settings from. Defaults to the current env when omitted.',
39
+ }),
40
+ 'db-dialect': Flags.string({
41
+ description: 'Database dialect: postgres, kingbase, mysql, or mariadb.',
42
+ options: ['postgres', 'kingbase', 'mysql', 'mariadb'],
43
+ }),
44
+ 'db-host': Flags.string({
45
+ description: 'Database host name or IP address.',
46
+ }),
47
+ 'db-port': Flags.string({
48
+ description: 'Database TCP port.',
49
+ }),
50
+ 'db-database': Flags.string({
51
+ description: 'Database name.',
52
+ }),
53
+ 'db-user': Flags.string({
54
+ description: 'Database username.',
55
+ }),
56
+ 'db-password': Flags.string({
57
+ description: 'Database password.',
58
+ }),
59
+ json: Flags.boolean({
60
+ description: 'Output the check result as JSON.',
61
+ default: false,
62
+ }),
63
+ };
64
+ async run() {
65
+ const { flags } = await this.parse(DbCheck);
66
+ const envName = flags.env?.trim() || undefined;
67
+ const env = envName || !flags['db-host'] ? await getEnv(envName) : undefined;
68
+ if (envName && !env) {
69
+ this.error(formatMissingManagedAppEnvMessage(envName));
70
+ }
71
+ const config = env?.config ?? {};
72
+ const dbConfig = {
73
+ builtinDb: false,
74
+ dbDialect: resolveRequiredDbField(flags['db-dialect'], config.dbDialect),
75
+ dbHost: resolveRequiredDbField(flags['db-host'], config.dbHost),
76
+ dbPort: resolveRequiredDbField(flags['db-port'], config.dbPort),
77
+ dbDatabase: resolveRequiredDbField(flags['db-database'], config.dbDatabase),
78
+ dbUser: resolveRequiredDbField(flags['db-user'], config.dbUser),
79
+ dbPassword: flags['db-password'] !== undefined
80
+ ? String(flags['db-password'] ?? '')
81
+ : String(config.dbPassword ?? ''),
82
+ };
83
+ const missing = [];
84
+ if (!dbConfig.dbDialect) {
85
+ missing.push('--db-dialect');
86
+ }
87
+ if (!dbConfig.dbHost) {
88
+ missing.push('--db-host');
89
+ }
90
+ if (!dbConfig.dbPort) {
91
+ missing.push('--db-port');
92
+ }
93
+ if (!dbConfig.dbDatabase) {
94
+ missing.push('--db-database');
95
+ }
96
+ if (!dbConfig.dbUser) {
97
+ missing.push('--db-user');
98
+ }
99
+ if (!dbConfig.dbPassword) {
100
+ missing.push('--db-password');
101
+ }
102
+ if (missing.length > 0) {
103
+ this.error(formatMissingFieldsMessage(missing));
104
+ }
105
+ const portError = validateTcpPort(dbConfig.dbPort);
106
+ if (portError) {
107
+ this.error(portError);
108
+ }
109
+ const connectionConfig = readExternalDbConnectionConfig(dbConfig);
110
+ if (!connectionConfig) {
111
+ this.error('Unsupported or incomplete database settings for connectivity check.');
112
+ }
113
+ const address = formatDbCheckAddress(connectionConfig);
114
+ const validationError = await checkExternalDbConnection(connectionConfig);
115
+ if (flags.json) {
116
+ this.log(JSON.stringify({
117
+ ok: !validationError,
118
+ env: env?.name,
119
+ dialect: connectionConfig.dialect,
120
+ address,
121
+ error: validationError ?? null,
122
+ }, null, 2));
123
+ return;
124
+ }
125
+ if (validationError) {
126
+ this.error(validationError);
127
+ }
128
+ this.log(env?.name
129
+ ? `Database check passed for env "${env.name}" (${connectionConfig.dialect} ${address}).`
130
+ : `Database check passed (${connectionConfig.dialect} ${address}).`);
131
+ }
132
+ }
@@ -6,7 +6,7 @@
6
6
  * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
- import { Command, Flags } from '@oclif/core';
9
+ import { Args, Command, Flags } from '@oclif/core';
10
10
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime } from '../../lib/app-runtime.js';
11
11
  import { renderTable } from '../../lib/ui.js';
12
12
  import { appRootPath, dbStatus, runtimeStatus, storagePath } from './shared.js';
@@ -42,14 +42,21 @@ function createGroupTable(title, values) {
42
42
  function serializeGroup(values) {
43
43
  return Object.fromEntries(Object.entries(values).map(([field, value]) => [field, normalizeJsonValue(value)]));
44
44
  }
45
- export default class AppInfo extends Command {
45
+ export default class EnvInfo extends Command {
46
46
  static hidden = false;
47
- static description = 'Show grouped details for the selected NocoBase app env, including app, database, API, and auth settings.';
47
+ static description = 'Show grouped details for the selected NocoBase env, including app, database, API, and auth settings.';
48
48
  static examples = [
49
+ '<%= config.bin %> <%= command.id %> app1',
50
+ '<%= config.bin %> <%= command.id %> app1 --json',
51
+ '<%= config.bin %> <%= command.id %> app1 --show-secrets',
49
52
  '<%= config.bin %> <%= command.id %> --env app1',
50
- '<%= config.bin %> <%= command.id %> --env app1 --json',
51
- '<%= config.bin %> <%= command.id %> --env app1 --show-secrets',
52
53
  ];
54
+ static args = {
55
+ name: Args.string({
56
+ description: 'CLI env name to inspect. Defaults to the current env when omitted',
57
+ required: false,
58
+ }),
59
+ };
53
60
  static flags = {
54
61
  env: Flags.string({
55
62
  char: 'e',
@@ -65,8 +72,13 @@ export default class AppInfo extends Command {
65
72
  }),
66
73
  };
67
74
  async run() {
68
- const { flags } = await this.parse(AppInfo);
69
- const requestedEnv = flags.env?.trim() || undefined;
75
+ const { args, flags } = await this.parse(EnvInfo);
76
+ const envNameArg = args.name?.trim() || undefined;
77
+ const envNameFlag = flags.env?.trim() || undefined;
78
+ if (envNameArg && envNameFlag && envNameArg !== envNameFlag) {
79
+ this.error(`Environment name was provided both as the argument ("${envNameArg}") and as --env ("${envNameFlag}"). Please use only one.`);
80
+ }
81
+ const requestedEnv = envNameArg || envNameFlag;
70
82
  const showSecrets = flags['show-secrets'];
71
83
  const runtime = await resolveManagedAppRuntime(requestedEnv);
72
84
  if (!runtime) {
@@ -7,30 +7,44 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { Command } from '@oclif/core';
10
+ import { resolveManagedAppRuntime } from '../../lib/app-runtime.js';
10
11
  import { listEnvs } from '../../lib/auth-store.js';
11
12
  import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
12
13
  import { renderTable } from '../../lib/ui.js';
13
- function resolveApiBaseUrl(config) {
14
- return String(config.apiBaseUrl ?? config.baseUrl ?? config.apibaseUrl ?? '').trim();
15
- }
14
+ import { apiStatus, appUrl, resolveApiBaseUrl } from './shared.js';
16
15
  export default class EnvList extends Command {
17
- static summary = 'List configured environments';
16
+ static summary = 'List configured environments and API auth status';
18
17
  static examples = [
19
18
  '<%= config.bin %> <%= command.id %>',
20
19
  ];
21
20
  async run() {
22
21
  await this.parse(EnvList);
23
- const { currentEnv, envs } = await listEnvs({ scope: resolveDefaultConfigScope() });
22
+ const scope = resolveDefaultConfigScope();
23
+ const { currentEnv, envs } = await listEnvs({ scope });
24
24
  const names = Object.keys(envs).sort();
25
25
  if (!names.length) {
26
26
  this.log('No envs configured.');
27
27
  this.log('Run `nb env add <name> --api-base-url <url>` to add one.');
28
28
  return;
29
29
  }
30
- const rows = names.map((name) => {
30
+ const rows = [];
31
+ for (const name of names) {
31
32
  const env = envs[name];
32
- return [name === currentEnv ? '*' : '', name, resolveApiBaseUrl(env), env.auth?.type ?? '', env.runtime?.version ?? ''];
33
- });
34
- this.log(renderTable(['Current', 'Name', 'Base URL', 'Auth', 'Runtime'], rows));
33
+ const runtime = await resolveManagedAppRuntime(name);
34
+ const statusConfig = {
35
+ ...env,
36
+ ...(runtime?.env.config ?? {}),
37
+ };
38
+ rows.push([
39
+ name === currentEnv ? '*' : '',
40
+ name,
41
+ runtime?.kind ?? env.kind ?? '-',
42
+ await apiStatus(name, statusConfig, { scope }),
43
+ runtime ? appUrl(runtime) : resolveApiBaseUrl(env),
44
+ env.auth?.type ?? '',
45
+ env.runtime?.version ?? '',
46
+ ]);
47
+ }
48
+ this.log(renderTable(['Current', 'Name', 'Kind', 'App Status', 'URL', 'Auth', 'Runtime'], rows));
35
49
  }
36
50
  }
@@ -6,7 +6,8 @@
6
6
  * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
- import { buildDockerDbContainerName, dockerContainerExists, dockerContainerIsRunning, defaultWorkspaceName, } from '../../lib/app-runtime.js';
9
+ import { buildDockerDbContainerName, dockerContainerExists, dockerContainerIsRunning, } from '../../lib/app-runtime.js';
10
+ import { executeRawApiRequest } from '../../lib/api-client.js';
10
11
  export function resolveApiBaseUrl(config) {
11
12
  return String(config.apiBaseUrl ?? config.baseUrl ?? config.apibaseUrl ?? '').trim();
12
13
  }
@@ -18,12 +19,6 @@ export function appUrl(runtime) {
18
19
  const baseUrl = resolveApiBaseUrl(runtime.env.config);
19
20
  return baseUrl.replace(/\/api\/?$/, '');
20
21
  }
21
- export function appNetwork(runtime) {
22
- if (runtime.kind === 'docker') {
23
- return runtime.workspaceName?.trim() || defaultWorkspaceName();
24
- }
25
- return '-';
26
- }
27
22
  export function appRootPath(runtime) {
28
23
  if (runtime.kind === 'http' || runtime.kind === 'docker') {
29
24
  return '-';
@@ -40,6 +35,74 @@ export function storagePath(runtime) {
40
35
  const value = String(runtime.env.storagePath ?? runtime.env.config.storagePath ?? '').trim();
41
36
  return value || '-';
42
37
  }
38
+ function collectErrorCodes(value) {
39
+ if (!value || typeof value !== 'object') {
40
+ return [];
41
+ }
42
+ if (Array.isArray(value)) {
43
+ return value.flatMap((item) => collectErrorCodes(item));
44
+ }
45
+ const out = [];
46
+ const record = value;
47
+ if (typeof record.code === 'string') {
48
+ out.push(record.code);
49
+ }
50
+ for (const key of ['data', 'error', 'errors']) {
51
+ out.push(...collectErrorCodes(record[key]));
52
+ }
53
+ return out;
54
+ }
55
+ function isAuthFailureData(value) {
56
+ const codes = collectErrorCodes(value);
57
+ return codes.some((code) => ['EMPTY_TOKEN', 'INVALID_TOKEN', 'EXPIRED_TOKEN', 'BLOCKED_TOKEN', 'EXPIRED_SESSION', 'NOT_EXIST_USER'].includes(code));
58
+ }
59
+ function isNetworkFailure(error) {
60
+ if (!(error instanceof Error)) {
61
+ return false;
62
+ }
63
+ return (error.name === 'AbortError'
64
+ || /fetch failed|network|timeout|timed out|abort|ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|EAI_AGAIN|ENETUNREACH/i.test(error.message));
65
+ }
66
+ function isUnconfiguredFailure(error) {
67
+ return error instanceof Error && /missing (a )?base url|missing base URL/i.test(error.message);
68
+ }
69
+ function isAuthFailure(error) {
70
+ return (error instanceof Error
71
+ && /EMPTY_TOKEN|INVALID_TOKEN|EXPIRED_TOKEN|BLOCKED_TOKEN|EXPIRED_SESSION|NOT_EXIST_USER|invalid_grant|sign in|signin|authentication failed/i.test(error.message));
72
+ }
73
+ export async function apiStatus(envName, config, options = {}) {
74
+ if (!resolveApiBaseUrl(config)) {
75
+ return 'unconfigured';
76
+ }
77
+ try {
78
+ const response = await executeRawApiRequest({
79
+ envName,
80
+ scope: options.scope,
81
+ method: 'GET',
82
+ path: '/auth:check',
83
+ timeoutMs: options.timeoutMs ?? 2000,
84
+ });
85
+ if (response.ok) {
86
+ return 'ok';
87
+ }
88
+ if (response.status === 401 || response.status === 403 || isAuthFailureData(response.data)) {
89
+ return 'auth failed';
90
+ }
91
+ return 'error';
92
+ }
93
+ catch (error) {
94
+ if (isUnconfiguredFailure(error)) {
95
+ return 'unconfigured';
96
+ }
97
+ if (isAuthFailure(error)) {
98
+ return 'auth failed';
99
+ }
100
+ if (isNetworkFailure(error)) {
101
+ return 'unreachable';
102
+ }
103
+ return 'error';
104
+ }
105
+ }
43
106
  async function isLocalAppHealthy(runtime) {
44
107
  const port = String(runtime.env.config.appPort ?? '').trim();
45
108
  if (!port) {
@@ -139,6 +139,12 @@ function logInitUiReady(command, url) {
139
139
  function logInitUiBrowserOpenFallback() {
140
140
  p.log.warn(translateCli('commands.init.messages.uiOpenBrowserFallback'));
141
141
  }
142
+ function formatBrowserOpenError(error) {
143
+ if (error instanceof Error) {
144
+ return error.message;
145
+ }
146
+ return String(error);
147
+ }
142
148
  export default class Init extends Command {
143
149
  static summary = 'Set up NocoBase so coding agents can connect and work with it';
144
150
  static description = `Set up NocoBase for coding agents in the current workspace.
@@ -297,7 +303,6 @@ Prompt modes:
297
303
  }),
298
304
  'skip-skills': Flags.boolean({
299
305
  description: 'Skip installing or updating NocoBase AI coding skills during init',
300
- hidden: true,
301
306
  default: false,
302
307
  }),
303
308
  'ui-host': Flags.string({
@@ -405,8 +410,9 @@ Prompt modes:
405
410
  onServerStart: ({ url }) => {
406
411
  logInitUiReady(this, url);
407
412
  },
408
- onOpenBrowserError: (_url, _err) => {
413
+ onOpenBrowserError: (_url, err) => {
409
414
  logInitUiBrowserOpenFallback();
415
+ p.log.info(`Browser open error: ${formatBrowserOpenError(err)}`);
410
416
  },
411
417
  });
412
418
  }
@@ -18,6 +18,7 @@ import { runPromptCatalog, } from "../lib/prompt-catalog.js";
18
18
  import { applyCliLocale, localeText, resolveCliLocale, translateCli, } from "../lib/cli-locale.js";
19
19
  import { resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRoot, resolveEnvRelativePath, } from '../lib/cli-home.js';
20
20
  import { findAvailableTcpPort, validateAvailableTcpPort, validateTcpPort, validateEnvKey, } from "../lib/prompt-validators.js";
21
+ import { validateExternalDbConfig } from "../lib/db-connection-check.js";
21
22
  import { formatMissingManagedAppEnvMessage } from '../lib/app-runtime.js';
22
23
  import { run, runNocoBaseCommand } from '../lib/run-npm.js';
23
24
  import { startTask, stopTask, updateTask } from '../lib/ui.js';
@@ -180,6 +181,16 @@ function validateBuiltinDbEnabled(value, values) {
180
181
  }
181
182
  return translateCli('commands.install.validation.builtinDbUnsupported', { dialect });
182
183
  }
184
+ async function validateExternalDbPromptField(value, values) {
185
+ const builtinDb = values.builtinDb === undefined ? true : Boolean(values.builtinDb);
186
+ if (builtinDb) {
187
+ return undefined;
188
+ }
189
+ if (typeof value === 'string' && value.trim() === '') {
190
+ return undefined;
191
+ }
192
+ return await validateExternalDbConfig(values);
193
+ }
183
194
  function defaultInstallAppRootPath(envName) {
184
195
  const name = String(envName ?? DEFAULT_INSTALL_ENV_NAME).trim() || DEFAULT_INSTALL_ENV_NAME;
185
196
  return `./${name}/source/`;
@@ -447,6 +458,7 @@ export default class Install extends Command {
447
458
  initialValue: (values) => defaultDbHostForBuiltinDb(values),
448
459
  yesInitialValue: DEFAULT_INSTALL_BUILTIN_DB_HOST,
449
460
  required: true,
461
+ validate: validateExternalDbPromptField,
450
462
  hidden: (values) => Boolean(values.builtinDb),
451
463
  },
452
464
  dbPort: {
@@ -464,6 +476,7 @@ export default class Install extends Command {
464
476
  message: installText('prompts.dbDatabase.message'),
465
477
  initialValue: (values) => defaultDbDatabaseForDialect(values.dbDialect),
466
478
  required: true,
479
+ validate: validateExternalDbPromptField,
467
480
  },
468
481
  dbUser: {
469
482
  type: 'text',
@@ -471,6 +484,7 @@ export default class Install extends Command {
471
484
  initialValue: DEFAULT_INSTALL_DB_USER,
472
485
  yesInitialValue: DEFAULT_INSTALL_DB_USER,
473
486
  required: true,
487
+ validate: validateExternalDbPromptField,
474
488
  },
475
489
  dbPassword: {
476
490
  type: 'password',
@@ -478,6 +492,7 @@ export default class Install extends Command {
478
492
  initialValue: DEFAULT_INSTALL_DB_PASSWORD,
479
493
  yesInitialValue: DEFAULT_INSTALL_DB_PASSWORD,
480
494
  required: true,
495
+ validate: validateExternalDbPromptField,
481
496
  },
482
497
  };
483
498
  static rootUserPrompts = {
@@ -741,6 +756,9 @@ export default class Install extends Command {
741
756
  const builtinDb = values.builtinDb === undefined ? true : Boolean(values.builtinDb);
742
757
  const source = String(values.source ?? '').trim();
743
758
  if (!builtinDb || source === 'docker') {
759
+ if (!builtinDb) {
760
+ return await validateExternalDbConfig({ ...values, dbPort: value });
761
+ }
744
762
  return undefined;
745
763
  }
746
764
  return await Install.validateResumeAwareTcpPort(value, values, 'db');
@@ -765,6 +783,23 @@ export default class Install extends Command {
765
783
  });
766
784
  return reusesManagedPort ? undefined : portError;
767
785
  }
786
+ static async ensureExternalDbReadyForInstall(dbResults) {
787
+ const builtinDb = dbResults.builtinDb === undefined ? true : Boolean(dbResults.builtinDb);
788
+ if (builtinDb) {
789
+ return;
790
+ }
791
+ const dialect = String(dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
792
+ const host = String(dbResults.dbHost ?? '').trim();
793
+ const port = String(dbResults.dbPort ?? '').trim();
794
+ const database = String(dbResults.dbDatabase ?? '').trim();
795
+ const address = host && port ? `${host}:${port}` : host || port || '(unknown address)';
796
+ const target = database ? `${address}/${database}` : address;
797
+ p.log.step(`Checking external ${dialect} database: ${target}`);
798
+ const validationError = await validateExternalDbConfig(dbResults);
799
+ if (validationError) {
800
+ throw new Error(validationError);
801
+ }
802
+ }
768
803
  static async readResumePortValidationContext(values) {
769
804
  if (!Boolean(values.resume)) {
770
805
  return undefined;
@@ -2044,6 +2079,7 @@ export default class Install extends Command {
2044
2079
  const workspaceName = usesDockerResources
2045
2080
  ? await Install.ensureWorkspaceName()
2046
2081
  : undefined;
2082
+ await Install.ensureExternalDbReadyForInstall(dbResults);
2047
2083
  if (!parsed.resume) {
2048
2084
  await this.saveInstalledEnv({
2049
2085
  envName,
@@ -2053,6 +2089,7 @@ export default class Install extends Command {
2053
2089
  rootResults,
2054
2090
  envAddResults,
2055
2091
  });
2092
+ p.log.info(`Saved install config for env "${envName}"`);
2056
2093
  }
2057
2094
  let builtinDbPlan;
2058
2095
  if (Boolean(dbResults.builtinDb)) {
@@ -235,10 +235,24 @@ export async function executeRawApiRequest(options) {
235
235
  }
236
236
  url.searchParams.set(key, typeof value === 'object' ? JSON.stringify(value) : String(value));
237
237
  }
238
- const response = await fetchWithPreservedAuthRedirect(url.toString(), {
239
- method: options.method.toUpperCase(),
240
- headers,
241
- body: options.body === undefined ? undefined : JSON.stringify(options.body),
242
- });
243
- return parseResponse(response);
238
+ const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController() : undefined;
239
+ const timeout = controller
240
+ ? setTimeout(() => {
241
+ controller.abort();
242
+ }, options.timeoutMs)
243
+ : undefined;
244
+ try {
245
+ const response = await fetchWithPreservedAuthRedirect(url.toString(), {
246
+ method: options.method.toUpperCase(),
247
+ headers,
248
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
249
+ signal: controller?.signal,
250
+ });
251
+ return parseResponse(response);
252
+ }
253
+ finally {
254
+ if (timeout) {
255
+ clearTimeout(timeout);
256
+ }
257
+ }
244
258
  }
@@ -0,0 +1,178 @@
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 { translateCli } from "./cli-locale.js";
10
+ import { validateTcpPort } from "./prompt-validators.js";
11
+ const DB_CONNECTION_TIMEOUT_MS = 5_000;
12
+ const externalDbValidationCache = new Map();
13
+ function trimPromptValue(value) {
14
+ return String(value ?? '').trim();
15
+ }
16
+ export function readExternalDbConnectionConfig(values) {
17
+ const builtinDb = values.builtinDb === undefined ? true : Boolean(values.builtinDb);
18
+ if (builtinDb) {
19
+ return undefined;
20
+ }
21
+ const dialect = trimPromptValue(values.dbDialect || 'postgres');
22
+ if (dialect !== 'postgres' && dialect !== 'kingbase' && dialect !== 'mysql' && dialect !== 'mariadb') {
23
+ return undefined;
24
+ }
25
+ const host = trimPromptValue(values.dbHost);
26
+ const portText = trimPromptValue(values.dbPort);
27
+ const database = trimPromptValue(values.dbDatabase);
28
+ const user = trimPromptValue(values.dbUser);
29
+ const password = String(values.dbPassword ?? '');
30
+ if (!host || !portText || !database || !user || !password) {
31
+ return undefined;
32
+ }
33
+ if (validateTcpPort(portText)) {
34
+ return undefined;
35
+ }
36
+ return {
37
+ dialect,
38
+ host,
39
+ port: Number.parseInt(portText, 10),
40
+ database,
41
+ user,
42
+ password,
43
+ };
44
+ }
45
+ export function formatDbCheckAddress(config) {
46
+ return `${config.host}:${config.port}/${config.database}`;
47
+ }
48
+ function buildValidationCacheKey(config) {
49
+ return JSON.stringify(config);
50
+ }
51
+ function formatDbConnectionError(config, error) {
52
+ const maybeError = error;
53
+ const code = String(maybeError?.code ?? '').trim().toUpperCase();
54
+ const errno = typeof maybeError?.errno === 'number' ? maybeError.errno : undefined;
55
+ const rawMessage = String(maybeError?.message || maybeError?.sqlMessage || error || '').trim();
56
+ if (code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'EHOSTUNREACH' || code === 'ECONNRESET') {
57
+ return translateCli('validators.dbConnection.unreachable', {
58
+ host: config.host,
59
+ port: config.port,
60
+ details: rawMessage,
61
+ });
62
+ }
63
+ if (code === 'ETIMEDOUT') {
64
+ return translateCli('validators.dbConnection.timeout', {
65
+ host: config.host,
66
+ port: config.port,
67
+ seconds: Math.ceil(DB_CONNECTION_TIMEOUT_MS / 1000),
68
+ });
69
+ }
70
+ if (code === '28P01' || code === '28000' || code === 'ER_ACCESS_DENIED_ERROR' || errno === 1045) {
71
+ return translateCli('validators.dbConnection.authenticationFailed', {
72
+ user: config.user,
73
+ database: config.database,
74
+ });
75
+ }
76
+ if (code === '3D000' || code === 'ER_BAD_DB_ERROR' || errno === 1049) {
77
+ return translateCli('validators.dbConnection.databaseNotFound', {
78
+ database: config.database,
79
+ });
80
+ }
81
+ return translateCli('validators.dbConnection.connectionFailed', {
82
+ details: rawMessage || code || String(error),
83
+ });
84
+ }
85
+ async function checkPostgresFamilyConnection(config) {
86
+ const { default: pg } = await import('pg');
87
+ const client = new pg.Client({
88
+ host: config.host,
89
+ port: config.port,
90
+ user: config.user,
91
+ password: config.password,
92
+ database: config.database,
93
+ connectionTimeoutMillis: DB_CONNECTION_TIMEOUT_MS,
94
+ });
95
+ try {
96
+ await client.connect();
97
+ await client.query('SELECT 1');
98
+ }
99
+ finally {
100
+ await Promise.resolve(client.end()).catch(() => undefined);
101
+ }
102
+ }
103
+ async function checkMysqlConnection(config) {
104
+ const { default: mysql } = await import('mysql2/promise');
105
+ const connection = await mysql.createConnection({
106
+ host: config.host,
107
+ port: config.port,
108
+ user: config.user,
109
+ password: config.password,
110
+ database: config.database,
111
+ connectTimeout: DB_CONNECTION_TIMEOUT_MS,
112
+ });
113
+ try {
114
+ await connection.query('SELECT 1');
115
+ }
116
+ finally {
117
+ await Promise.resolve(connection.end()).catch(() => undefined);
118
+ }
119
+ }
120
+ async function checkMariaDbConnection(config) {
121
+ const { default: mariadb } = await import('mariadb');
122
+ const connection = await mariadb.createConnection({
123
+ host: config.host,
124
+ port: config.port,
125
+ user: config.user,
126
+ password: config.password,
127
+ database: config.database,
128
+ connectTimeout: DB_CONNECTION_TIMEOUT_MS,
129
+ });
130
+ try {
131
+ await connection.query('SELECT 1');
132
+ }
133
+ finally {
134
+ await Promise.resolve(connection.end()).catch(() => undefined);
135
+ }
136
+ }
137
+ async function performExternalDbConnectionCheck(config) {
138
+ try {
139
+ switch (config.dialect) {
140
+ case 'postgres':
141
+ case 'kingbase': {
142
+ await checkPostgresFamilyConnection(config);
143
+ return undefined;
144
+ }
145
+ case 'mysql': {
146
+ await checkMysqlConnection(config);
147
+ return undefined;
148
+ }
149
+ case 'mariadb': {
150
+ await checkMariaDbConnection(config);
151
+ return undefined;
152
+ }
153
+ }
154
+ }
155
+ catch (error) {
156
+ return formatDbConnectionError(config, error);
157
+ }
158
+ }
159
+ export async function checkExternalDbConnection(config) {
160
+ const cacheKey = buildValidationCacheKey(config);
161
+ const cached = externalDbValidationCache.get(cacheKey);
162
+ if (cached) {
163
+ return await cached;
164
+ }
165
+ const pending = performExternalDbConnectionCheck(config);
166
+ externalDbValidationCache.set(cacheKey, pending);
167
+ return await pending;
168
+ }
169
+ export async function validateExternalDbConfig(values) {
170
+ const config = readExternalDbConnectionConfig(values);
171
+ if (!config) {
172
+ return undefined;
173
+ }
174
+ return await checkExternalDbConnection(config);
175
+ }
176
+ export function clearExternalDbValidationCache() {
177
+ externalDbValidationCache.clear();
178
+ }
@@ -559,17 +559,23 @@ function readFormFromClientStrippingPwcMeta(o) {
559
559
  const { [PWC_FORM_META_STEP]: _meta, ...rest } = o;
560
560
  return rest;
561
561
  }
562
- function openUrlInDefaultBrowser(url) {
562
+ function openUrlInDefaultBrowser(url, onError) {
563
+ const reportError = (error) => {
564
+ onError?.(url, error);
565
+ };
563
566
  const platform = process.platform;
567
+ let child;
564
568
  if (platform === 'darwin') {
565
- spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
569
+ child = spawn('open', [url], { stdio: 'ignore', detached: true });
566
570
  }
567
571
  else if (platform === 'win32') {
568
- spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true, windowsHide: true }).unref();
572
+ child = spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true, windowsHide: true });
569
573
  }
570
574
  else {
571
- spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();
575
+ child = spawn('xdg-open', [url], { stdio: 'ignore', detached: true });
572
576
  }
577
+ child.once('error', reportError);
578
+ child.unref();
573
579
  }
574
580
  function closePromptWebUiServer(server, done) {
575
581
  server.close(done);
@@ -2084,11 +2090,12 @@ function runPromptCatalogWebUIImpl(options) {
2084
2090
  const port = addr.port;
2085
2091
  const startUrl = `http://${host}:${port}/`;
2086
2092
  options.onServerStart?.({ host, port, url: startUrl });
2093
+ const onOpenBrowserError = options.onOpenBrowserError ?? ((u, err) => console.warn(String(err), u));
2087
2094
  try {
2088
- openUrlInDefaultBrowser(startUrl);
2095
+ openUrlInDefaultBrowser(startUrl, onOpenBrowserError);
2089
2096
  }
2090
2097
  catch (e) {
2091
- (options.onOpenBrowserError ?? ((u, err) => console.warn(String(err), u)))(startUrl, e);
2098
+ onOpenBrowserError(startUrl, e);
2092
2099
  }
2093
2100
  timeoutId = setTimeout(() => rejectAndClose(new Error('Local UI timeout — close the tab and try again, or resubmit within the time limit.')), timeoutMs);
2094
2101
  });
@@ -20,7 +20,10 @@ function getStateFile() {
20
20
  return path.join(resolveCliHomeDir('global'), STARTUP_UPDATE_STATE_FILE);
21
21
  }
22
22
  function todayStamp(now = new Date()) {
23
- return now.toISOString().slice(0, 10);
23
+ const year = now.getFullYear();
24
+ const month = String(now.getMonth() + 1).padStart(2, '0');
25
+ const day = String(now.getDate()).padStart(2, '0');
26
+ return `${year}-${month}-${day}`;
24
27
  }
25
28
  function shouldSkipByArgv(argv) {
26
29
  const tokens = argv.filter((token) => token && !token.startsWith('-'));
@@ -62,6 +62,13 @@
62
62
  "allocateNotDockerPublished": "Failed to allocate an available TCP port that is not already published by Docker.",
63
63
  "alreadyInUse": "Port {{port}} is already in use. Choose another port.",
64
64
  "alreadyInUseByDocker": "Port {{port}} is already in use by a Docker container. Choose another port."
65
+ },
66
+ "dbConnection": {
67
+ "unreachable": "Can't reach the database at {{host}}:{{port}}. Check the host, port, and network connectivity. Details: {{details}}",
68
+ "timeout": "Timed out connecting to the database at {{host}}:{{port}} after about {{seconds}} seconds.",
69
+ "authenticationFailed": "Failed to sign in to database \"{{database}}\" with user \"{{user}}\". Check the username and password.",
70
+ "databaseNotFound": "Database \"{{database}}\" does not exist or is not accessible with the current connection settings.",
71
+ "connectionFailed": "Database connection check failed. Details: {{details}}"
65
72
  }
66
73
  },
67
74
  "commands": {
@@ -273,7 +280,7 @@
273
280
  "envExists": "Env \"{{envName}}\" already exists. Choose another env name."
274
281
  },
275
282
  "messages": {
276
- "title": "Set Up Your NocoBase AI Workspace",
283
+ "title": "Set Up NocoBase for Coding Agents",
277
284
  "appNameRequiredWhenSkipped": "Env name is required when prompts are skipped.",
278
285
  "appNameEnvHelp": "Use `nb init --yes --env <envName>` to continue.",
279
286
  "resumeEnvRequired": "Env name is required when resuming setup.",
@@ -303,24 +310,24 @@
303
310
  }
304
311
  },
305
312
  "webUi": {
306
- "pageTitle": "Set Up Your NocoBase AI Workspace",
307
- "documentHeading": "Set Up Your NocoBase AI Workspace",
308
- "documentHint": "Connect an existing NocoBase app, or install a new one and connect it, so coding agents can work with NocoBase in this workspace.",
313
+ "pageTitle": "Set Up NocoBase for Coding Agents",
314
+ "documentHeading": "Set Up NocoBase for Coding Agents",
315
+ "documentHint": "Connect an existing NocoBase app, or install a new one, so coding agents can access and work with NocoBase.",
309
316
  "gettingStarted": {
310
317
  "title": "Getting started",
311
- "description": "Pick your setup path."
318
+ "description": "Choose whether to connect an existing app or install a new one."
312
319
  },
313
320
  "connectExistingApp": {
314
321
  "title": "Connect an existing app",
315
- "description": "Add your app connection."
322
+ "description": "Save your app connection."
316
323
  },
317
324
  "createNewApp": {
318
- "title": "Create a new app",
319
- "description": "Set project basics."
325
+ "title": "Install a new app",
326
+ "description": "Set app basics and install options."
320
327
  },
321
328
  "downloadAppFiles": {
322
329
  "title": "Download app files",
323
- "description": "Choose source and options."
330
+ "description": "Choose how to get the app files."
324
331
  },
325
332
  "configureDatabase": {
326
333
  "title": "Configure the database",
@@ -62,6 +62,13 @@
62
62
  "allocateNotDockerPublished": "分配未被 Docker 占用的可用 TCP 端口失败。",
63
63
  "alreadyInUse": "端口 {{port}} 已被占用,请更换其他端口。",
64
64
  "alreadyInUseByDocker": "端口 {{port}} 已被 Docker 容器占用,请更换其他端口。"
65
+ },
66
+ "dbConnection": {
67
+ "unreachable": "无法连接到数据库 {{host}}:{{port}}。请检查主机、端口和网络连通性。详情:{{details}}",
68
+ "timeout": "连接数据库 {{host}}:{{port}} 超时,约 {{seconds}} 秒内未成功建立连接。",
69
+ "authenticationFailed": "无法使用用户 \"{{user}}\" 登录数据库 \"{{database}}\"。请检查用户名和密码。",
70
+ "databaseNotFound": "数据库 \"{{database}}\" 不存在,或当前连接配置无权访问该数据库。",
71
+ "connectionFailed": "数据库连接检查失败。详情:{{details}}"
65
72
  }
66
73
  },
67
74
  "commands": {
@@ -273,7 +280,7 @@
273
280
  "envExists": "Env \"{{envName}}\" 已存在,请换一个 env name。"
274
281
  },
275
282
  "messages": {
276
- "title": "初始化你的 NocoBase AI 工作区",
283
+ "title": "配置供 Coding Agents 使用的 NocoBase",
277
284
  "appNameRequiredWhenSkipped": "跳过 prompts 时必须提供 Env name。",
278
285
  "appNameEnvHelp": "请使用 `nb init --yes --env <envName>` 继续。",
279
286
  "resumeEnvRequired": "恢复安装时必须提供 Env name。",
@@ -303,24 +310,24 @@
303
310
  }
304
311
  },
305
312
  "webUi": {
306
- "pageTitle": "初始化你的 NocoBase AI 工作区",
307
- "documentHeading": "初始化你的 NocoBase AI 工作区",
308
- "documentHint": "连接已有的 NocoBase 应用,或安装一个新的应用后再完成连接,让 Coding Agents 可以在当前工作区中结合 NocoBase 协同工作。",
313
+ "pageTitle": "配置供 Coding Agents 使用的 NocoBase",
314
+ "documentHeading": "配置供 Coding Agents 使用的 NocoBase",
315
+ "documentHint": "连接已有的 NocoBase 应用,或安装一个新的应用,让 Coding Agents 可以在当前工作区中访问和操作 NocoBase",
309
316
  "gettingStarted": {
310
317
  "title": "开始设置",
311
- "description": "选择你的配置路径。"
318
+ "description": "选择连接已有应用,或安装一个新应用。"
312
319
  },
313
320
  "connectExistingApp": {
314
321
  "title": "连接已有应用",
315
- "description": "添加现有应用连接。"
322
+ "description": "保存现有应用连接。"
316
323
  },
317
324
  "createNewApp": {
318
- "title": "创建新应用",
319
- "description": "设置项目基础信息。"
325
+ "title": "安装新应用",
326
+ "description": "设置应用基础信息和安装选项。"
320
327
  },
321
328
  "downloadAppFiles": {
322
329
  "title": "下载应用文件",
323
- "description": "选择来源和下载选项。"
330
+ "description": "选择获取应用文件的方式。"
324
331
  },
325
332
  "configureDatabase": {
326
333
  "title": "配置数据库",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/cli",
3
- "version": "2.1.0-beta.22",
3
+ "version": "2.1.0-beta.24",
4
4
  "description": "NocoBase Command Line Tool",
5
5
  "type": "module",
6
6
  "main": "dist/generated/command-registry.js",
@@ -37,7 +37,7 @@
37
37
  "topicSeparator": " ",
38
38
  "topics": {
39
39
  "app": {
40
- "description": "Manage NocoBase app runtimes: start, stop, restart, logs, status, and upgrades."
40
+ "description": "Manage NocoBase app runtimes: start, stop, restart, logs, and upgrades."
41
41
  },
42
42
  "source": {
43
43
  "description": "Work with the local NocoBase source project: download, develop, build, and test."
@@ -56,7 +56,7 @@
56
56
  "description": "Manage the built-in database for the selected env."
57
57
  },
58
58
  "env": {
59
- "description": "Manage NocoBase project environments and update command runtimes."
59
+ "description": "Manage NocoBase project environments, status, details, and command runtimes."
60
60
  },
61
61
  "self": {
62
62
  "description": "Inspect or update the NocoBase CLI itself."
@@ -78,8 +78,11 @@
78
78
  "@oclif/core": "^4.10.4",
79
79
  "cross-spawn": "^7.0.6",
80
80
  "lodash": "^4.17.21",
81
+ "mariadb": "^2.5.6",
82
+ "mysql2": "^3.14.0",
81
83
  "openapi-types": "^12.1.3",
82
84
  "ora": "^8.2.0",
85
+ "pg": "^8.14.1",
83
86
  "picocolors": "^1.1.1"
84
87
  },
85
88
  "devDependencies": {
@@ -91,5 +94,5 @@
91
94
  "type": "git",
92
95
  "url": "git+https://github.com/nocobase/nocobase.git"
93
96
  },
94
- "gitHead": "53ad02861ed8e813103f59659804417118c85b4c"
97
+ "gitHead": "f77b85530a2d127d9bfe4dca3a26fbb02c1139ba"
95
98
  }
@@ -1,60 +0,0 @@
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 { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, } from '../../lib/app-runtime.js';
11
- import { listEnvs } from '../../lib/auth-store.js';
12
- import { renderTable } from '../../lib/ui.js';
13
- import { appNetwork, appRootPath, appUrl, dbStatus, runtimeStatus, storagePath } from './shared.js';
14
- export default class AppPs extends Command {
15
- static hidden = false;
16
- static description = 'Show NocoBase runtime status for configured envs without starting or stopping anything.';
17
- static examples = [
18
- '<%= config.bin %> <%= command.id %>',
19
- '<%= config.bin %> <%= command.id %> --env app1',
20
- ];
21
- static flags = {
22
- env: Flags.string({
23
- char: 'e',
24
- description: 'CLI env name to inspect. Omit to show all configured envs',
25
- }),
26
- };
27
- async run() {
28
- const { flags } = await this.parse(AppPs);
29
- const requestedEnv = flags.env?.trim() || undefined;
30
- const envNames = requestedEnv
31
- ? [requestedEnv]
32
- : Object.keys((await listEnvs()).envs).sort();
33
- if (!envNames.length) {
34
- this.log('No NocoBase env is configured yet. Run `nb init` to create one first.');
35
- return;
36
- }
37
- const rows = [];
38
- for (const envName of envNames) {
39
- const runtime = await resolveManagedAppRuntime(envName);
40
- if (!runtime) {
41
- if (requestedEnv) {
42
- this.error(formatMissingManagedAppEnvMessage(envName));
43
- }
44
- rows.push([envName, '-', 'missing', '-', '-', '-', '-', '']);
45
- continue;
46
- }
47
- rows.push([
48
- runtime.envName,
49
- runtime.kind,
50
- await runtimeStatus(runtime),
51
- await dbStatus(runtime),
52
- appNetwork(runtime),
53
- appRootPath(runtime),
54
- storagePath(runtime),
55
- appUrl(runtime),
56
- ]);
57
- }
58
- this.log(renderTable(['Env', 'Kind', 'App Status', 'Database Status', 'Network', 'App Root', 'Storage', 'URL'], rows));
59
- }
60
- }
@@ -1,12 +0,0 @@
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 AppPs from './app/ps.js';
10
- export default class Ps extends AppPs {
11
- static hidden = true;
12
- }