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

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 (50) hide show
  1. package/README.md +28 -46
  2. package/README.zh-CN.md +27 -44
  3. package/dist/commands/app/down.js +260 -0
  4. package/dist/commands/app/info.js +140 -0
  5. package/dist/commands/app/logs.js +98 -0
  6. package/dist/commands/app/ps.js +60 -0
  7. package/dist/commands/app/restart.js +75 -0
  8. package/dist/commands/app/shared.js +95 -0
  9. package/dist/commands/app/start.js +252 -0
  10. package/dist/commands/app/stop.js +98 -0
  11. package/dist/commands/app/upgrade.js +595 -0
  12. package/dist/commands/build.js +3 -48
  13. package/dist/commands/dev.js +3 -147
  14. package/dist/commands/down.js +3 -188
  15. package/dist/commands/download.js +4 -856
  16. package/dist/commands/env/add.js +28 -23
  17. package/dist/commands/{prompts-stages.js → examples/prompts-stages.js} +3 -3
  18. package/dist/commands/{prompts-test.js → examples/prompts-test.js} +3 -3
  19. package/dist/commands/init.js +76 -5
  20. package/dist/commands/install.js +288 -61
  21. package/dist/commands/logs.js +3 -88
  22. package/dist/commands/plugin/disable.js +64 -0
  23. package/dist/commands/plugin/enable.js +64 -0
  24. package/dist/commands/plugin/list.js +62 -0
  25. package/dist/commands/pm/disable.js +3 -54
  26. package/dist/commands/pm/enable.js +3 -54
  27. package/dist/commands/pm/list.js +3 -52
  28. package/dist/commands/ps.js +3 -110
  29. package/dist/commands/restart.js +3 -65
  30. package/dist/commands/scaffold/migration.js +1 -1
  31. package/dist/commands/scaffold/plugin.js +1 -1
  32. package/dist/commands/skills/remove.js +71 -0
  33. package/dist/commands/skills/update.js +7 -0
  34. package/dist/commands/source/build.js +58 -0
  35. package/dist/commands/source/dev.js +157 -0
  36. package/dist/commands/source/download.js +866 -0
  37. package/dist/commands/source/test.js +467 -0
  38. package/dist/commands/start.js +3 -209
  39. package/dist/commands/stop.js +3 -88
  40. package/dist/commands/test.js +3 -457
  41. package/dist/commands/upgrade.js +3 -585
  42. package/dist/help/runtime-help.js +3 -0
  43. package/dist/lib/app-health.js +126 -0
  44. package/dist/lib/app-managed-resources.js +264 -0
  45. package/dist/lib/auth-store.js +5 -2
  46. package/dist/lib/cli-home.js +7 -6
  47. package/dist/lib/cli-locale.js +15 -1
  48. package/dist/lib/env-config.js +80 -0
  49. package/dist/lib/skills-manager.js +34 -7
  50. package/package.json +26 -3
@@ -0,0 +1,140 @@
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 { renderTable } from '../../lib/ui.js';
12
+ import { appRootPath, dbStatus, runtimeStatus, storagePath } from './shared.js';
13
+ function normalizeJsonValue(value) {
14
+ if (value === undefined || value === null || value === '') {
15
+ return '-';
16
+ }
17
+ if (typeof value === 'boolean' || typeof value === 'number') {
18
+ return value;
19
+ }
20
+ return String(value);
21
+ }
22
+ function normalizeValue(value) {
23
+ if (value === undefined || value === null || value === '') {
24
+ return '-';
25
+ }
26
+ if (typeof value === 'boolean') {
27
+ return value ? 'true' : 'false';
28
+ }
29
+ return String(value);
30
+ }
31
+ function maskSecret(value, showSecrets) {
32
+ const normalized = normalizeValue(value);
33
+ if (normalized === '-') {
34
+ return normalized;
35
+ }
36
+ return showSecrets ? normalized : '********';
37
+ }
38
+ function createGroupTable(title, values) {
39
+ const rows = Object.entries(values).map(([field, value]) => [field, normalizeValue(value)]);
40
+ return `${title}\n${renderTable(['Field', 'Value'], rows)}`;
41
+ }
42
+ function serializeGroup(values) {
43
+ return Object.fromEntries(Object.entries(values).map(([field, value]) => [field, normalizeJsonValue(value)]));
44
+ }
45
+ export default class AppInfo extends Command {
46
+ static hidden = false;
47
+ static description = 'Show grouped details for the selected NocoBase app env, including app, database, API, and auth settings.';
48
+ static examples = [
49
+ '<%= 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
+ static flags = {
54
+ env: Flags.string({
55
+ char: 'e',
56
+ description: 'CLI env name to inspect. Defaults to the current env when omitted',
57
+ }),
58
+ json: Flags.boolean({
59
+ description: 'Output the result as JSON',
60
+ default: false,
61
+ }),
62
+ 'show-secrets': Flags.boolean({
63
+ description: 'Show secret values in plain text',
64
+ default: false,
65
+ }),
66
+ };
67
+ async run() {
68
+ const { flags } = await this.parse(AppInfo);
69
+ const requestedEnv = flags.env?.trim() || undefined;
70
+ const showSecrets = flags['show-secrets'];
71
+ const runtime = await resolveManagedAppRuntime(requestedEnv);
72
+ if (!runtime) {
73
+ this.error(formatMissingManagedAppEnvMessage(requestedEnv));
74
+ }
75
+ const auth = runtime.env.auth;
76
+ const appGroup = {
77
+ appRootPath: appRootPath(runtime),
78
+ storagePath: storagePath(runtime),
79
+ appPort: runtime.env.config.appPort,
80
+ appStatus: await runtimeStatus(runtime),
81
+ source: runtime.source,
82
+ downloadVersion: runtime.env.config.downloadVersion,
83
+ dockerRegistry: runtime.env.config.dockerRegistry,
84
+ dockerPlatform: runtime.env.config.dockerPlatform,
85
+ timezone: runtime.env.config.timezone,
86
+ };
87
+ const dbGroup = {
88
+ databaseStatus: await dbStatus(runtime),
89
+ builtinDb: runtime.env.config.builtinDb,
90
+ dbDialect: runtime.env.config.dbDialect,
91
+ builtinDbImage: runtime.env.config.builtinDbImage,
92
+ dbHost: runtime.env.config.dbHost,
93
+ dbPort: runtime.env.config.dbPort,
94
+ dbDatabase: runtime.env.config.dbDatabase,
95
+ dbUser: runtime.env.config.dbUser,
96
+ dbPassword: maskSecret(runtime.env.config.dbPassword, showSecrets),
97
+ };
98
+ const authGroup = {
99
+ type: auth?.type,
100
+ expiresAt: auth?.type === 'oauth' ? auth.expiresAt : undefined,
101
+ scope: auth?.type === 'oauth' ? auth.scope : undefined,
102
+ issuer: auth?.type === 'oauth' ? auth.issuer : undefined,
103
+ clientId: auth?.type === 'oauth' ? auth.clientId : undefined,
104
+ resource: auth?.type === 'oauth' ? auth.resource : undefined,
105
+ accessToken: maskSecret(auth?.accessToken, showSecrets),
106
+ refreshToken: maskSecret(auth?.type === 'oauth' ? auth.refreshToken : undefined, showSecrets),
107
+ };
108
+ const apiGroup = {
109
+ apiBaseUrl: runtime.env.apiBaseUrl,
110
+ 'auth.type': authGroup.type,
111
+ 'auth.expiresAt': authGroup.expiresAt,
112
+ 'auth.scope': authGroup.scope,
113
+ 'auth.issuer': authGroup.issuer,
114
+ 'auth.clientId': authGroup.clientId,
115
+ 'auth.resource': authGroup.resource,
116
+ 'auth.accessToken': authGroup.accessToken,
117
+ 'auth.refreshToken': authGroup.refreshToken,
118
+ };
119
+ const output = {
120
+ ok: true,
121
+ env: runtime.envName,
122
+ kind: runtime.kind,
123
+ app: serializeGroup(appGroup),
124
+ db: serializeGroup(dbGroup),
125
+ api: {
126
+ apiBaseUrl: normalizeJsonValue(runtime.env.apiBaseUrl),
127
+ auth: serializeGroup(authGroup),
128
+ },
129
+ };
130
+ if (flags.json) {
131
+ this.log(JSON.stringify(output, null, 2));
132
+ return;
133
+ }
134
+ this.log([
135
+ createGroupTable('App', appGroup),
136
+ createGroupTable('DB', dbGroup),
137
+ createGroupTable('API', apiGroup),
138
+ ].join('\n\n'));
139
+ }
140
+ }
@@ -0,0 +1,98 @@
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, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
11
+ import { run } from '../../lib/run-npm.js';
12
+ import { printInfo } from '../../lib/ui.js';
13
+ function formatLogsFailure(envName, message) {
14
+ return [
15
+ `Couldn't show logs for "${envName}".`,
16
+ 'Check that the saved local app or Docker container still exists on this machine, then try again.',
17
+ `Details: ${message}`,
18
+ ].join('\n');
19
+ }
20
+ export default class AppLogs extends Command {
21
+ static hidden = false;
22
+ static description = 'Show NocoBase logs for the selected env. Local npm/git installs use pm2 logs, and Docker installs use docker logs.';
23
+ static examples = [
24
+ '<%= config.bin %> <%= command.id %>',
25
+ '<%= config.bin %> <%= command.id %> --env app1',
26
+ '<%= config.bin %> <%= command.id %> --env app1 --tail 200',
27
+ '<%= config.bin %> <%= command.id %> --env app1 --no-follow',
28
+ ];
29
+ static flags = {
30
+ env: Flags.string({
31
+ char: 'e',
32
+ description: 'CLI env name to inspect logs for. Defaults to the current env when omitted',
33
+ }),
34
+ tail: Flags.integer({
35
+ description: 'Number of recent log lines to show before following',
36
+ default: 100,
37
+ min: 0,
38
+ }),
39
+ follow: Flags.boolean({
40
+ char: 'f',
41
+ description: 'Keep streaming new log lines',
42
+ default: true,
43
+ allowNo: true,
44
+ }),
45
+ };
46
+ async run() {
47
+ const { flags } = await this.parse(AppLogs);
48
+ const requestedEnv = flags.env?.trim() || undefined;
49
+ const runtime = await resolveManagedAppRuntime(requestedEnv);
50
+ if (!runtime) {
51
+ this.error(formatMissingManagedAppEnvMessage(requestedEnv));
52
+ }
53
+ if (runtime.kind === 'http') {
54
+ this.error([
55
+ `Can't show runtime logs for "${runtime.envName}" from this machine.`,
56
+ 'This env only has an API connection, so there is no saved local app or Docker container to read logs from.',
57
+ 'Connect it to a local checkout or reinstall it with npm, git, or Docker if you want CLI-managed logs.',
58
+ ].join('\n'));
59
+ }
60
+ if (runtime.kind === 'ssh') {
61
+ this.error([
62
+ `Can't show runtime logs for "${runtime.envName}" yet.`,
63
+ 'SSH env support is reserved but not implemented yet.',
64
+ 'Use a local or Docker env for CLI-managed logs for now.',
65
+ ].join('\n'));
66
+ }
67
+ const tail = String(flags.tail ?? 100);
68
+ const follow = flags.follow !== false;
69
+ printInfo(follow
70
+ ? `Showing logs for "${runtime.envName}" (press Ctrl+C to stop).`
71
+ : `Showing recent logs for "${runtime.envName}".`);
72
+ try {
73
+ if (runtime.kind === 'docker') {
74
+ const dockerArgs = ['logs', '--tail', tail];
75
+ if (follow) {
76
+ dockerArgs.push('--follow');
77
+ }
78
+ dockerArgs.push(runtime.containerName);
79
+ await run('docker', dockerArgs, {
80
+ errorName: 'docker logs',
81
+ stdio: 'inherit',
82
+ });
83
+ return;
84
+ }
85
+ const localArgs = ['pm2', 'logs', '--lines', tail];
86
+ if (!follow) {
87
+ localArgs.push('--nostream');
88
+ }
89
+ await runLocalNocoBaseCommand(runtime, localArgs, {
90
+ stdio: 'inherit',
91
+ });
92
+ }
93
+ catch (error) {
94
+ const message = error instanceof Error ? error.message : String(error);
95
+ this.error(formatLogsFailure(runtime.envName, message));
96
+ }
97
+ }
98
+ }
@@ -0,0 +1,60 @@
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
+ }
@@ -0,0 +1,75 @@
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
+ function argvHasToken(argv, tokens) {
11
+ return tokens.some((token) => argv.includes(token));
12
+ }
13
+ function pushFlag(argv, flag, value) {
14
+ if (value !== undefined) {
15
+ argv.push(flag, String(value));
16
+ }
17
+ }
18
+ export default class AppRestart extends Command {
19
+ static hidden = false;
20
+ static description = 'Restart NocoBase for the selected env by stopping it first, then starting it again.';
21
+ static examples = [
22
+ '<%= config.bin %> <%= command.id %>',
23
+ '<%= config.bin %> <%= command.id %> --env local',
24
+ '<%= config.bin %> <%= command.id %> --env local --quickstart',
25
+ '<%= config.bin %> <%= command.id %> --env local --port 12000',
26
+ '<%= config.bin %> <%= command.id %> --env local --daemon',
27
+ '<%= config.bin %> <%= command.id %> --env local --no-daemon',
28
+ '<%= config.bin %> <%= command.id %> --env local --instances 2',
29
+ '<%= config.bin %> <%= command.id %> --env local --launch-mode pm2',
30
+ '<%= config.bin %> <%= command.id %> --env local --verbose',
31
+ '<%= config.bin %> <%= command.id %> --env local-docker',
32
+ ];
33
+ static flags = {
34
+ env: Flags.string({
35
+ char: 'e',
36
+ description: 'CLI env name to restart. Defaults to the current env when omitted',
37
+ }),
38
+ quickstart: Flags.boolean({ description: 'Quickstart the application after stopping it', required: false }),
39
+ port: Flags.string({ description: 'Port (overrides appPort from env config when set)', char: 'p', required: false }),
40
+ daemon: Flags.boolean({
41
+ description: 'Run the application as a daemon after stopping it (default: true; use --no-daemon to stay in the foreground)',
42
+ char: 'd',
43
+ required: false,
44
+ default: true,
45
+ allowNo: true,
46
+ }),
47
+ instances: Flags.integer({ description: 'Number of instances to run after stopping it', char: 'i', required: false }),
48
+ 'launch-mode': Flags.string({ description: 'Launch Mode', required: false, options: ['pm2', 'node'] }),
49
+ verbose: Flags.boolean({
50
+ description: 'Show raw shutdown/startup output from the underlying local or Docker command',
51
+ default: false,
52
+ }),
53
+ };
54
+ async run() {
55
+ const { flags } = await this.parse(AppRestart);
56
+ const stopArgv = [];
57
+ const daemonFlagWasProvided = argvHasToken(this.argv, ['--daemon', '--no-daemon']);
58
+ pushFlag(stopArgv, '--env', flags.env?.trim() || undefined);
59
+ if (flags.verbose) {
60
+ stopArgv.push('--verbose');
61
+ }
62
+ await this.config.runCommand('app:stop', stopArgv);
63
+ const startArgv = [...stopArgv];
64
+ if (flags.quickstart) {
65
+ startArgv.push('--quickstart');
66
+ }
67
+ pushFlag(startArgv, '--port', flags.port);
68
+ if (daemonFlagWasProvided) {
69
+ startArgv.push(flags.daemon === false ? '--no-daemon' : '--daemon');
70
+ }
71
+ pushFlag(startArgv, '--instances', flags.instances);
72
+ pushFlag(startArgv, '--launch-mode', flags['launch-mode']);
73
+ await this.config.runCommand('app:start', startArgv);
74
+ }
75
+ }
@@ -0,0 +1,95 @@
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 { buildDockerDbContainerName, dockerContainerExists, dockerContainerIsRunning, defaultWorkspaceName, } from '../../lib/app-runtime.js';
10
+ export function resolveApiBaseUrl(config) {
11
+ return String(config.apiBaseUrl ?? config.baseUrl ?? config.apibaseUrl ?? '').trim();
12
+ }
13
+ export function appUrl(runtime) {
14
+ const port = String(runtime.env.config.appPort ?? '').trim();
15
+ if (port) {
16
+ return `http://127.0.0.1:${port}`;
17
+ }
18
+ const baseUrl = resolveApiBaseUrl(runtime.env.config);
19
+ return baseUrl.replace(/\/api\/?$/, '');
20
+ }
21
+ export function appNetwork(runtime) {
22
+ if (runtime.kind === 'docker') {
23
+ return runtime.workspaceName?.trim() || defaultWorkspaceName();
24
+ }
25
+ return '-';
26
+ }
27
+ export function appRootPath(runtime) {
28
+ if (runtime.kind === 'http' || runtime.kind === 'docker') {
29
+ return '-';
30
+ }
31
+ if (runtime.kind === 'local') {
32
+ return String(runtime.projectRoot ?? runtime.env.appRootPath ?? '').trim() || '-';
33
+ }
34
+ return String(runtime.env.config.appRootPath ?? '').trim() || '-';
35
+ }
36
+ export function storagePath(runtime) {
37
+ if (runtime.kind === 'http') {
38
+ return '-';
39
+ }
40
+ const value = String(runtime.env.storagePath ?? runtime.env.config.storagePath ?? '').trim();
41
+ return value || '-';
42
+ }
43
+ async function isLocalAppHealthy(runtime) {
44
+ const port = String(runtime.env.config.appPort ?? '').trim();
45
+ if (!port) {
46
+ return false;
47
+ }
48
+ const controller = new AbortController();
49
+ const timeout = setTimeout(() => controller.abort(), 1500);
50
+ try {
51
+ const response = await fetch(`http://127.0.0.1:${port}/api/__health_check`, {
52
+ signal: controller.signal,
53
+ });
54
+ const text = await response.text();
55
+ return response.ok && text.trim().toLowerCase() === 'ok';
56
+ }
57
+ catch (_error) {
58
+ return false;
59
+ }
60
+ finally {
61
+ clearTimeout(timeout);
62
+ }
63
+ }
64
+ async function dockerStatus(containerName) {
65
+ if (!(await dockerContainerExists(containerName))) {
66
+ return 'missing';
67
+ }
68
+ return await dockerContainerIsRunning(containerName) ? 'running' : 'stopped';
69
+ }
70
+ export async function dbStatus(runtime) {
71
+ if (!runtime.env.config.builtinDb) {
72
+ return runtime.kind === 'http' ? 'external' : '-';
73
+ }
74
+ if (runtime.kind === 'http') {
75
+ return 'external';
76
+ }
77
+ if (runtime.kind === 'ssh') {
78
+ return '-';
79
+ }
80
+ const dbDialect = String(runtime.env.config.dbDialect ?? 'postgres').trim() || 'postgres';
81
+ const containerName = buildDockerDbContainerName(runtime.envName, dbDialect, runtime.workspaceName);
82
+ return await dockerStatus(containerName);
83
+ }
84
+ export async function runtimeStatus(runtime) {
85
+ if (runtime.kind === 'http') {
86
+ return 'http';
87
+ }
88
+ if (runtime.kind === 'ssh') {
89
+ return 'ssh';
90
+ }
91
+ if (runtime.kind === 'docker') {
92
+ return await dockerStatus(runtime.containerName);
93
+ }
94
+ return await isLocalAppHealthy(runtime) ? 'running' : 'stopped';
95
+ }