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

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
@@ -115,7 +115,7 @@ nb init --env app1 --resume
115
115
 
116
116
  `--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
117
 
118
- In non-interactive mode, pass these setup-only flags again because they are not saved in env config:
118
+ In non-interactive resume mode, `nb init --resume --yes` uses default initialization values unless these flags are passed explicitly:
119
119
 
120
120
  - `--lang`
121
121
  - `--root-username`
@@ -128,7 +128,7 @@ In non-interactive mode, pass these setup-only flags again because they are not
128
128
  | Command | Description |
129
129
  | --- | --- |
130
130
  | `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. |
131
+ | `nb app` | Manage app runtimes: start, stop, restart, logs, cleanup, and upgrades. |
132
132
  | `nb source` | Manage the local source project: download, develop, build, and test. |
133
133
  | `nb db` | Inspect or manage built-in database runtime status for local envs. |
134
134
  | `nb env` | Manage saved CLI env connections. |
@@ -137,13 +137,13 @@ In non-interactive mode, pass these setup-only flags again because they are not
137
137
  | `nb self` | Check or update the installed NocoBase CLI. |
138
138
  | `nb skills` | Check, install, or update global NocoBase AI coding skills. |
139
139
 
140
- Recommended style: use `--env` explicitly for app/runtime commands. `-e` is the short form:
140
+ 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
141
 
142
142
  ```bash
143
143
  nb app start --env app1
144
144
  nb app restart --env app1
145
145
  nb app logs --env app1
146
- nb app ps --env app1
146
+ nb env info app1
147
147
  nb db ps --env app1
148
148
  ```
149
149
 
@@ -291,12 +291,18 @@ Show the current env:
291
291
  nb env
292
292
  ```
293
293
 
294
- List configured envs:
294
+ List configured envs with token-verified API status:
295
295
 
296
296
  ```bash
297
297
  nb env list
298
298
  ```
299
299
 
300
+ Show details for one env:
301
+
302
+ ```bash
303
+ nb env info app1
304
+ ```
305
+
300
306
  Switch the current env:
301
307
 
302
308
  ```bash
package/README.zh-CN.md CHANGED
@@ -110,7 +110,7 @@ nb init --env app1 --resume
110
110
 
111
111
  `--resume` 会复用工作区里已保存的 env config,包括应用、source、数据库和 env 连接相关配置。在交互模式下,只会继续补齐缺失的初始化参数。
112
112
 
113
- 在非交互模式下,需要重新传这些只用于初始化、不会保存到 env config 的参数:
113
+ 在非交互恢复模式下,如果没有显式传入这些参数,`nb init --resume --yes` 会使用默认初始化值:
114
114
 
115
115
  - `--lang`
116
116
  - `--root-username`
@@ -130,13 +130,13 @@ nb init --env app1 --resume
130
130
  | `nb api` | 通过 CLI 调用 NocoBase API 资源。 |
131
131
  | `nb plugin` | 管理选中 NocoBase env 的插件。 |
132
132
 
133
- 推荐在应用和运行时相关命令里显式使用 `--env`;`-e` 是它的简写:
133
+ 推荐在操作指定 env 时显式传入 env 名称。运行时命令支持 `--env`,`nb env info` 也支持位置参数:
134
134
 
135
135
  ```bash
136
136
  nb app start --env app1
137
137
  nb app restart --env app1
138
138
  nb app logs --env app1
139
- nb app ps --env app1
139
+ nb env info app1
140
140
  nb db ps --env app1
141
141
  ```
142
142
 
@@ -252,12 +252,18 @@ nb app down --env app1 --all --yes
252
252
  nb env
253
253
  ```
254
254
 
255
- 查看已配置的 env
255
+ 查看已配置的 env 及 Token 验证后的 API 状态:
256
256
 
257
257
  ```bash
258
258
  nb env list
259
259
  ```
260
260
 
261
+ 查看某个 env 的详情:
262
+
263
+ ```bash
264
+ nb env info app1
265
+ ```
266
+
261
267
  切换当前 env:
262
268
 
263
269
  ```bash
@@ -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.
@@ -405,8 +411,9 @@ Prompt modes:
405
411
  onServerStart: ({ url }) => {
406
412
  logInitUiReady(this, url);
407
413
  },
408
- onOpenBrowserError: (_url, _err) => {
414
+ onOpenBrowserError: (_url, err) => {
409
415
  logInitUiBrowserOpenFallback();
416
+ p.log.info(`Browser open error: ${formatBrowserOpenError(err)}`);
410
417
  },
411
418
  });
412
419
  }
@@ -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
  }
@@ -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
  });
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.23",
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."
@@ -91,5 +91,5 @@
91
91
  "type": "git",
92
92
  "url": "git+https://github.com/nocobase/nocobase.git"
93
93
  },
94
- "gitHead": "53ad02861ed8e813103f59659804417118c85b4c"
94
+ "gitHead": "bb4c0d3551bf9eff505b63756dd24a0813231f16"
95
95
  }
@@ -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
- }