@nocobase/cli 2.1.0-alpha.17 → 2.1.0-alpha.19

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/bin/run.js CHANGED
@@ -9,6 +9,10 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
9
  const root = path.resolve(__dirname, '..');
10
10
  const realRoot = fs.realpathSync(root);
11
11
  const isSourcePackage = realRoot.split(path.sep).join('/').endsWith('/packages/core/cli');
12
+ let isDev = isSourcePackage;
13
+ if (process.env.NODE_ENV === 'production') {
14
+ isDev = false;
15
+ }
12
16
 
13
17
  /**
14
18
  * In the monorepo, plain `node` cannot load `.ts`. Re-exec once with `--import tsx`
@@ -30,17 +34,15 @@ function reexecWithTsx() {
30
34
  process.exit(result.status === null ? 1 : result.status);
31
35
  }
32
36
 
33
- if (isSourcePackage && !process.env._NOCO_CLI_TSX_CHILD) {
37
+ if (isDev && !process.env._NOCO_CLI_TSX_CHILD) {
34
38
  reexecWithTsx();
35
39
  }
36
40
 
37
- const bootstrapPath = isSourcePackage
38
- ? path.join(root, 'src/lib/bootstrap.ts')
39
- : path.join(root, 'dist/lib/bootstrap.js');
41
+ const bootstrapPath = isDev ? path.join(root, 'src/lib/bootstrap.ts') : path.join(root, 'dist/lib/bootstrap.js');
40
42
  const { ensureRuntimeFromArgv } = await import(pathToFileURL(bootstrapPath).href);
41
43
  const { flush, run, settings } = await import('@oclif/core');
42
44
 
43
- if (isSourcePackage) {
45
+ if (isDev) {
44
46
  settings.debug = true;
45
47
  }
46
48
 
@@ -1,5 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
- import { buildCreateArgs, createFlags, runResourceCommand } from '../../lib/resource-command.js';
2
+ import { buildCreateArgs, createFlags, runResourceCommand } from '../../../lib/resource-command.js';
3
3
  export default class ResourceCreate extends Command {
4
4
  static summary = 'Create a record in a resource';
5
5
  static description = 'Create a record in a generic resource. Pass record content through --values as a JSON object.';
@@ -1,5 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
- import { buildDestroyArgs, destroyFlags, runResourceCommand } from '../../lib/resource-command.js';
2
+ import { buildDestroyArgs, destroyFlags, runResourceCommand } from '../../../lib/resource-command.js';
3
3
  export default class ResourceDestroy extends Command {
4
4
  static summary = 'Delete records from a resource';
5
5
  static description = 'Delete records from a generic resource. Target records with --filter-by-tk or --filter.';
@@ -1,5 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
- import { buildGetArgs, getFlags, runResourceCommand } from '../../lib/resource-command.js';
2
+ import { buildGetArgs, getFlags, runResourceCommand } from '../../../lib/resource-command.js';
3
3
  export default class ResourceGet extends Command {
4
4
  static summary = 'Get a record from a resource';
5
5
  static description = 'Get a record from a generic resource. Use --filter-by-tk for the primary key and association resource names with --source-id when needed.';
@@ -1,5 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
- import { buildListArgs, listFlags, runResourceCommand } from '../../lib/resource-command.js';
2
+ import { buildListArgs, listFlags, runResourceCommand } from '../../../lib/resource-command.js';
3
3
  export default class ResourceList extends Command {
4
4
  static summary = 'List records from a resource';
5
5
  static description = 'List records from a generic resource. Use association resource names like posts.comments with --source-id when needed.';
@@ -1,5 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
- import { buildQueryArgs, queryFlags, runResourceCommand } from '../../lib/resource-command.js';
2
+ import { buildQueryArgs, queryFlags, runResourceCommand } from '../../../lib/resource-command.js';
3
3
  export default class ResourceQuery extends Command {
4
4
  static summary = 'Run an aggregate query on a resource';
5
5
  static description = 'Run an aggregate query on a generic resource. Pass measures, dimensions, and orders as JSON arrays.';
@@ -1,5 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
- import { buildUpdateArgs, runResourceCommand, updateFlags } from '../../lib/resource-command.js';
2
+ import { buildUpdateArgs, runResourceCommand, updateFlags } from '../../../lib/resource-command.js';
3
3
  export default class ResourceUpdate extends Command {
4
4
  static summary = 'Update records in a resource';
5
5
  static description = 'Update records in a generic resource. Target records with --filter-by-tk or --filter, and pass updated values through --values.';
@@ -0,0 +1,51 @@
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 { Args, Command, Flags } from '@oclif/core';
10
+ import { runNocoBaseCommand } from "../lib/run-npm.js";
11
+ export default class Build extends Command {
12
+ static args = {
13
+ /** Matches `nb build @nocobase/acl @nocobase/actions` — zero or more package names. */
14
+ packages: Args.string({
15
+ description: 'package names to build',
16
+ multiple: true,
17
+ required: false,
18
+ }),
19
+ };
20
+ static description = 'Run the legacy NocoBase build (forwards to `npm run build` in the repo root)';
21
+ static examples = [
22
+ '<%= config.bin %> <%= command.id %>',
23
+ '<%= config.bin %> <%= command.id %> --no-dts',
24
+ '<%= config.bin %> <%= command.id %> --sourcemap',
25
+ '<%= config.bin %> <%= command.id %> @nocobase/acl',
26
+ '<%= config.bin %> <%= command.id %> @nocobase/acl @nocobase/actions',
27
+ ];
28
+ static flags = {
29
+ env: Flags.string({ description: 'Environment', char: 'e', required: false }),
30
+ 'no-dts': Flags.boolean({ description: 'not generate dts' }),
31
+ sourcemap: Flags.boolean({ description: 'generate sourcemap' }),
32
+ };
33
+ async run() {
34
+ const { args, flags } = await this.parse(Build);
35
+ const packages = args.packages ?? [];
36
+ const npmArgs = ['build', ...packages];
37
+ if (flags['no-dts']) {
38
+ npmArgs.push('--no-dts');
39
+ }
40
+ if (flags.sourcemap) {
41
+ npmArgs.push('--sourcemap');
42
+ }
43
+ try {
44
+ await runNocoBaseCommand(npmArgs, process.cwd());
45
+ }
46
+ catch (error) {
47
+ const message = error instanceof Error ? error.message : String(error);
48
+ this.error(message);
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,58 @@
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 { Args, Command, Flags } from '@oclif/core';
10
+ import { runNocoBaseCommand } from "../lib/run-npm.js";
11
+ export default class Dev extends Command {
12
+ static args = {
13
+ file: Args.string({ description: 'file to read' }),
14
+ };
15
+ static description = 'describe the command here';
16
+ static examples = [
17
+ '<%= config.bin %> <%= command.id %>',
18
+ '<%= config.bin %> <%= command.id %> --db-sync',
19
+ '<%= config.bin %> <%= command.id %> --port 12000',
20
+ '<%= config.bin %> <%= command.id %> --client',
21
+ '<%= config.bin %> <%= command.id %> --server',
22
+ '<%= config.bin %> <%= command.id %> --inspect 9229',
23
+ ];
24
+ static flags = {
25
+ env: Flags.string({ description: 'Environment', char: 'e', required: false }),
26
+ 'db-sync': Flags.boolean({ description: 'Sync the database', required: false }),
27
+ port: Flags.string({ description: 'Port', char: 'p', required: false }),
28
+ client: Flags.boolean({ description: 'Client', char: 'c', required: false }),
29
+ server: Flags.boolean({ description: 'Server', char: 's', required: false }),
30
+ inspect: Flags.string({ description: 'Inspect port', char: 'i', required: false }),
31
+ };
32
+ async run() {
33
+ const { flags } = await this.parse(Dev);
34
+ const npmArgs = ['dev', '--rsbuild'];
35
+ if (flags['db-sync']) {
36
+ npmArgs.push('--db-sync');
37
+ }
38
+ if (flags.port) {
39
+ npmArgs.push('--port', flags.port);
40
+ }
41
+ if (flags.client) {
42
+ npmArgs.push('--client');
43
+ }
44
+ if (flags.server) {
45
+ npmArgs.push('--server');
46
+ }
47
+ if (flags.inspect) {
48
+ npmArgs.push('--inspect', flags.inspect);
49
+ }
50
+ try {
51
+ await runNocoBaseCommand(npmArgs, process.cwd());
52
+ }
53
+ catch (error) {
54
+ const message = error instanceof Error ? error.message : String(error);
55
+ this.error(message);
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,121 @@
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 fsp from 'node:fs/promises';
10
+ import { Command, Flags } from '@oclif/core';
11
+ import path from 'node:path';
12
+ import { run } from "../lib/run-npm.js";
13
+ export default class Download extends Command {
14
+ static description = 'Download a NocoBase app from npm (create-nocobase-app), Docker Hub, or git.';
15
+ static examples = [
16
+ '<%= config.bin %> <%= command.id %> -s npm -v latest --app-root-path=./app --env-file=.env',
17
+ '<%= config.bin %> <%= command.id %> -s docker -v latest --docker-registry=nocobase/nocobase',
18
+ '<%= config.bin %> <%= command.id %> -s git -v main --git-url=https://github.com/nocobase/nocobase.git --app-root-path=./my-nocobase-app',
19
+ ];
20
+ static flags = {
21
+ source: Flags.string({
22
+ char: 's',
23
+ description: 'Where to download from',
24
+ options: ['docker', 'npm', 'git'],
25
+ required: true,
26
+ }),
27
+ version: Flags.string({
28
+ char: 'v',
29
+ default: 'latest',
30
+ description: 'Version or tag: npm package dist-tag/version, Docker image tag, or git branch/tag',
31
+ }),
32
+ 'force-overwrite': Flags.boolean({
33
+ char: 'f',
34
+ description: 'Pass --force to create-nocobase-app (npm) or overwrite when supported',
35
+ default: false,
36
+ }),
37
+ 'dev': Flags.boolean({
38
+ description: 'Include dev dependencies in the app directory',
39
+ default: false,
40
+ }),
41
+ 'env-file': Flags.string({
42
+ description: 'Dotenv file whose variables are passed to create-nocobase-app as -e KEY=value (npm only)',
43
+ }),
44
+ 'app-root-path': Flags.string({
45
+ description: 'Project directory name or path for the new app (default: my-nocobase-app)',
46
+ default: 'my-nocobase-app',
47
+ }),
48
+ 'db-password': Flags.string({ description: 'DB password (npm: DB_PASSWORD)' }),
49
+ 'git-url': Flags.string({
50
+ description: 'Git remote URL to clone (git source)',
51
+ }),
52
+ 'docker-registry': Flags.string({
53
+ description: 'Docker image repository without tag (default: nocobase/nocobase). Example: ghcr.io/nocobase/nocobase',
54
+ }),
55
+ };
56
+ async downloadFromDocker(flags) {
57
+ const image = flags['docker-registry'] ?? 'nocobase/nocobase';
58
+ const tag = flags.version ?? 'latest';
59
+ await run('docker', ['pull', `${image}:${tag}`], process.cwd());
60
+ }
61
+ async downloadFromNpm(flags) {
62
+ const appRoot = flags['app-root-path'] ?? 'my-nocobase-app';
63
+ const versionSpec = flags.version || 'latest';
64
+ const npxArgs = ['-y', `create-nocobase-app@${versionSpec}`, appRoot];
65
+ if (!flags['--dev']) {
66
+ npxArgs.push('--skip-dev-dependencies');
67
+ }
68
+ if (flags['force-overwrite']) {
69
+ await fsp.rm(path.resolve(process.cwd(), appRoot), { recursive: true, force: true });
70
+ }
71
+ await run('npx', npxArgs, process.cwd());
72
+ const installArgs = ['install'];
73
+ if (!flags['--dev']) {
74
+ installArgs.push('--production');
75
+ }
76
+ await run('yarn', installArgs, path.resolve(process.cwd(), appRoot));
77
+ }
78
+ async downloadFromGit(flags) {
79
+ const repoUrl = flags['git-url'] ?? 'https://github.com/nocobase/nocobase.git';
80
+ const appRoot = flags['app-root-path'] ?? 'my-nocobase-app';
81
+ const versionToRef = {
82
+ 'latest': 'main',
83
+ 'beta': 'next',
84
+ 'alpha': 'develop',
85
+ };
86
+ if (flags['force-overwrite']) {
87
+ await fsp.rm(path.resolve(process.cwd(), appRoot), { recursive: true, force: true });
88
+ }
89
+ const branch = versionToRef[flags.version || 'latest'] || flags.version || 'latest';
90
+ const gitArgs = ['clone'];
91
+ gitArgs.push('--branch', branch);
92
+ gitArgs.push('--depth', '1', repoUrl, appRoot);
93
+ await run('git', gitArgs, process.cwd());
94
+ await run('yarn', ['install'], path.resolve(process.cwd(), appRoot));
95
+ }
96
+ async download() {
97
+ const { flags } = await this.parse(Download);
98
+ switch (flags.source) {
99
+ case 'npm':
100
+ await this.downloadFromNpm(flags);
101
+ break;
102
+ case 'docker':
103
+ await this.downloadFromDocker(flags);
104
+ break;
105
+ case 'git':
106
+ await this.downloadFromGit(flags);
107
+ break;
108
+ default:
109
+ this.error(`Invalid source: ${flags.source}`);
110
+ }
111
+ }
112
+ async run() {
113
+ try {
114
+ await this.download();
115
+ }
116
+ catch (error) {
117
+ const message = error instanceof Error ? error.message : String(error);
118
+ this.error(message);
119
+ }
120
+ }
121
+ }
@@ -0,0 +1,55 @@
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 { runNocoBaseCommand } from "../lib/run-npm.js";
11
+ export default class Install extends Command {
12
+ static description = 'Run the legacy NocoBase install (forwards to `npm run install` in the repo root)';
13
+ static examples = [
14
+ '<%= config.bin %> <%= command.id %>',
15
+ '<%= config.bin %> <%= command.id %> -f',
16
+ '<%= config.bin %> <%= command.id %> -l zh-CN',
17
+ '<%= config.bin %> <%= command.id %> -m demo@nocobase.com',
18
+ '<%= config.bin %> <%= command.id %> -p admin123',
19
+ '<%= config.bin %> <%= command.id %> -n "Super Admin"',
20
+ ];
21
+ static flags = {
22
+ env: Flags.string({ description: 'Environment', char: 'e', required: false }),
23
+ force: Flags.boolean({ description: 'Reinstall the application by clearing the database', char: 'f', required: false }),
24
+ lang: Flags.string({ description: 'Language during installation', char: 'l', required: false }),
25
+ rootEmail: Flags.string({ description: 'Root user email', char: 'm', required: false }),
26
+ rootPassword: Flags.string({ description: 'Root user password', char: 'p', required: false }),
27
+ rootNickname: Flags.string({ description: 'Root user nickname', char: 'n', required: false }),
28
+ };
29
+ async run() {
30
+ const { flags } = await this.parse(Install);
31
+ const npmArgs = ['install'];
32
+ if (flags.force) {
33
+ npmArgs.push('--force');
34
+ }
35
+ if (flags.lang !== undefined) {
36
+ npmArgs.push('--lang', flags.lang);
37
+ }
38
+ if (flags.rootEmail !== undefined) {
39
+ npmArgs.push('--root-email', flags.rootEmail);
40
+ }
41
+ if (flags.rootPassword !== undefined) {
42
+ npmArgs.push('--root-password', flags.rootPassword);
43
+ }
44
+ if (flags.rootNickname !== undefined) {
45
+ npmArgs.push('--root-nickname', flags.rootNickname);
46
+ }
47
+ try {
48
+ await runNocoBaseCommand(npmArgs, process.cwd());
49
+ }
50
+ catch (error) {
51
+ const message = error instanceof Error ? error.message : String(error);
52
+ this.error(message);
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,32 @@
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 { Args, Command, Flags } from '@oclif/core';
10
+ export default class PmDisable extends Command {
11
+ static args = {
12
+ file: Args.string({ description: 'file to read' }),
13
+ };
14
+ static description = 'describe the command here';
15
+ static examples = [
16
+ '<%= config.bin %> <%= command.id %>',
17
+ ];
18
+ static flags = {
19
+ // flag with no value (-f, --force)
20
+ force: Flags.boolean({ char: 'f' }),
21
+ // flag with a value (-n, --name=VALUE)
22
+ name: Flags.string({ char: 'n', description: 'name to print' }),
23
+ };
24
+ async run() {
25
+ const { args, flags } = await this.parse(PmDisable);
26
+ const name = flags.name ?? 'world';
27
+ this.log(`hello ${name} from packages/core/cli/src/commands/pm/disable.ts`);
28
+ if (args.file && flags.force) {
29
+ this.log(`you input --force and --file: ${args.file}`);
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,32 @@
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 { Args, Command, Flags } from '@oclif/core';
10
+ export default class PmEnable extends Command {
11
+ static args = {
12
+ file: Args.string({ description: 'file to read' }),
13
+ };
14
+ static description = 'describe the command here';
15
+ static examples = [
16
+ '<%= config.bin %> <%= command.id %>',
17
+ ];
18
+ static flags = {
19
+ // flag with no value (-f, --force)
20
+ force: Flags.boolean({ char: 'f' }),
21
+ // flag with a value (-n, --name=VALUE)
22
+ name: Flags.string({ char: 'n', description: 'name to print' }),
23
+ };
24
+ async run() {
25
+ const { args, flags } = await this.parse(PmEnable);
26
+ const name = flags.name ?? 'world';
27
+ this.log(`hello ${name} from packages/core/cli/src/commands/pm/enable.ts`);
28
+ if (args.file && flags.force) {
29
+ this.log(`you input --force and --file: ${args.file}`);
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,32 @@
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 { Args, Command, Flags } from '@oclif/core';
10
+ export default class PmList extends Command {
11
+ static args = {
12
+ file: Args.string({ description: 'file to read' }),
13
+ };
14
+ static description = 'describe the command here';
15
+ static examples = [
16
+ '<%= config.bin %> <%= command.id %>',
17
+ ];
18
+ static flags = {
19
+ // flag with no value (-f, --force)
20
+ force: Flags.boolean({ char: 'f' }),
21
+ // flag with a value (-n, --name=VALUE)
22
+ name: Flags.string({ char: 'n', description: 'name to print' }),
23
+ };
24
+ async run() {
25
+ const { args, flags } = await this.parse(PmList);
26
+ const name = flags.name ?? 'world';
27
+ this.log(`hello ${name} from packages/core/cli/src/commands/pm/list.ts`);
28
+ if (args.file && flags.force) {
29
+ this.log(`you input --force and --file: ${args.file}`);
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,32 @@
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 { Args, Command, Flags } from '@oclif/core';
10
+ export default class Restart extends Command {
11
+ static args = {
12
+ file: Args.string({ description: 'file to read' }),
13
+ };
14
+ static description = 'describe the command here';
15
+ static examples = [
16
+ '<%= config.bin %> <%= command.id %>',
17
+ ];
18
+ static flags = {
19
+ // flag with no value (-f, --force)
20
+ force: Flags.boolean({ char: 'f' }),
21
+ // flag with a value (-n, --name=VALUE)
22
+ name: Flags.string({ char: 'n', description: 'name to print' }),
23
+ };
24
+ async run() {
25
+ const { args, flags } = await this.parse(Restart);
26
+ const name = flags.name ?? 'world';
27
+ this.log(`hello ${name} from packages/core/cli/src/commands/restart.ts`);
28
+ if (args.file && flags.force) {
29
+ this.log(`you input --force and --file: ${args.file}`);
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,38 @@
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 { Args, Command, Flags } from '@oclif/core';
10
+ import { runNocoBaseCommand } from "../../lib/run-npm.js";
11
+ export default class ScaffoldMigration extends Command {
12
+ static args = {
13
+ name: Args.string({ description: 'migration name', required: true }),
14
+ };
15
+ static description = 'Run the legacy NocoBase scaffold migration (forwards to `npm run scaffold:migration` in the repo root)';
16
+ static examples = [
17
+ '<%= config.bin %> <%= command.id %> migration-name --pkg @nocobase/plugin-acl',
18
+ '<%= config.bin %> <%= command.id %> migration-name --pkg @nocobase/plugin-acl --on afterLoad',
19
+ ];
20
+ static flags = {
21
+ pkg: Flags.string({ description: 'plugin package name', required: true }),
22
+ on: Flags.string({ description: 'on', required: false, options: ['beforeLoad', 'afterSync', 'afterLoad'] }),
23
+ };
24
+ async run() {
25
+ const { args, flags } = await this.parse(ScaffoldMigration);
26
+ const npmArgs = ['create-migration', args.name, '--pkg', flags.pkg];
27
+ if (flags.on) {
28
+ npmArgs.push('--on', flags.on);
29
+ }
30
+ try {
31
+ await runNocoBaseCommand(npmArgs, process.cwd(), { env: { LOGGER_SILENT: 'true' } });
32
+ }
33
+ catch (error) {
34
+ const message = error instanceof Error ? error.message : String(error);
35
+ this.error(message);
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,37 @@
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 { Args, Command, Flags } from '@oclif/core';
10
+ import { runNocoBaseCommand } from "../../lib/run-npm.js";
11
+ export default class ScaffoldPlugin extends Command {
12
+ static args = {
13
+ pkg: Args.string({ description: 'plugin package name', required: true }),
14
+ };
15
+ static description = 'Run the legacy NocoBase scaffold plugin (forwards to `npm run scaffold:plugin` in the repo root)';
16
+ static examples = [
17
+ '<%= config.bin %> <%= command.id %> @nocobase-example/plugin-hello',
18
+ '<%= config.bin %> <%= command.id %> @nocobase-example/plugin-hello --force-recreate',
19
+ ];
20
+ static flags = {
21
+ 'force-recreate': Flags.boolean({ description: 'Force recreate the plugin', char: 'f', required: false }),
22
+ };
23
+ async run() {
24
+ const { args, flags } = await this.parse(ScaffoldPlugin);
25
+ const npmArgs = ['pm', 'create', args.pkg];
26
+ if (flags['force-recreate']) {
27
+ npmArgs.push('--force-recreate');
28
+ }
29
+ try {
30
+ await runNocoBaseCommand(npmArgs, process.cwd(), { env: { LOGGER_SILENT: 'true' } });
31
+ }
32
+ catch (error) {
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ this.error(message);
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,46 @@
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 } from '@oclif/core';
10
+ import { spawn } from 'node:child_process';
11
+ function runNpmInstallGlobal(args) {
12
+ return new Promise((resolve, reject) => {
13
+ const child = spawn('npm', ['install', '-g', ...args], {
14
+ stdio: 'inherit',
15
+ shell: true,
16
+ env: process.env,
17
+ });
18
+ child.once('error', reject);
19
+ child.once('close', (code) => {
20
+ if (code === 0) {
21
+ resolve();
22
+ return;
23
+ }
24
+ reject(new Error(`npm exited with code ${code}`));
25
+ });
26
+ });
27
+ }
28
+ export default class SelfUpdate extends Command {
29
+ static description = 'Update the NocoBase CLI to the latest version';
30
+ static examples = ['<%= config.bin %> <%= command.id %>'];
31
+ async run() {
32
+ this.log('Updating NocoBase CLI to the latest version...');
33
+ try {
34
+ await runNpmInstallGlobal(['@nocobase/cli']);
35
+ this.log('Update finished.');
36
+ }
37
+ catch (error) {
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ this.error([
40
+ 'Failed to update the CLI.',
41
+ 'Try running manually: npm install -g @nocobase/cli',
42
+ message,
43
+ ].join('\n'), { exit: 1 });
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,55 @@
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 { runNocoBaseCommand } from "../lib/run-npm.js";
11
+ export default class Start extends Command {
12
+ static description = 'Run the legacy NocoBase start (forwards to `npm run start` in the repo root)';
13
+ static examples = [
14
+ '<%= config.bin %> <%= command.id %>',
15
+ '<%= config.bin %> <%= command.id %> --quickstart',
16
+ '<%= config.bin %> <%= command.id %> --port 12000',
17
+ '<%= config.bin %> <%= command.id %> --daemon',
18
+ '<%= config.bin %> <%= command.id %> --instances 2',
19
+ '<%= config.bin %> <%= command.id %> --launch-mode pm2',
20
+ ];
21
+ static flags = {
22
+ env: Flags.string({ description: 'Environment', char: 'e', required: false }),
23
+ quickstart: Flags.boolean({ description: 'Quickstart the application', required: false }),
24
+ port: Flags.string({ description: 'Port', char: 'p', required: false }),
25
+ daemon: Flags.boolean({ description: 'Run the application as a daemon', char: 'd', required: false }),
26
+ instances: Flags.integer({ description: 'Number of instances to run', char: 'i', required: false }),
27
+ 'launch-mode': Flags.string({ description: 'Launch Mode', required: false, options: ['pm2', 'node'] }),
28
+ };
29
+ async run() {
30
+ const { flags } = await this.parse(Start);
31
+ const npmArgs = ['start'];
32
+ if (flags.quickstart) {
33
+ npmArgs.push('--quickstart');
34
+ }
35
+ if (flags.port) {
36
+ npmArgs.push('--port', flags.port);
37
+ }
38
+ if (flags.daemon) {
39
+ npmArgs.push('--daemon');
40
+ }
41
+ if (flags.instances) {
42
+ npmArgs.push('--instances', flags.instances.toString());
43
+ }
44
+ if (flags['launch-mode']) {
45
+ npmArgs.push('--launch-mode', flags['launch-mode']);
46
+ }
47
+ try {
48
+ await runNocoBaseCommand(npmArgs, process.cwd());
49
+ }
50
+ catch (error) {
51
+ const message = error instanceof Error ? error.message : String(error);
52
+ this.error(message);
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,35 @@
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 { runNocoBaseCommand } from "../lib/run-npm.js";
11
+ export default class Upgrade extends Command {
12
+ static description = 'Run the legacy NocoBase upgrade (forwards to `npm run upgrade` in the repo root)';
13
+ static examples = [
14
+ '<%= config.bin %> <%= command.id %>',
15
+ '<%= config.bin %> <%= command.id %> --skip-code-update',
16
+ ];
17
+ static flags = {
18
+ env: Flags.string({ description: 'Environment', char: 'e', required: false }),
19
+ 'skip-code-update': Flags.boolean({ description: 'Skip code update', char: 'S', required: false }),
20
+ };
21
+ async run() {
22
+ const { flags } = await this.parse(Upgrade);
23
+ const npmArgs = ['upgrade'];
24
+ if (flags['skip-code-update']) {
25
+ npmArgs.push('--skip-code-update');
26
+ }
27
+ try {
28
+ await runNocoBaseCommand(npmArgs, process.cwd());
29
+ }
30
+ catch (error) {
31
+ const message = error instanceof Error ? error.message : String(error);
32
+ this.error(message);
33
+ }
34
+ }
35
+ }
@@ -6,22 +6,34 @@
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
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
10
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
11
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
12
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
13
+ });
14
+ }
15
+ return path;
16
+ };
9
17
  import { Command } from '@oclif/core';
10
- import EnvAdd from "../commands/env/add.js";
11
- import EnvAuth from "../commands/env/auth.js";
12
- import EnvList from "../commands/env/list.js";
13
- import EnvRemove from "../commands/env/remove.js";
14
- import EnvUpdate from "../commands/env/update.js";
15
- import EnvUse from "../commands/env/use.js";
16
- import ResourceCreate from "../commands/resource/create.js";
17
- import ResourceDestroy from "../commands/resource/destroy.js";
18
- import ResourceGet from "../commands/resource/get.js";
19
- import ResourceList from "../commands/resource/list.js";
20
- import ResourceQuery from "../commands/resource/query.js";
21
- import ResourceUpdate from "../commands/resource/update.js";
18
+ import { dirname, join, relative } from 'node:path';
19
+ import { fileURLToPath, pathToFileURL } from 'node:url';
20
+ import { collectCommandModulePaths, commandRelativePathToRegistryKey, } from "../lib/command-discovery.js";
22
21
  import { getCurrentEnvName, getEnv } from "../lib/auth-store.js";
23
22
  import { createGeneratedFlags, GeneratedApiCommand } from "../lib/generated-command.js";
24
23
  import { loadRuntimeSync } from "../lib/runtime-store.js";
24
+ const registryFilePath = fileURLToPath(import.meta.url);
25
+ const commandsRoot = join(dirname(registryFilePath), '../commands');
26
+ const commandModuleExtension = registryFilePath.endsWith('.ts') ? '.ts' : '.js';
27
+ async function loadCommandsFromDirectory() {
28
+ const absolutePaths = await collectCommandModulePaths(commandsRoot, commandModuleExtension);
29
+ const entries = await Promise.all(absolutePaths.map(async (absolutePath) => {
30
+ const rel = relative(commandsRoot, absolutePath).replace(/\\/g, '/');
31
+ const key = commandRelativePathToRegistryKey(rel);
32
+ const mod = await import(__rewriteRelativeImportExtension(pathToFileURL(absolutePath).href));
33
+ return [key, mod.default];
34
+ }));
35
+ return Object.fromEntries(entries);
36
+ }
25
37
  function readEnvName(argv) {
26
38
  for (let index = 0; index < argv.length; index += 1) {
27
39
  const token = argv[index];
@@ -56,20 +68,7 @@ function createRuntimeIndexCommand(commandId, operation) {
56
68
  };
57
69
  }
58
70
  const registry = {
59
- // env: Env,
60
- 'env:add': EnvAdd,
61
- 'env:auth': EnvAuth,
62
- 'env:list': EnvList,
63
- 'env:remove': EnvRemove,
64
- 'env:update': EnvUpdate,
65
- 'env:use': EnvUse,
66
- // 'api:resource': Resource,
67
- 'api:resource:create': ResourceCreate,
68
- 'api:resource:destroy': ResourceDestroy,
69
- 'api:resource:get': ResourceGet,
70
- 'api:resource:list': ResourceList,
71
- 'api:resource:query': ResourceQuery,
72
- 'api:resource:update': ResourceUpdate,
71
+ ...(await loadCommandsFromDirectory()),
73
72
  };
74
73
  const envName = readEnvName(process.argv.slice(2)) ?? (await getCurrentEnvName());
75
74
  const env = await getEnv(envName);
@@ -1,8 +1,16 @@
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
+ */
1
9
  import { getCurrentEnvName, getEnv, setEnvRuntime, updateEnvConnection } from './auth-store.js';
2
10
  import { resolveAccessToken } from './env-auth.js';
3
11
  import { generateRuntime } from './runtime-generator.js';
4
12
  import { hasRuntimeSync, saveRuntime } from './runtime-store.js';
5
- import { confirmAction, printInfo, printVerbose, printWarning, setVerboseMode, stopTask, updateTask } from './ui.js';
13
+ import { confirmAction, printInfo, printVerbose, printWarningBlock, setVerboseMode, stopTask, updateTask } from './ui.js';
6
14
  const APP_RETRY_INTERVAL = 2000;
7
15
  const APP_RETRY_TIMEOUT = 120000;
8
16
  function readFlag(argv, name) {
@@ -66,6 +74,9 @@ function isBuiltinCommand(argv) {
66
74
  export function shouldSkipRuntimeBootstrap(argv) {
67
75
  return hasVersionFlag(argv) || isBuiltinCommand(argv);
68
76
  }
77
+ function shouldIgnoreBootstrapFailure(argv, commandToken) {
78
+ return !commandToken || hasHelpFlag(argv) || (commandToken === 'api' && argv.length === 1);
79
+ }
69
80
  async function requestJson(url, options) {
70
81
  const headers = new Headers();
71
82
  if (options.token) {
@@ -215,6 +226,9 @@ function collectErrorEntries(data) {
215
226
  function hasInvalidTokenError(data) {
216
227
  return collectErrorEntries(data).some((entry) => entry?.code === 'INVALID_TOKEN');
217
228
  }
229
+ function isNetworkFetchFailure(response) {
230
+ return response.status === 0;
231
+ }
218
232
  export function formatSwaggerSchemaError(response, context) {
219
233
  if (hasInvalidTokenError(response.data)) {
220
234
  const entries = collectErrorEntries(response.data);
@@ -236,6 +250,17 @@ export function formatSwaggerSchemaError(response, context) {
236
250
  commandHint,
237
251
  ].join('\n');
238
252
  }
253
+ if (isNetworkFetchFailure(response)) {
254
+ const rawMessage = response.data?.error?.message || 'fetch failed';
255
+ return [
256
+ 'Failed to reach the NocoBase server while loading the command runtime from `swagger:get`.',
257
+ `Base URL: ${context.baseUrl}`,
258
+ `Network error: ${rawMessage}`,
259
+ 'Check that the NocoBase app is running, the base URL is correct, and the server is reachable from this machine.',
260
+ 'If you recently changed the server address, update it with `nb env add --name <name> --base-url <url>` and retry `nb env update`.',
261
+ 'Use `nb env list` to inspect the current env configuration.',
262
+ ].join('\n');
263
+ }
239
264
  return `Failed to load swagger schema from \`swagger:get\`.\n${JSON.stringify(response.data, null, 2)}`;
240
265
  }
241
266
  export function formatMissingRuntimeEnvError(commandToken) {
@@ -256,55 +281,59 @@ export function formatMissingRuntimeEnvError(commandToken) {
256
281
  export async function ensureRuntimeFromArgv(argv, options) {
257
282
  const commandToken = getCommandToken(argv);
258
283
  const isRootInvocation = !commandToken;
284
+ const canContinueWithoutRuntime = shouldIgnoreBootstrapFailure(argv, commandToken);
259
285
  setVerboseMode(hasBooleanFlag(argv, 'verbose'));
260
286
  if (shouldSkipRuntimeBootstrap(argv)) {
261
287
  return;
262
288
  }
263
- const envName = readFlag(argv, 'env') ?? (await getCurrentEnvName());
264
- const env = await getEnv(envName);
265
- const baseUrl = readFlag(argv, 'base-url') ?? env?.baseUrl;
266
- const role = readFlag(argv, 'role');
267
- const token = await resolveAccessToken({
268
- envName,
269
- baseUrl,
270
- token: readFlag(argv, 'token'),
271
- });
272
- const runtimeVersion = env?.runtime?.version;
273
- if (runtimeVersion && hasRuntimeSync(runtimeVersion)) {
274
- return;
275
- }
276
- if (!baseUrl) {
277
- if (isRootInvocation) {
289
+ try {
290
+ const envName = readFlag(argv, 'env') ?? (await getCurrentEnvName());
291
+ const env = await getEnv(envName);
292
+ const baseUrl = readFlag(argv, 'base-url') ?? env?.baseUrl;
293
+ const role = readFlag(argv, 'role');
294
+ const token = await resolveAccessToken({
295
+ envName,
296
+ baseUrl,
297
+ token: readFlag(argv, 'token'),
298
+ });
299
+ const runtimeVersion = env?.runtime?.version;
300
+ if (runtimeVersion && hasRuntimeSync(runtimeVersion)) {
278
301
  return;
279
302
  }
280
- throw new Error(formatMissingRuntimeEnvError(commandToken));
281
- }
282
- updateTask('Loading command runtime...');
283
- try {
284
- printVerbose(`Runtime source: ${baseUrl}`);
285
- const document = await fetchSwaggerSchema(baseUrl, token, role, { envName, commandToken }, isRootInvocation
286
- ? {
287
- allowEnableApiDoc: false,
288
- retryAppAvailability: false,
303
+ if (!baseUrl) {
304
+ if (isRootInvocation) {
305
+ return;
289
306
  }
290
- : undefined);
291
- const runtime = await generateRuntime(document, options.configFile, baseUrl);
292
- await saveRuntime(runtime);
293
- await setEnvRuntime(envName, {
294
- version: runtime.version,
295
- schemaHash: runtime.schemaHash,
296
- generatedAt: runtime.generatedAt,
297
- });
307
+ throw new Error(formatMissingRuntimeEnvError(commandToken));
308
+ }
309
+ updateTask('Loading command runtime...');
310
+ try {
311
+ printVerbose(`Runtime source: ${baseUrl}`);
312
+ const document = await fetchSwaggerSchema(baseUrl, token, role, { envName, commandToken }, isRootInvocation
313
+ ? {
314
+ allowEnableApiDoc: false,
315
+ retryAppAvailability: false,
316
+ }
317
+ : undefined);
318
+ const runtime = await generateRuntime(document, options.configFile, baseUrl);
319
+ await saveRuntime(runtime);
320
+ await setEnvRuntime(envName, {
321
+ version: runtime.version,
322
+ schemaHash: runtime.schemaHash,
323
+ generatedAt: runtime.generatedAt,
324
+ });
325
+ }
326
+ finally {
327
+ stopTask();
328
+ }
298
329
  }
299
330
  catch (error) {
300
- if (!isRootInvocation) {
331
+ if (!canContinueWithoutRuntime) {
301
332
  throw error;
302
333
  }
303
- const message = error instanceof Error ? error.message : String(error);
304
- printWarning(`${message}\nContinuing with built-in help because runtime commands could not be loaded.`);
305
- }
306
- finally {
307
334
  stopTask();
335
+ const message = error instanceof Error ? error.message : String(error);
336
+ printWarningBlock(`Unable to load runtime commands. Showing built-in help instead.\n\n${message}`);
308
337
  }
309
338
  }
310
339
  export async function updateEnvRuntime(options) {
@@ -0,0 +1,39 @@
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 { readdir } from 'node:fs/promises';
10
+ import { join } from 'node:path';
11
+ /**
12
+ * Recursively collect command module paths under `commandsRoot` (e.g. `dist/commands` → `.js`, `src/commands` → `.ts`).
13
+ */
14
+ export async function collectCommandModulePaths(commandsRoot, extension) {
15
+ const entries = await readdir(commandsRoot, { withFileTypes: true });
16
+ const files = [];
17
+ for (const ent of entries) {
18
+ const full = join(commandsRoot, ent.name);
19
+ if (ent.isDirectory()) {
20
+ files.push(...(await collectCommandModulePaths(full, extension)));
21
+ }
22
+ else if (ent.isFile() && ent.name.endsWith(extension)) {
23
+ files.push(full);
24
+ }
25
+ }
26
+ return files.sort();
27
+ }
28
+ /**
29
+ * Map a path relative to `commands/` with `.js` / `.ts` to an oclif explicit-registry key.
30
+ * `resource/foo.js` → `api:resource:foo` (topic `api resource …`); other paths use `:` between segments.
31
+ */
32
+ export function commandRelativePathToRegistryKey(relativePath) {
33
+ const normalized = relativePath.replace(/\\/g, '/').replace(/\.(js|ts)$/i, '');
34
+ const segments = normalized.split('/').filter(Boolean);
35
+ // if (segments[0] === 'resource' && segments.length >= 2) {
36
+ // return ['api', 'resource', ...segments.slice(1)].join(':');
37
+ // }
38
+ return segments.join(':');
39
+ }
@@ -1,9 +1,17 @@
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
+ */
1
9
  import crypto from 'node:crypto';
2
10
  import { createServer } from 'node:http';
3
11
  import { spawn } from 'node:child_process';
4
12
  import { URL } from 'node:url';
5
13
  import { getCurrentEnvName, getEnv, setEnvOauthSession, } from './auth-store.js';
6
- import { printInfo, printVerbose, printWarning, updateTask } from './ui.js';
14
+ import { printInfo, printVerbose, printWarning, printWarningBlock, updateTask } from './ui.js';
7
15
  const ACCESS_TOKEN_REFRESH_WINDOW_MS = 60_000;
8
16
  const LOOPBACK_HOST = '127.0.0.1';
9
17
  const OAUTH_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
@@ -62,9 +70,36 @@ function formatOauthError(prefix, data, fallbackStatus) {
62
70
  }
63
71
  return prefix;
64
72
  }
65
- async function fetchOauthServerMetadata(baseUrl) {
73
+ function formatOauthFetchFailure(prefix, options) {
74
+ return [
75
+ prefix,
76
+ options.envName ? `Env: ${options.envName}` : undefined,
77
+ options.baseUrl ? `Base URL: ${options.baseUrl}` : undefined,
78
+ `Request URL: ${options.url}`,
79
+ `Network error: ${options.rawMessage || 'fetch failed'}`,
80
+ 'Check that the NocoBase app is running, the base URL is correct, and the server is reachable from this machine.',
81
+ options.envName
82
+ ? `If the saved login is stale, run \`nb env auth -e ${options.envName}\` again after connectivity is restored.`
83
+ : 'If the saved login is stale, run `nb env auth -e <name>` again after connectivity is restored.',
84
+ 'Use `nb env list` to inspect the current env configuration.',
85
+ ]
86
+ .filter(Boolean)
87
+ .join('\n');
88
+ }
89
+ async function fetchOauthServerMetadata(baseUrl, options = {}) {
66
90
  const metadataUrl = getOauthMetadataUrl(baseUrl);
67
- const response = await fetch(metadataUrl);
91
+ let response;
92
+ try {
93
+ response = await fetch(metadataUrl);
94
+ }
95
+ catch (error) {
96
+ throw new Error(formatOauthFetchFailure('Failed to load OAuth metadata.', {
97
+ envName: options.envName,
98
+ baseUrl,
99
+ url: metadataUrl,
100
+ rawMessage: error?.message,
101
+ }));
102
+ }
68
103
  const data = await parseJsonResponse(response);
69
104
  if (!response.ok) {
70
105
  throw new Error(formatOauthError(`Failed to load OAuth metadata from ${metadataUrl}`, data, response.status));
@@ -246,7 +281,7 @@ async function refreshOauthAccessToken(options) {
246
281
  if (!options.auth.refreshToken || !options.auth.clientId) {
247
282
  throw new Error(`OAuth session for env "${options.envName}" cannot be refreshed. Run \`nb env auth -e ${options.envName}\`.`);
248
283
  }
249
- const metadata = await fetchOauthServerMetadata(options.baseUrl);
284
+ const metadata = await fetchOauthServerMetadata(options.baseUrl, { envName: options.envName });
250
285
  const resource = options.auth.resource || getOauthResource(metadata.issuer);
251
286
  const body = new URLSearchParams({
252
287
  grant_type: 'refresh_token',
@@ -261,6 +296,13 @@ async function refreshOauthAccessToken(options) {
261
296
  'content-type': 'application/x-www-form-urlencoded',
262
297
  },
263
298
  body,
299
+ }).catch((error) => {
300
+ throw new Error(formatOauthFetchFailure(`Failed to refresh OAuth session for env "${options.envName}".`, {
301
+ envName: options.envName,
302
+ baseUrl: options.baseUrl,
303
+ url: metadata.token_endpoint,
304
+ rawMessage: error?.message,
305
+ }));
264
306
  });
265
307
  const data = await parseJsonResponse(response);
266
308
  if (!response.ok) {
@@ -338,7 +380,7 @@ export async function authenticateEnvWithOauth(options) {
338
380
  ].join('\n'));
339
381
  }
340
382
  updateTask(`Loading OAuth metadata for env "${envName}"...`);
341
- const metadata = await fetchOauthServerMetadata(baseUrl);
383
+ const metadata = await fetchOauthServerMetadata(baseUrl, { envName });
342
384
  const state = encodeBase64Url(crypto.randomBytes(16));
343
385
  const { codeVerifier, codeChallenge } = buildPkcePair();
344
386
  const callback = await createLoopbackServer(state);
@@ -359,7 +401,7 @@ export async function authenticateEnvWithOauth(options) {
359
401
  updateTask(`Waiting for OAuth login for env "${envName}"...`);
360
402
  const opened = maybeOpenBrowser(authorizationUrl.toString());
361
403
  if (!opened) {
362
- printWarning('Unable to open the browser automatically. Open this URL manually:');
404
+ printWarningBlock('Unable to open the browser automatically. Open this URL manually:');
363
405
  }
364
406
  else {
365
407
  printInfo('Complete the OAuth login in your browser.');
@@ -0,0 +1,74 @@
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 { spawn } from 'node:child_process';
10
+ export function run(name, args, cwd, options) {
11
+ return new Promise((resolve, reject) => {
12
+ const child = spawn(name, [...args], {
13
+ stdio: 'inherit',
14
+ shell: true,
15
+ cwd,
16
+ env: {
17
+ ...options?.env,
18
+ ...process.env,
19
+ },
20
+ });
21
+ child.once('error', reject);
22
+ child.once('close', (code) => {
23
+ if (code === 0) {
24
+ resolve();
25
+ return;
26
+ }
27
+ reject(new Error(`${name} exited with code ${code}`));
28
+ });
29
+ });
30
+ }
31
+ /** Run `npm` with the given argument list in `cwd`, inheriting stdio. */
32
+ export function runNpm(args, cwd) {
33
+ return new Promise((resolve, reject) => {
34
+ const child = spawn('yarn', [...args], {
35
+ stdio: 'inherit',
36
+ shell: true,
37
+ cwd,
38
+ env: process.env,
39
+ });
40
+ child.once('error', reject);
41
+ child.once('close', (code, signal) => {
42
+ if (code === 0) {
43
+ resolve();
44
+ return;
45
+ }
46
+ if (signal) {
47
+ reject(new Error(`npm exited due to signal ${signal}`));
48
+ return;
49
+ }
50
+ reject(new Error(`npm exited with code ${code}`));
51
+ });
52
+ });
53
+ }
54
+ export async function runNocoBaseCommand(args, cwd, options) {
55
+ return new Promise((resolve, reject) => {
56
+ const child = spawn('node', ['./node_modules/.bin/nocobase-v1', ...args], {
57
+ stdio: 'inherit',
58
+ shell: true,
59
+ cwd,
60
+ env: {
61
+ ...options?.env,
62
+ ...process.env,
63
+ },
64
+ });
65
+ child.once('error', reject);
66
+ child.once('close', (code) => {
67
+ if (code === 0) {
68
+ resolve();
69
+ return;
70
+ }
71
+ reject(new Error(`nocobase command exited with code ${code}`));
72
+ });
73
+ });
74
+ }
package/dist/lib/ui.js CHANGED
@@ -1,3 +1,11 @@
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
+ */
1
9
  import readline from 'node:readline/promises';
2
10
  import { stdin as input, stdout as output } from 'node:process';
3
11
  import ora from 'ora';
@@ -99,18 +107,19 @@ export function printSuccess(message) {
99
107
  }
100
108
  console.log(pc.green(message));
101
109
  }
102
- export function printWarning(message) {
110
+ function clearActiveSpinner() {
103
111
  if (activeSpinner) {
104
- if (!isInteractiveTerminal()) {
105
- activeSpinner = undefined;
106
- console.log(pc.yellow(message));
107
- return;
108
- }
109
- activeSpinner.warn(pc.yellow(message));
112
+ activeSpinner.stop();
110
113
  activeSpinner = undefined;
111
- return;
112
114
  }
113
- console.log(pc.yellow(message));
115
+ }
116
+ export function printWarning(message) {
117
+ clearActiveSpinner();
118
+ console.log(pc.yellow(`⚠ Warning: ${message}`));
119
+ }
120
+ export function printWarningBlock(message) {
121
+ clearActiveSpinner();
122
+ console.log(pc.yellow(`⚠ Warning\n${message}\n`));
114
123
  }
115
124
  export function printVerboseWarning(message) {
116
125
  if (!verboseMode) {
@@ -154,10 +163,7 @@ export function failTask(message) {
154
163
  console.error(pc.red(message));
155
164
  }
156
165
  export function stopTask() {
157
- if (activeSpinner) {
158
- activeSpinner.stop();
159
- activeSpinner = undefined;
160
- }
166
+ clearActiveSpinner();
161
167
  }
162
168
  export function renderTable(headers, rows) {
163
169
  const widths = headers.map((header, index) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/cli",
3
- "version": "2.1.0-alpha.17",
3
+ "version": "2.1.0-alpha.19",
4
4
  "description": "NocoBase Command Line Tool",
5
5
  "type": "module",
6
6
  "main": "dist/generated/command-registry.js",
@@ -59,5 +59,5 @@
59
59
  "type": "git",
60
60
  "url": "git+https://github.com/nocobase/nocobase.git"
61
61
  },
62
- "gitHead": "586cb00f56557e66168b9720d0e0193a1b752067"
62
+ "gitHead": "3d13700360eac1c0f9dbf6a5f167ed396a294a3c"
63
63
  }
@@ -1,8 +0,0 @@
1
- import { Command } from '@oclif/core';
2
- export default class Api extends Command {
3
- static summary = 'Work with NocoBase APIs, environments, resources, and runtime commands';
4
- static id = 'api';
5
- async run() {
6
- this.log('Use `nb api --help` to view available subcommands.');
7
- }
8
- }
@@ -1,27 +0,0 @@
1
- import { Command, Flags } from '@oclif/core';
2
- import { getCurrentEnvName, getEnv } from '../../lib/auth-store.js';
3
- import { formatCliHomeScope } from '../../lib/cli-home.js';
4
- import { renderTable } from '../../lib/ui.js';
5
- export default class Env extends Command {
6
- static summary = 'Show the current environment';
7
- static id = 'env';
8
- static flags = {
9
- scope: Flags.string({
10
- char: 's',
11
- description: 'Config scope',
12
- options: ['project', 'global'],
13
- }),
14
- };
15
- async run() {
16
- const { flags } = await this.parse(Env);
17
- const scope = flags.scope;
18
- const envName = await getCurrentEnvName({ scope });
19
- const env = await getEnv(envName, { scope });
20
- if (!env?.baseUrl) {
21
- this.log(`No current env is configured${scope ? ` in ${formatCliHomeScope(scope)} scope` : ''}.`);
22
- this.log('Run `nb env add --name <name> --base-url <url>` to add one.');
23
- return;
24
- }
25
- this.log(renderTable(['Name', 'Base URL', 'Auth', 'Runtime'], [[envName, env?.baseUrl ?? '', env?.auth?.type ?? '', env?.runtime?.version ?? '']]));
26
- }
27
- }
@@ -1,7 +0,0 @@
1
- import { Command } from '@oclif/core';
2
- export default class Resource extends Command {
3
- static summary = 'Work with generic collection resources';
4
- async run() {
5
- this.log('Use `nb api resource --help` to view available subcommands.');
6
- }
7
- }