@nocobase/cli 2.1.0-alpha.19 → 2.1.0-alpha.20
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 +15 -9
- package/bin/run.js +9 -3
- package/dist/commands/api/resource/index.js +20 -0
- package/dist/commands/build.js +2 -2
- package/dist/commands/db/start.js +22 -0
- package/dist/commands/dev.js +1 -1
- package/dist/commands/download.js +221 -49
- package/dist/commands/env/add.js +179 -34
- package/dist/commands/env/auth.js +31 -6
- package/dist/commands/env/list.js +12 -2
- package/dist/commands/env/remove.js +12 -1
- package/dist/commands/env/update.js +24 -9
- package/dist/commands/env/use.js +11 -1
- package/dist/commands/init.js +186 -0
- package/dist/commands/install.js +660 -12
- package/dist/commands/pm/disable.js +14 -15
- package/dist/commands/pm/enable.js +14 -15
- package/dist/commands/pm/list.js +5 -16
- package/dist/commands/scaffold/migration.js +1 -1
- package/dist/commands/scaffold/plugin.js +1 -1
- package/dist/commands/start.js +1 -1
- package/dist/commands/upgrade.js +1 -1
- package/dist/generated/command-registry.js +57 -11
- package/dist/help/runtime-help.js +20 -0
- package/dist/lib/auth-store.js +48 -3
- package/dist/lib/bootstrap.js +14 -9
- package/dist/lib/command-discovery.js +4 -4
- package/dist/lib/env-auth.js +95 -15
- package/dist/lib/init-browser-wizard.js +431 -0
- package/dist/lib/openapi.js +8 -200
- package/dist/lib/run-npm.js +27 -42
- package/nocobase-ctl.config.json +28 -68
- package/package.json +7 -6
- package/dist/commands/self-update.js +0 -46
|
@@ -6,27 +6,26 @@
|
|
|
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 { Args, Command
|
|
9
|
+
import { Args, Command } from '@oclif/core';
|
|
10
10
|
export default class PmDisable extends Command {
|
|
11
11
|
static args = {
|
|
12
|
-
|
|
12
|
+
packages: Args.string({
|
|
13
|
+
required: true,
|
|
14
|
+
multiple: true,
|
|
15
|
+
description: 'Plugin package name(s) to disable (e.g. `@nocobase/plugin-sample`). Pass one or more names as separate arguments.',
|
|
16
|
+
}),
|
|
13
17
|
};
|
|
14
|
-
static description = '
|
|
18
|
+
static description = 'Disable one or more plugins';
|
|
15
19
|
static examples = [
|
|
16
|
-
'<%= config.bin %> <%= command.id %>',
|
|
20
|
+
'<%= config.bin %> <%= command.id %> @nocobase/plugin-sample',
|
|
21
|
+
'<%= config.bin %> <%= command.id %> @nocobase/plugin-a @nocobase/plugin-b',
|
|
17
22
|
];
|
|
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
23
|
async run() {
|
|
25
|
-
const { args
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
this.log(`you input --force and --file: ${args.file}`);
|
|
24
|
+
const { args } = await this.parse(PmDisable);
|
|
25
|
+
const packages = args.packages;
|
|
26
|
+
if (!Array.isArray(packages) || packages.length === 0) {
|
|
27
|
+
this.error('Pass at least one plugin package name.');
|
|
30
28
|
}
|
|
29
|
+
await this.config.runCommand('api:pm:disable', ['--await-response', '--filter-by-tk', packages.join(',')]);
|
|
31
30
|
}
|
|
32
31
|
}
|
|
@@ -6,27 +6,26 @@
|
|
|
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 { Args, Command
|
|
9
|
+
import { Args, Command } from '@oclif/core';
|
|
10
10
|
export default class PmEnable extends Command {
|
|
11
11
|
static args = {
|
|
12
|
-
|
|
12
|
+
packages: Args.string({
|
|
13
|
+
required: true,
|
|
14
|
+
multiple: true,
|
|
15
|
+
description: 'Plugin package name(s) to enable (e.g. `@nocobase/plugin-sample`). Pass one or more names as separate arguments.',
|
|
16
|
+
}),
|
|
13
17
|
};
|
|
14
|
-
static description = '
|
|
18
|
+
static description = 'Enable one or more plugins';
|
|
15
19
|
static examples = [
|
|
16
|
-
'<%= config.bin %> <%= command.id %>',
|
|
20
|
+
'<%= config.bin %> <%= command.id %> @nocobase/plugin-sample',
|
|
21
|
+
'<%= config.bin %> <%= command.id %> @nocobase/plugin-a @nocobase/plugin-b',
|
|
17
22
|
];
|
|
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
23
|
async run() {
|
|
25
|
-
const { args
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
this.log(`you input --force and --file: ${args.file}`);
|
|
24
|
+
const { args } = await this.parse(PmEnable);
|
|
25
|
+
const packages = args.packages;
|
|
26
|
+
if (!Array.isArray(packages) || packages.length === 0) {
|
|
27
|
+
this.error('Pass at least one plugin package name.');
|
|
30
28
|
}
|
|
29
|
+
await this.config.runCommand('api:pm:enable', ['--await-response', '--filter-by-tk', packages.join(',')]);
|
|
31
30
|
}
|
|
32
31
|
}
|
package/dist/commands/pm/list.js
CHANGED
|
@@ -6,27 +6,16 @@
|
|
|
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 {
|
|
9
|
+
import { Command } from '@oclif/core';
|
|
10
10
|
export default class PmList extends Command {
|
|
11
|
-
static args = {
|
|
12
|
-
|
|
13
|
-
};
|
|
14
|
-
static description = 'describe the command here';
|
|
11
|
+
static args = {};
|
|
12
|
+
static summary = 'List all plugins';
|
|
15
13
|
static examples = [
|
|
16
14
|
'<%= config.bin %> <%= command.id %>',
|
|
17
15
|
];
|
|
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
|
-
};
|
|
16
|
+
static flags = {};
|
|
24
17
|
async run() {
|
|
25
18
|
const { args, flags } = await this.parse(PmList);
|
|
26
|
-
|
|
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
|
-
}
|
|
19
|
+
await this.config.runCommand('api:pm:list', ['--mode=summary']);
|
|
31
20
|
}
|
|
32
21
|
}
|
|
@@ -28,7 +28,7 @@ export default class ScaffoldMigration extends Command {
|
|
|
28
28
|
npmArgs.push('--on', flags.on);
|
|
29
29
|
}
|
|
30
30
|
try {
|
|
31
|
-
await runNocoBaseCommand(npmArgs,
|
|
31
|
+
await runNocoBaseCommand(npmArgs, { env: { LOGGER_SILENT: 'true' } });
|
|
32
32
|
}
|
|
33
33
|
catch (error) {
|
|
34
34
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -27,7 +27,7 @@ export default class ScaffoldPlugin extends Command {
|
|
|
27
27
|
npmArgs.push('--force-recreate');
|
|
28
28
|
}
|
|
29
29
|
try {
|
|
30
|
-
await runNocoBaseCommand(npmArgs,
|
|
30
|
+
await runNocoBaseCommand(npmArgs, { env: { LOGGER_SILENT: 'true' } });
|
|
31
31
|
}
|
|
32
32
|
catch (error) {
|
|
33
33
|
const message = error instanceof Error ? error.message : String(error);
|
package/dist/commands/start.js
CHANGED
|
@@ -45,7 +45,7 @@ export default class Start extends Command {
|
|
|
45
45
|
npmArgs.push('--launch-mode', flags['launch-mode']);
|
|
46
46
|
}
|
|
47
47
|
try {
|
|
48
|
-
await runNocoBaseCommand(npmArgs
|
|
48
|
+
await runNocoBaseCommand(npmArgs);
|
|
49
49
|
}
|
|
50
50
|
catch (error) {
|
|
51
51
|
const message = error instanceof Error ? error.message : String(error);
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -25,7 +25,7 @@ export default class Upgrade extends Command {
|
|
|
25
25
|
npmArgs.push('--skip-code-update');
|
|
26
26
|
}
|
|
27
27
|
try {
|
|
28
|
-
await runNocoBaseCommand(npmArgs
|
|
28
|
+
await runNocoBaseCommand(npmArgs);
|
|
29
29
|
}
|
|
30
30
|
catch (error) {
|
|
31
31
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -14,12 +14,13 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
14
14
|
}
|
|
15
15
|
return path;
|
|
16
16
|
};
|
|
17
|
-
import { Command } from '@oclif/core';
|
|
17
|
+
import { Command, loadHelpClass } from '@oclif/core';
|
|
18
18
|
import { dirname, join, relative } from 'node:path';
|
|
19
19
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
20
20
|
import { collectCommandModulePaths, commandRelativePathToRegistryKey, } from "../lib/command-discovery.js";
|
|
21
21
|
import { getCurrentEnvName, getEnv } from "../lib/auth-store.js";
|
|
22
22
|
import { createGeneratedFlags, GeneratedApiCommand } from "../lib/generated-command.js";
|
|
23
|
+
import { toKebabCase } from "../lib/naming.js";
|
|
23
24
|
import { loadRuntimeSync } from "../lib/runtime-store.js";
|
|
24
25
|
const registryFilePath = fileURLToPath(import.meta.url);
|
|
25
26
|
const commandsRoot = join(dirname(registryFilePath), '../commands');
|
|
@@ -58,15 +59,60 @@ function createRuntimeCommand(operation) {
|
|
|
58
59
|
static operation = operation;
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
|
-
function createRuntimeIndexCommand(commandId,
|
|
62
|
+
function createRuntimeIndexCommand(commandId, metadata) {
|
|
63
|
+
const summary = metadata.summary || `Work with ${commandId}`;
|
|
64
|
+
const description = metadata.description && metadata.description !== summary ? metadata.description : undefined;
|
|
62
65
|
return class RuntimeIndexCommand extends Command {
|
|
63
|
-
static summary =
|
|
64
|
-
static description =
|
|
66
|
+
static summary = summary;
|
|
67
|
+
static description = description;
|
|
65
68
|
async run() {
|
|
66
|
-
this.
|
|
69
|
+
await this.parse(RuntimeIndexCommand);
|
|
70
|
+
const Help = await loadHelpClass(this.config);
|
|
71
|
+
await new Help(this.config, this.config.pjson.oclif.helpOptions ?? this.config.pjson.helpOptions).showHelp([
|
|
72
|
+
this.id ?? commandId.replaceAll(' ', ':'),
|
|
73
|
+
...this.argv,
|
|
74
|
+
]);
|
|
67
75
|
}
|
|
68
76
|
};
|
|
69
77
|
}
|
|
78
|
+
function getRuntimeTopicEntries(operation) {
|
|
79
|
+
const commandSegments = operation.commandId.split(' ');
|
|
80
|
+
const topLevelCommandId = commandSegments[0];
|
|
81
|
+
const modulePrefix = toKebabCase(operation.moduleDisplayName || operation.moduleName || '');
|
|
82
|
+
const isTopLevelResource = Boolean(topLevelCommandId && modulePrefix && topLevelCommandId !== modulePrefix);
|
|
83
|
+
const entries = [];
|
|
84
|
+
if (!topLevelCommandId) {
|
|
85
|
+
return entries;
|
|
86
|
+
}
|
|
87
|
+
if (isTopLevelResource) {
|
|
88
|
+
entries.push([
|
|
89
|
+
topLevelCommandId,
|
|
90
|
+
{
|
|
91
|
+
summary: operation.resourceDescription || operation.resourceDisplayName,
|
|
92
|
+
description: operation.resourceDescription,
|
|
93
|
+
},
|
|
94
|
+
]);
|
|
95
|
+
return entries;
|
|
96
|
+
}
|
|
97
|
+
entries.push([
|
|
98
|
+
topLevelCommandId,
|
|
99
|
+
{
|
|
100
|
+
summary: operation.moduleDescription || operation.moduleDisplayName || operation.moduleName,
|
|
101
|
+
description: operation.moduleDescription,
|
|
102
|
+
},
|
|
103
|
+
]);
|
|
104
|
+
const resourceCommandId = commandSegments.slice(0, 2).join(' ');
|
|
105
|
+
if (commandSegments[1]) {
|
|
106
|
+
entries.push([
|
|
107
|
+
resourceCommandId,
|
|
108
|
+
{
|
|
109
|
+
summary: operation.resourceDescription || operation.resourceDisplayName,
|
|
110
|
+
description: operation.resourceDescription,
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
}
|
|
114
|
+
return entries;
|
|
115
|
+
}
|
|
70
116
|
const registry = {
|
|
71
117
|
...(await loadCommandsFromDirectory()),
|
|
72
118
|
};
|
|
@@ -77,11 +123,11 @@ for (const operation of runtime?.commands ?? []) {
|
|
|
77
123
|
const commandSegments = operation.commandId.split(' ');
|
|
78
124
|
const commandKey = commandSegments.join(':');
|
|
79
125
|
registry[`api:${commandKey}`] = createRuntimeCommand(operation);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
126
|
+
for (const [topicCommandId, metadata] of getRuntimeTopicEntries(operation)) {
|
|
127
|
+
const topicKey = `api:${topicCommandId.split(' ').join(':')}`;
|
|
128
|
+
if (!registry[topicKey]) {
|
|
129
|
+
registry[topicKey] = createRuntimeIndexCommand(`api ${topicCommandId}`, metadata);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
86
132
|
}
|
|
87
133
|
export default registry;
|
|
@@ -0,0 +1,20 @@
|
|
|
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 { Help } from '@oclif/core';
|
|
10
|
+
export function isTopicIndexCommand(commandId, topics) {
|
|
11
|
+
if (!commandId) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return topics.some((topic) => topic.name.startsWith(`${commandId}:`));
|
|
15
|
+
}
|
|
16
|
+
export default class RuntimeHelp extends Help {
|
|
17
|
+
get sortedCommands() {
|
|
18
|
+
return super.sortedCommands.filter((command) => !isTopicIndexCommand(command.id, this.config.topics));
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/lib/auth-store.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
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 { promises as fs } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
10
|
+
import path, { isAbsolute } from 'node:path';
|
|
3
11
|
import { resolveCliHomeDir } from './cli-home.js';
|
|
4
12
|
const DEFAULT_CONFIG = {
|
|
5
13
|
currentEnv: 'default',
|
|
@@ -45,10 +53,45 @@ export async function setCurrentEnv(envName, options = {}) {
|
|
|
45
53
|
config.currentEnv = envName;
|
|
46
54
|
await saveAuthConfig(config, options);
|
|
47
55
|
}
|
|
56
|
+
export class Env {
|
|
57
|
+
config;
|
|
58
|
+
constructor(config = {}) {
|
|
59
|
+
this.config = config;
|
|
60
|
+
}
|
|
61
|
+
get name() {
|
|
62
|
+
return this.config.name;
|
|
63
|
+
}
|
|
64
|
+
get baseUrl() {
|
|
65
|
+
return this.config.baseUrl;
|
|
66
|
+
}
|
|
67
|
+
get auth() {
|
|
68
|
+
return this.config.auth;
|
|
69
|
+
}
|
|
70
|
+
get runtime() {
|
|
71
|
+
return this.config.runtime;
|
|
72
|
+
}
|
|
73
|
+
get appRootPath() {
|
|
74
|
+
const appRootPath = this.config.appRootPath;
|
|
75
|
+
if (!appRootPath) {
|
|
76
|
+
return process.cwd();
|
|
77
|
+
}
|
|
78
|
+
if (isAbsolute(appRootPath)) {
|
|
79
|
+
return appRootPath;
|
|
80
|
+
}
|
|
81
|
+
return path.resolve(process.cwd(), appRootPath);
|
|
82
|
+
}
|
|
83
|
+
get storagePath() {
|
|
84
|
+
const storagePath = this.config.storagePath;
|
|
85
|
+
if (isAbsolute(storagePath)) {
|
|
86
|
+
return storagePath;
|
|
87
|
+
}
|
|
88
|
+
return path.resolve(process.cwd(), storagePath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
48
91
|
export async function getEnv(envName, options = {}) {
|
|
49
92
|
const config = await loadAuthConfig(options);
|
|
50
93
|
const resolved = envName || config.currentEnv || 'default';
|
|
51
|
-
return config.envs[resolved];
|
|
94
|
+
return new Env({ ...config.envs[resolved], name: resolved });
|
|
52
95
|
}
|
|
53
96
|
function areAuthConfigsEquivalent(left, right) {
|
|
54
97
|
if (!left && !right) {
|
|
@@ -78,8 +121,9 @@ async function writeEnv(envName, updater, options = {}) {
|
|
|
78
121
|
config.currentEnv = envName;
|
|
79
122
|
await saveAuthConfig(config, options);
|
|
80
123
|
}
|
|
81
|
-
export async function upsertEnv(envName,
|
|
124
|
+
export async function upsertEnv(envName, config, options = {}) {
|
|
82
125
|
await writeEnv(envName, (previous) => {
|
|
126
|
+
const { baseUrl, accessToken, ...rest } = config;
|
|
83
127
|
const baseUrlChanged = previous?.baseUrl !== baseUrl;
|
|
84
128
|
const nextAuth = accessToken
|
|
85
129
|
? {
|
|
@@ -94,6 +138,7 @@ export async function upsertEnv(envName, baseUrl, accessToken, options = {}) {
|
|
|
94
138
|
...previous,
|
|
95
139
|
baseUrl,
|
|
96
140
|
auth: nextAuth,
|
|
141
|
+
...rest,
|
|
97
142
|
runtime: baseUrlChanged || authChanged ? undefined : previous?.runtime,
|
|
98
143
|
};
|
|
99
144
|
}, options);
|
package/dist/lib/bootstrap.js
CHANGED
|
@@ -53,13 +53,17 @@ function hasBooleanFlag(argv, name) {
|
|
|
53
53
|
return false;
|
|
54
54
|
}
|
|
55
55
|
function getCommandToken(argv) {
|
|
56
|
+
const tokens = [];
|
|
56
57
|
for (const token of argv) {
|
|
57
58
|
if (!token || token.startsWith('-')) {
|
|
58
59
|
continue;
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
+
tokens.push(token);
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
+
if (tokens[0] === 'api') {
|
|
64
|
+
return tokens[1] ?? tokens[0];
|
|
65
|
+
}
|
|
66
|
+
return tokens[0];
|
|
63
67
|
}
|
|
64
68
|
function hasHelpFlag(argv) {
|
|
65
69
|
return argv.includes('--help') || argv.includes('-h');
|
|
@@ -68,8 +72,9 @@ function hasVersionFlag(argv) {
|
|
|
68
72
|
return argv.includes('--version') || argv.includes('-v');
|
|
69
73
|
}
|
|
70
74
|
function isBuiltinCommand(argv) {
|
|
71
|
-
const
|
|
72
|
-
|
|
75
|
+
const commandTokens = argv.filter((token) => token && !token.startsWith('-'));
|
|
76
|
+
const [topic, subtopic] = commandTokens;
|
|
77
|
+
return topic === 'env' || topic === 'resource' || (topic === 'api' && subtopic === 'resource');
|
|
73
78
|
}
|
|
74
79
|
export function shouldSkipRuntimeBootstrap(argv) {
|
|
75
80
|
return hasVersionFlag(argv) || isBuiltinCommand(argv);
|
|
@@ -246,7 +251,7 @@ export function formatSwaggerSchemaError(response, context) {
|
|
|
246
251
|
`Authentication failed while loading the command runtime from \`swagger:get\`${envLabel}.`,
|
|
247
252
|
`Base URL: ${context.baseUrl}`,
|
|
248
253
|
details,
|
|
249
|
-
'Update the API key with `nb env add
|
|
254
|
+
'Update the API key with `nb env add <name> --base-url <url> --auth-type token --token <api-key>`, log in with `nb env auth <name>`, or rerun the command with `--token <api-key>`.',
|
|
250
255
|
commandHint,
|
|
251
256
|
].join('\n');
|
|
252
257
|
}
|
|
@@ -257,7 +262,7 @@ export function formatSwaggerSchemaError(response, context) {
|
|
|
257
262
|
`Base URL: ${context.baseUrl}`,
|
|
258
263
|
`Network error: ${rawMessage}`,
|
|
259
264
|
'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
|
|
265
|
+
'If you recently changed the server address, update it with `nb env add <name> --base-url <url>` and retry `nb env update`.',
|
|
261
266
|
'Use `nb env list` to inspect the current env configuration.',
|
|
262
267
|
].join('\n');
|
|
263
268
|
}
|
|
@@ -267,7 +272,7 @@ export function formatMissingRuntimeEnvError(commandToken) {
|
|
|
267
272
|
if (!commandToken) {
|
|
268
273
|
return [
|
|
269
274
|
'No env is configured for runtime commands.',
|
|
270
|
-
'Run `nb env add
|
|
275
|
+
'Run `nb env add <name> --base-url <url>` first.',
|
|
271
276
|
'If you configure multiple environments later, switch with `nb env use <name>`.',
|
|
272
277
|
].join('\n');
|
|
273
278
|
}
|
|
@@ -275,7 +280,7 @@ export function formatMissingRuntimeEnvError(commandToken) {
|
|
|
275
280
|
`Unable to resolve runtime command \`${commandToken}\`.`,
|
|
276
281
|
'No env is configured, so the CLI cannot load runtime commands from `swagger:get`.',
|
|
277
282
|
'If this is a built-in command or a typo, run `nb --help` to inspect available commands.',
|
|
278
|
-
'If this should be an application runtime command, run `nb env add
|
|
283
|
+
'If this should be an application runtime command, run `nb env add <name> --base-url <url>` and then `nb env update`.',
|
|
279
284
|
].join('\n');
|
|
280
285
|
}
|
|
281
286
|
export async function ensureRuntimeFromArgv(argv, options) {
|
|
@@ -350,7 +355,7 @@ export async function updateEnvRuntime(options) {
|
|
|
350
355
|
if (!baseUrl) {
|
|
351
356
|
throw new Error([
|
|
352
357
|
`Env "${envName}" is missing a base URL.`,
|
|
353
|
-
'Update it with `nb env add
|
|
358
|
+
'Update it with `nb env add <name> --base-url <url>` first.',
|
|
354
359
|
].join('\n'));
|
|
355
360
|
}
|
|
356
361
|
updateTask('Loading command runtime...');
|
|
@@ -27,13 +27,13 @@ export async function collectCommandModulePaths(commandsRoot, extension) {
|
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
29
|
* Map a path relative to `commands/` with `.js` / `.ts` to an oclif explicit-registry key.
|
|
30
|
-
* `resource/foo.js` → `api:resource:foo
|
|
30
|
+
* `api/resource/foo.js` → `api:resource:foo`; trailing `index` maps to the parent command.
|
|
31
31
|
*/
|
|
32
32
|
export function commandRelativePathToRegistryKey(relativePath) {
|
|
33
33
|
const normalized = relativePath.replace(/\\/g, '/').replace(/\.(js|ts)$/i, '');
|
|
34
34
|
const segments = normalized.split('/').filter(Boolean);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
if (segments.at(-1) === 'index') {
|
|
36
|
+
segments.pop();
|
|
37
|
+
}
|
|
38
38
|
return segments.join(':');
|
|
39
39
|
}
|
package/dist/lib/env-auth.js
CHANGED
|
@@ -10,6 +10,9 @@ import crypto from 'node:crypto';
|
|
|
10
10
|
import { createServer } from 'node:http';
|
|
11
11
|
import { spawn } from 'node:child_process';
|
|
12
12
|
import { URL } from 'node:url';
|
|
13
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
13
16
|
import { getCurrentEnvName, getEnv, setEnvOauthSession, } from './auth-store.js';
|
|
14
17
|
import { printInfo, printVerbose, printWarning, printWarningBlock, updateTask } from './ui.js';
|
|
15
18
|
const ACCESS_TOKEN_REFRESH_WINDOW_MS = 60_000;
|
|
@@ -79,8 +82,8 @@ function formatOauthFetchFailure(prefix, options) {
|
|
|
79
82
|
`Network error: ${options.rawMessage || 'fetch failed'}`,
|
|
80
83
|
'Check that the NocoBase app is running, the base URL is correct, and the server is reachable from this machine.',
|
|
81
84
|
options.envName
|
|
82
|
-
? `If the saved login is stale, run \`nb env auth
|
|
83
|
-
: 'If the saved login is stale, run `nb env auth
|
|
85
|
+
? `If the saved login is stale, run \`nb env auth ${options.envName}\` again after connectivity is restored.`
|
|
86
|
+
: 'If the saved login is stale, run `nb env auth <name>` again after connectivity is restored.',
|
|
84
87
|
'Use `nb env list` to inspect the current env configuration.',
|
|
85
88
|
]
|
|
86
89
|
.filter(Boolean)
|
|
@@ -159,12 +162,61 @@ function buildPkcePair() {
|
|
|
159
162
|
codeChallenge,
|
|
160
163
|
};
|
|
161
164
|
}
|
|
162
|
-
function
|
|
165
|
+
function escapeHtmlAttribute(value) {
|
|
166
|
+
return value
|
|
167
|
+
.replace(/&/g, '&')
|
|
168
|
+
.replace(/"/g, '"')
|
|
169
|
+
.replace(/</g, '<')
|
|
170
|
+
.replace(/>/g, '>');
|
|
171
|
+
}
|
|
172
|
+
function escapeScriptString(value) {
|
|
173
|
+
return JSON.stringify(value).replace(/</g, '\\u003c');
|
|
174
|
+
}
|
|
175
|
+
export function buildOauthRedirectHtml(url) {
|
|
176
|
+
const escapedUrl = escapeHtmlAttribute(url);
|
|
177
|
+
return `<!doctype html>
|
|
178
|
+
<html>
|
|
179
|
+
<head>
|
|
180
|
+
<meta charset="utf-8">
|
|
181
|
+
<meta http-equiv="refresh" content="0; url=${escapedUrl}">
|
|
182
|
+
<title>NocoBase OAuth Login</title>
|
|
183
|
+
</head>
|
|
184
|
+
<body>
|
|
185
|
+
<script>window.location.replace(${escapeScriptString(url)});</script>
|
|
186
|
+
<p>Redirecting to the OAuth login page. If nothing happens, <a href="${escapedUrl}">continue manually</a>.</p>
|
|
187
|
+
</body>
|
|
188
|
+
</html>
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
async function createWindowsBrowserRedirectFile(url) {
|
|
192
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-oauth-'));
|
|
193
|
+
const filePath = path.join(directory, 'authorize.html');
|
|
194
|
+
await writeFile(filePath, buildOauthRedirectHtml(url), 'utf8');
|
|
195
|
+
const cleanup = setTimeout(() => {
|
|
196
|
+
void rm(directory, { recursive: true, force: true });
|
|
197
|
+
}, OAUTH_LOGIN_TIMEOUT_MS);
|
|
198
|
+
cleanup.unref?.();
|
|
199
|
+
return {
|
|
200
|
+
target: filePath,
|
|
201
|
+
cleanup: async () => {
|
|
202
|
+
clearTimeout(cleanup);
|
|
203
|
+
await rm(directory, { recursive: true, force: true });
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
async function getBrowserOpenTarget(url) {
|
|
208
|
+
if (process.platform !== 'win32') {
|
|
209
|
+
return { target: url };
|
|
210
|
+
}
|
|
211
|
+
return createWindowsBrowserRedirectFile(url);
|
|
212
|
+
}
|
|
213
|
+
async function maybeOpenBrowser(url) {
|
|
214
|
+
const { target, cleanup } = await getBrowserOpenTarget(url);
|
|
163
215
|
const candidates = process.platform === 'darwin'
|
|
164
|
-
? [['open',
|
|
216
|
+
? [['open', target]]
|
|
165
217
|
: process.platform === 'win32'
|
|
166
|
-
? [['cmd', '/c', 'start', '',
|
|
167
|
-
: [['xdg-open',
|
|
218
|
+
? [['cmd', '/c', 'start', '', target]]
|
|
219
|
+
: [['xdg-open', target]];
|
|
168
220
|
for (const [command, ...args] of candidates) {
|
|
169
221
|
try {
|
|
170
222
|
const child = spawn(command, args, {
|
|
@@ -172,13 +224,19 @@ function maybeOpenBrowser(url) {
|
|
|
172
224
|
stdio: 'ignore',
|
|
173
225
|
});
|
|
174
226
|
child.unref();
|
|
175
|
-
return
|
|
227
|
+
return {
|
|
228
|
+
opened: true,
|
|
229
|
+
cleanup,
|
|
230
|
+
};
|
|
176
231
|
}
|
|
177
232
|
catch (_error) {
|
|
178
233
|
continue;
|
|
179
234
|
}
|
|
180
235
|
}
|
|
181
|
-
return
|
|
236
|
+
return {
|
|
237
|
+
opened: false,
|
|
238
|
+
cleanup,
|
|
239
|
+
};
|
|
182
240
|
}
|
|
183
241
|
async function createLoopbackServer(state) {
|
|
184
242
|
const result = await new Promise((resolve, reject) => {
|
|
@@ -208,7 +266,26 @@ async function createLoopbackServer(state) {
|
|
|
208
266
|
return;
|
|
209
267
|
}
|
|
210
268
|
res.statusCode = 200;
|
|
211
|
-
res.end(
|
|
269
|
+
res.end(`<!DOCTYPE html>
|
|
270
|
+
<html lang="en">
|
|
271
|
+
<head><meta charset="utf-8" /><title>Authentication complete</title></head>
|
|
272
|
+
<body>
|
|
273
|
+
<h1>Authentication complete</h1>
|
|
274
|
+
<p>You can return to the terminal.</p>
|
|
275
|
+
<p id="manual"></p>
|
|
276
|
+
<script>
|
|
277
|
+
setTimeout(function () {
|
|
278
|
+
window.close();
|
|
279
|
+
setTimeout(function () {
|
|
280
|
+
var el = document.getElementById('manual');
|
|
281
|
+
if (document.visibilityState === 'visible' && el) {
|
|
282
|
+
el.textContent = 'Please close this tab manually if it is still open.';
|
|
283
|
+
}
|
|
284
|
+
}, 400);
|
|
285
|
+
}, 1000);
|
|
286
|
+
</script>
|
|
287
|
+
</body>
|
|
288
|
+
</html>`);
|
|
212
289
|
resolveWaiter(code);
|
|
213
290
|
}
|
|
214
291
|
catch (error) {
|
|
@@ -279,7 +356,7 @@ async function exchangeAuthorizationCode(options) {
|
|
|
279
356
|
}
|
|
280
357
|
async function refreshOauthAccessToken(options) {
|
|
281
358
|
if (!options.auth.refreshToken || !options.auth.clientId) {
|
|
282
|
-
throw new Error(`OAuth session for env "${options.envName}" cannot be refreshed. Run \`nb env auth
|
|
359
|
+
throw new Error(`OAuth session for env "${options.envName}" cannot be refreshed. Run \`nb env auth ${options.envName}\`.`);
|
|
283
360
|
}
|
|
284
361
|
const metadata = await fetchOauthServerMetadata(options.baseUrl, { envName: options.envName });
|
|
285
362
|
const resource = options.auth.resource || getOauthResource(metadata.issuer);
|
|
@@ -306,7 +383,7 @@ async function refreshOauthAccessToken(options) {
|
|
|
306
383
|
});
|
|
307
384
|
const data = await parseJsonResponse(response);
|
|
308
385
|
if (!response.ok) {
|
|
309
|
-
throw new Error(formatOauthError(`Failed to refresh OAuth session for env "${options.envName}". Run \`nb env auth
|
|
386
|
+
throw new Error(formatOauthError(`Failed to refresh OAuth session for env "${options.envName}". Run \`nb env auth ${options.envName}\` again`, data, response.status));
|
|
310
387
|
}
|
|
311
388
|
if (!data || typeof data !== 'object' || typeof data.access_token !== 'string') {
|
|
312
389
|
throw new Error(`OAuth refresh response for env "${options.envName}" is missing access_token.`);
|
|
@@ -344,7 +421,7 @@ export async function resolveAccessToken(options) {
|
|
|
344
421
|
}
|
|
345
422
|
const baseUrl = options.baseUrl ?? env.baseUrl;
|
|
346
423
|
if (!baseUrl) {
|
|
347
|
-
throw new Error(`Env "${envName}" is missing a base URL. Run \`nb env add
|
|
424
|
+
throw new Error(`Env "${envName}" is missing a base URL. Run \`nb env add ${envName} --base-url <url>\`.`);
|
|
348
425
|
}
|
|
349
426
|
printVerbose(`Refreshing OAuth session for env "${envName}"`);
|
|
350
427
|
return refreshOauthAccessToken({
|
|
@@ -376,7 +453,7 @@ export async function authenticateEnvWithOauth(options) {
|
|
|
376
453
|
if (!baseUrl) {
|
|
377
454
|
throw new Error([
|
|
378
455
|
`Env "${envName}" is missing a base URL.`,
|
|
379
|
-
'Run `nb env add
|
|
456
|
+
'Run `nb env add <name> --base-url <url>` first.',
|
|
380
457
|
].join('\n'));
|
|
381
458
|
}
|
|
382
459
|
updateTask(`Loading OAuth metadata for env "${envName}"...`);
|
|
@@ -385,6 +462,7 @@ export async function authenticateEnvWithOauth(options) {
|
|
|
385
462
|
const { codeVerifier, codeChallenge } = buildPkcePair();
|
|
386
463
|
const callback = await createLoopbackServer(state);
|
|
387
464
|
const resource = getOauthResource(metadata.issuer);
|
|
465
|
+
let cleanupBrowserOpenTarget;
|
|
388
466
|
try {
|
|
389
467
|
updateTask(`Registering OAuth client for env "${envName}"...`);
|
|
390
468
|
const registration = await registerOauthClient(metadata, callback.redirectUri);
|
|
@@ -399,8 +477,9 @@ export async function authenticateEnvWithOauth(options) {
|
|
|
399
477
|
authorizationUrl.searchParams.set('code_challenge_method', 'S256');
|
|
400
478
|
authorizationUrl.searchParams.set('resource', resource);
|
|
401
479
|
updateTask(`Waiting for OAuth login for env "${envName}"...`);
|
|
402
|
-
const
|
|
403
|
-
|
|
480
|
+
const browser = await maybeOpenBrowser(authorizationUrl.toString());
|
|
481
|
+
cleanupBrowserOpenTarget = browser.cleanup;
|
|
482
|
+
if (!browser.opened) {
|
|
404
483
|
printWarningBlock('Unable to open the browser automatically. Open this URL manually:');
|
|
405
484
|
}
|
|
406
485
|
else {
|
|
@@ -442,6 +521,7 @@ export async function authenticateEnvWithOauth(options) {
|
|
|
442
521
|
}, { scope: options.scope });
|
|
443
522
|
}
|
|
444
523
|
finally {
|
|
524
|
+
await cleanupBrowserOpenTarget?.().catch(() => undefined);
|
|
445
525
|
await callback.close().catch(() => undefined);
|
|
446
526
|
}
|
|
447
527
|
}
|