@nocobase/cli 3.0.0-alpha.5 → 3.0.0-alpha.7
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/dist/commands/env/add.js +6 -0
- package/dist/commands/env/update.js +13 -0
- package/dist/commands/init.js +49 -0
- package/dist/commands/install.js +18 -62
- package/dist/commands/portal/config.js +16 -5
- package/dist/commands/portal/create.js +13 -25
- package/dist/commands/portal/destroy.js +28 -6
- package/dist/commands/portal/list.js +5 -5
- package/dist/commands/portal/pull.js +28 -2
- package/dist/lib/api-client.js +35 -9
- package/dist/lib/app-client-entry-mode.js +28 -0
- package/dist/lib/app-managed-resources.js +2 -0
- package/dist/lib/auth-store.js +52 -1
- package/dist/lib/env-auth.js +2 -36
- package/dist/lib/env-command-config.js +1 -0
- package/dist/lib/env-config.js +10 -2
- package/dist/lib/env-portal-config.js +30 -0
- package/dist/lib/env-proxy.js +1 -1
- package/dist/lib/managed-env-file.js +57 -1
- package/dist/lib/managed-init-env.js +0 -2
- package/dist/lib/portal-config.js +0 -17
- package/dist/lib/portal-configure.js +46 -54
- package/dist/lib/portal-create.js +21 -5
- package/dist/lib/portal-deploy.js +5 -42
- package/dist/lib/portal-destroy.js +27 -9
- package/dist/lib/portal-dev.js +2 -3
- package/dist/lib/portal-info.js +2 -5
- package/dist/lib/portal-list.js +16 -23
- package/dist/lib/portal-path-safety.js +76 -0
- package/dist/lib/portal-source.js +60 -43
- package/dist/lib/prompt-catalog-core.js +2 -2
- package/dist/lib/prompt-catalog-terminal.js +4 -5
- package/dist/lib/prompt-web-ui.js +8 -1
- package/dist/locale/en-US.json +44 -16
- package/dist/locale/zh-CN.json +44 -16
- package/package.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
import { Args, Command, Flags } from '@oclif/core';
|
|
10
|
-
import { getCurrentEnvName, getEnv } from '../../lib/auth-store.js';
|
|
10
|
+
import { getCurrentEnvName, getEnv, setEnvPortalPath } from '../../lib/auth-store.js';
|
|
11
11
|
import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
|
|
12
12
|
import { translateCli } from '../../lib/cli-locale.js';
|
|
13
13
|
import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
|
|
@@ -19,6 +19,8 @@ export default class PortalPull extends Command {
|
|
|
19
19
|
static examples = [
|
|
20
20
|
'<%= config.bin %> <%= command.id %> customer',
|
|
21
21
|
'<%= config.bin %> <%= command.id %> customer --env prod --yes',
|
|
22
|
+
'<%= config.bin %> <%= command.id %> customer --path ./portals/customer',
|
|
23
|
+
'<%= config.bin %> <%= command.id %> customer --git-repo git@github.com:nocobase/customer.git',
|
|
22
24
|
'<%= config.bin %> <%= command.id %> customer --force',
|
|
23
25
|
'<%= config.bin %> <%= command.id %> customer --no-install',
|
|
24
26
|
];
|
|
@@ -42,6 +44,18 @@ export default class PortalPull extends Command {
|
|
|
42
44
|
description: 'Delete the existing local files and pull them again',
|
|
43
45
|
default: false,
|
|
44
46
|
}),
|
|
47
|
+
path: Flags.string({
|
|
48
|
+
description: 'Portal workspace directory; defaults to the saved path, then ./<portal>',
|
|
49
|
+
}),
|
|
50
|
+
'git-repo': Flags.string({
|
|
51
|
+
description: 'Temporarily pull source from this Git repository without updating the portal source configuration',
|
|
52
|
+
}),
|
|
53
|
+
'git-branch': Flags.string({
|
|
54
|
+
description: 'Git branch for the temporary --git-repo pull; defaults to main',
|
|
55
|
+
}),
|
|
56
|
+
'git-path': Flags.string({
|
|
57
|
+
description: 'Directory inside the temporary Git repository; defaults to the repository root',
|
|
58
|
+
}),
|
|
45
59
|
install: Flags.boolean({
|
|
46
60
|
description: 'Run pnpm install after pulling the portal source',
|
|
47
61
|
default: true,
|
|
@@ -50,6 +64,12 @@ export default class PortalPull extends Command {
|
|
|
50
64
|
};
|
|
51
65
|
async run() {
|
|
52
66
|
const { args, flags } = await this.parse(PortalPull);
|
|
67
|
+
if ((flags['git-branch'] || flags['git-path']) && !flags['git-repo']) {
|
|
68
|
+
this.error(portalPullText('errors.gitRepoRequiredForTemporaryPull', undefined, [
|
|
69
|
+
'--git-branch and --git-path require --git-repo for a temporary Git pull.',
|
|
70
|
+
'To update the portal configuration, use `nb portal config`.',
|
|
71
|
+
].join(' ')));
|
|
72
|
+
}
|
|
53
73
|
const requestedEnv = hasExplicitEnvSelection(this.argv) ? flags.env : undefined;
|
|
54
74
|
const confirmed = await ensureCrossEnvConfirmed({
|
|
55
75
|
command: this,
|
|
@@ -74,12 +94,18 @@ export default class PortalPull extends Command {
|
|
|
74
94
|
cliVersion: String(this.config.pjson.version ?? '').trim(),
|
|
75
95
|
force: flags.force,
|
|
76
96
|
installDependencies: flags.install,
|
|
97
|
+
sourcePath: flags.path,
|
|
98
|
+
defaultSourcePath: true,
|
|
99
|
+
gitRepo: flags['git-repo'],
|
|
100
|
+
gitBranch: flags['git-branch'],
|
|
101
|
+
gitPath: flags['git-path'],
|
|
77
102
|
});
|
|
78
103
|
if (!result.changed) {
|
|
79
104
|
printInfo(result.noopReason ?? portalPullText('messages.noop', undefined, 'No pull is needed.'));
|
|
80
105
|
return;
|
|
81
106
|
}
|
|
82
|
-
|
|
107
|
+
await setEnvPortalPath(envName, result.portal, result.portalDir, { scope });
|
|
108
|
+
printSuccess(portalPullText('messages.pulled', { portal: result.portal, portalDir: result.portalDir }, `Pulled portal source "${result.portal}" into ${result.portalDir}`));
|
|
83
109
|
if (result.installFailed) {
|
|
84
110
|
printWarning(portalPullText('messages.installFailed', { portalDir: result.portalDir }, `Dependency installation did not finish successfully. Run \`pnpm install\` manually in ${result.portalDir}.`));
|
|
85
111
|
}
|
package/dist/lib/api-client.js
CHANGED
|
@@ -19,6 +19,7 @@ import { promises as fs } from 'node:fs';
|
|
|
19
19
|
import { basename, dirname } from 'node:path';
|
|
20
20
|
import { Readable } from 'node:stream';
|
|
21
21
|
import { pipeline } from 'node:stream/promises';
|
|
22
|
+
import { translateCli } from './cli-locale.js';
|
|
22
23
|
import { resolveServerRequestTarget } from './env-auth.js';
|
|
23
24
|
import { fetchWithPreservedAuthRedirect } from './http-request.js';
|
|
24
25
|
const CLI_REQUEST_SOURCE_HEADER = 'x-request-source';
|
|
@@ -40,7 +41,32 @@ function parseJsonInput(raw, flagName) {
|
|
|
40
41
|
function normalizeBaseUrl(baseUrl) {
|
|
41
42
|
return baseUrl.replace(/\/+$/, '');
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
+
function buildAuthFailureCommand(envName) {
|
|
45
|
+
const normalizedEnvName = String(envName ?? '').trim();
|
|
46
|
+
return normalizedEnvName ? `nb env auth ${normalizedEnvName}` : 'nb env auth';
|
|
47
|
+
}
|
|
48
|
+
function decorateAuthFailureResponse(data, status, envName) {
|
|
49
|
+
if (status !== 401) {
|
|
50
|
+
return data;
|
|
51
|
+
}
|
|
52
|
+
const cliCommand = buildAuthFailureCommand(envName);
|
|
53
|
+
const cliHint = translateCli('apiClient.authRequiredHint', { command: cliCommand }, {
|
|
54
|
+
fallback: 'Authentication failed or the saved session has expired. Run `{{command}}` to sign in again.',
|
|
55
|
+
});
|
|
56
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
57
|
+
return {
|
|
58
|
+
...data,
|
|
59
|
+
cliHint,
|
|
60
|
+
cliCommand,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
error: data,
|
|
65
|
+
cliHint,
|
|
66
|
+
cliCommand,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function parseResponse(response, envName) {
|
|
44
70
|
const text = await response.text();
|
|
45
71
|
let data = text;
|
|
46
72
|
if (text) {
|
|
@@ -54,10 +80,10 @@ async function parseResponse(response) {
|
|
|
54
80
|
return {
|
|
55
81
|
ok: response.ok,
|
|
56
82
|
status: response.status,
|
|
57
|
-
data,
|
|
83
|
+
data: decorateAuthFailureResponse(data, response.status, envName),
|
|
58
84
|
};
|
|
59
85
|
}
|
|
60
|
-
async function parseBinaryResponse(response, outputPath) {
|
|
86
|
+
async function parseBinaryResponse(response, outputPath, envName) {
|
|
61
87
|
if (response.ok && response.body) {
|
|
62
88
|
await fs.mkdir(dirname(outputPath), { recursive: true }).catch(() => undefined);
|
|
63
89
|
await pipeline(Readable.fromWeb(response.body), createWriteStream(outputPath));
|
|
@@ -69,7 +95,7 @@ async function parseBinaryResponse(response, outputPath) {
|
|
|
69
95
|
},
|
|
70
96
|
};
|
|
71
97
|
}
|
|
72
|
-
return parseResponse(response);
|
|
98
|
+
return parseResponse(response, envName);
|
|
73
99
|
}
|
|
74
100
|
function parseScalarValue(value, type) {
|
|
75
101
|
if (value === undefined) {
|
|
@@ -215,7 +241,7 @@ async function createMultipartBody(flags, operation) {
|
|
|
215
241
|
return hasValues ? formData : undefined;
|
|
216
242
|
}
|
|
217
243
|
export async function executeApiRequest(options) {
|
|
218
|
-
const { baseUrl, token } = await resolveServerRequestTarget(options);
|
|
244
|
+
const { baseUrl, token, envName } = await resolveServerRequestTarget(options);
|
|
219
245
|
const headers = new Headers();
|
|
220
246
|
headers.set(CLI_REQUEST_SOURCE_HEADER, CLI_REQUEST_SOURCE_VALUE);
|
|
221
247
|
headers.set(CLI_VERSION_HEADER, options.cliVersion);
|
|
@@ -283,12 +309,12 @@ export async function executeApiRequest(options) {
|
|
|
283
309
|
if (!outputPath) {
|
|
284
310
|
throw new Error('Missing required output path --output');
|
|
285
311
|
}
|
|
286
|
-
return parseBinaryResponse(response, outputPath);
|
|
312
|
+
return parseBinaryResponse(response, outputPath, envName);
|
|
287
313
|
}
|
|
288
|
-
return parseResponse(response);
|
|
314
|
+
return parseResponse(response, envName);
|
|
289
315
|
}
|
|
290
316
|
export async function executeRawApiRequest(options) {
|
|
291
|
-
const { baseUrl, token } = await resolveServerRequestTarget(options);
|
|
317
|
+
const { baseUrl, token, envName } = await resolveServerRequestTarget(options);
|
|
292
318
|
const headers = new Headers();
|
|
293
319
|
headers.set(CLI_REQUEST_SOURCE_HEADER, CLI_REQUEST_SOURCE_VALUE);
|
|
294
320
|
if (token) {
|
|
@@ -332,7 +358,7 @@ export async function executeRawApiRequest(options) {
|
|
|
332
358
|
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
333
359
|
signal: controller?.signal,
|
|
334
360
|
});
|
|
335
|
-
return parseResponse(response);
|
|
361
|
+
return parseResponse(response, envName);
|
|
336
362
|
}
|
|
337
363
|
finally {
|
|
338
364
|
if (timeout) {
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
export const APP_CLIENT_ENTRY_MODES = [
|
|
10
|
+
'legacy-default',
|
|
11
|
+
'modern-default',
|
|
12
|
+
'modern-only',
|
|
13
|
+
'settings-default',
|
|
14
|
+
];
|
|
15
|
+
export const PUBLIC_APP_CLIENT_ENTRY_MODES = ['legacy-default', 'modern-default', 'modern-only'];
|
|
16
|
+
export function normalizeAppClientEntryMode(value) {
|
|
17
|
+
const text = String(value ?? '').trim();
|
|
18
|
+
return APP_CLIENT_ENTRY_MODES.includes(text) ? text : undefined;
|
|
19
|
+
}
|
|
20
|
+
export function normalizePublicAppClientEntryMode(value) {
|
|
21
|
+
const text = String(value ?? '').trim();
|
|
22
|
+
return PUBLIC_APP_CLIENT_ENTRY_MODES.includes(text)
|
|
23
|
+
? text
|
|
24
|
+
: undefined;
|
|
25
|
+
}
|
|
26
|
+
export function defaultAppClientEntryModeForDownloadVersion(version) {
|
|
27
|
+
return String(version ?? '').trim() === 'latest' ? 'legacy-default' : 'modern-only';
|
|
28
|
+
}
|
|
@@ -186,6 +186,7 @@ export async function buildSavedDockerRunArgs(runtime, options) {
|
|
|
186
186
|
const dbTablePrefix = trimValue(config.dbTablePrefix);
|
|
187
187
|
const dbUnderscored = typeof config.dbUnderscored === 'boolean' ? config.dbUnderscored : undefined;
|
|
188
188
|
const extractClientAssets = resolveDockerClientAssetsExtractEnabled(process.env.NOCOBASE_EXTRACT_CLIENT_ASSETS);
|
|
189
|
+
const appClientEntryMode = trimValue(config.appClientEntryMode);
|
|
189
190
|
const dockerRegistry = trimValue(config.dockerRegistry) || DEFAULT_DOCKER_REGISTRY;
|
|
190
191
|
const version = trimValue(config.downloadVersion) || DEFAULT_DOCKER_VERSION;
|
|
191
192
|
const imageRef = resolveDockerImageRef(dockerRegistry, version, {
|
|
@@ -245,6 +246,7 @@ export async function buildSavedDockerRunArgs(runtime, options) {
|
|
|
245
246
|
const lifecycleEnvVars = managedAppLifecycleEnvVars();
|
|
246
247
|
args.push('-e', `APP_ENV=${lifecycleEnvVars.APP_ENV}`, '-e', `NODE_ENV=${lifecycleEnvVars.NODE_ENV}`, '-e', `APP_KEY=${appKey}`, '-e', `DB_DIALECT=${dbDialect}`, '-e', `DB_HOST=${dbHost}`, '-e', `DB_PORT=${dbPort}`, '-e', `DB_DATABASE=${dbDatabase}`, '-e', `DB_USER=${dbUser}`, '-e', `DB_PASSWORD=${dbPassword}`, '-e', `TZ=${timeZone}`, '-v', `${storagePath}:${DOCKER_APP_STORAGE_DESTINATION}`);
|
|
247
248
|
pushOptionalEnvArg(args, 'APP_PUBLIC_PATH', appPublicPath ? resolveAppPublicPath(appPublicPath) : undefined);
|
|
249
|
+
pushOptionalEnvArg(args, 'APP_CLIENT_ENTRY_MODE', appClientEntryMode);
|
|
248
250
|
pushOptionalEnvArg(args, 'DB_SCHEMA', dbSchema || undefined);
|
|
249
251
|
pushOptionalEnvArg(args, 'DB_TABLE_PREFIX', dbTablePrefix || undefined);
|
|
250
252
|
pushOptionalEnvArg(args, 'DB_UNDERSCORED', dbUnderscored);
|
package/dist/lib/auth-store.js
CHANGED
|
@@ -14,6 +14,7 @@ import { normalizeCliLocale } from './cli-locale.js';
|
|
|
14
14
|
import { normalizeEnvProxyConfig, normalizeEnvProxyProviderConfig, } from './env-proxy-config.js';
|
|
15
15
|
import { inferConfiguredAppPathFromLegacyConfig, resolveConfiguredAppPath, resolveConfiguredSourcePath, resolveConfiguredStoragePath, } from './env-paths.js';
|
|
16
16
|
import { ENV_CONFIG_SCHEMA_VERSION, normalizeEnvConfigSchemaVersion } from './env-config.js';
|
|
17
|
+
import { normalizeEnvPortalsConfig, } from './env-portal-config.js';
|
|
17
18
|
import { cleanupCurrentSessionAfterEnvRemoval, resolveEffectiveCurrentEnv, setSessionCurrentEnv, } from './session-store.js';
|
|
18
19
|
function normalizeStoredEnvKind(value) {
|
|
19
20
|
const kind = String(value ?? '').trim();
|
|
@@ -78,11 +79,12 @@ function normalizeEnvConfigEntry(entry) {
|
|
|
78
79
|
if (!entry) {
|
|
79
80
|
return entry;
|
|
80
81
|
}
|
|
81
|
-
const { kind: _kind, apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, schemaVersion: _schemaVersion, ...rest } = entry;
|
|
82
|
+
const { kind: _kind, apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, schemaVersion: _schemaVersion, portals: _portals, ...rest } = entry;
|
|
82
83
|
const normalizedKind = resolveEnvKind(entry);
|
|
83
84
|
const apiBaseUrl = readEnvApiBaseUrl(entry);
|
|
84
85
|
const schemaVersion = normalizeEnvConfigSchemaVersion(entry.schemaVersion);
|
|
85
86
|
const proxy = normalizeEnvProxyConfig(entry.proxy);
|
|
87
|
+
const portals = normalizeEnvPortalsConfig(entry.portals);
|
|
86
88
|
return {
|
|
87
89
|
...rest,
|
|
88
90
|
...(schemaVersion ? { schemaVersion } : {}),
|
|
@@ -90,6 +92,7 @@ function normalizeEnvConfigEntry(entry) {
|
|
|
90
92
|
...(apiBaseUrl !== undefined ? { apiBaseUrl } : {}),
|
|
91
93
|
...(normalizeOptionalString(entry.appPublicPath) ? { appPublicPath: resolveAppPublicPath(entry.appPublicPath) } : {}),
|
|
92
94
|
...(proxy ? { proxy } : {}),
|
|
95
|
+
...(portals ? { portals } : {}),
|
|
93
96
|
};
|
|
94
97
|
}
|
|
95
98
|
function normalizeAuthConfig(config) {
|
|
@@ -332,6 +335,7 @@ export class Env {
|
|
|
332
335
|
put('APP_PORT', this.appPort);
|
|
333
336
|
put('APP_PUBLIC_PATH', this.config.appPublicPath ? resolveAppPublicPath(this.config.appPublicPath) : undefined);
|
|
334
337
|
put('CDN_BASE_URL', this.config.cdnBaseUrl);
|
|
338
|
+
put('APP_CLIENT_ENTRY_MODE', this.config.appClientEntryMode);
|
|
335
339
|
put('APP_KEY', this.config.appKey);
|
|
336
340
|
put('TZ', this.config.timezone);
|
|
337
341
|
put('DB_DIALECT', this.config.dbDialect);
|
|
@@ -521,6 +525,53 @@ export async function setEnvRuntime(envName, runtime, options = {}) {
|
|
|
521
525
|
};
|
|
522
526
|
await saveAuthConfig(config, options);
|
|
523
527
|
}
|
|
528
|
+
export function resolveEnvPortalPath(config, portal) {
|
|
529
|
+
const portalName = normalizeOptionalString(portal);
|
|
530
|
+
if (!portalName) {
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
return normalizeOptionalString(config?.portals?.[portalName]?.path);
|
|
534
|
+
}
|
|
535
|
+
export async function setEnvPortalPath(envName, portal, portalPath, options = {}) {
|
|
536
|
+
const portalName = normalizeOptionalString(portal);
|
|
537
|
+
const normalizedPath = normalizeOptionalString(portalPath);
|
|
538
|
+
if (!portalName || !normalizedPath) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
await writeEnv(envName, (previous) => {
|
|
542
|
+
if (!previous) {
|
|
543
|
+
throw new Error(`Env "${envName}" is not configured`);
|
|
544
|
+
}
|
|
545
|
+
const portals = normalizeEnvPortalsConfig(previous.portals) ?? {};
|
|
546
|
+
return {
|
|
547
|
+
...previous,
|
|
548
|
+
portals: {
|
|
549
|
+
...portals,
|
|
550
|
+
[portalName]: {
|
|
551
|
+
...(portals[portalName] ?? {}),
|
|
552
|
+
path: normalizedPath,
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
}, options);
|
|
557
|
+
}
|
|
558
|
+
export async function unsetEnvPortalPath(envName, portal, options = {}) {
|
|
559
|
+
const portalName = normalizeOptionalString(portal);
|
|
560
|
+
if (!portalName) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
await writeEnv(envName, (previous) => {
|
|
564
|
+
if (!previous) {
|
|
565
|
+
throw new Error(`Env "${envName}" is not configured`);
|
|
566
|
+
}
|
|
567
|
+
const portals = normalizeEnvPortalsConfig(previous.portals) ?? {};
|
|
568
|
+
delete portals[portalName];
|
|
569
|
+
return {
|
|
570
|
+
...previous,
|
|
571
|
+
...(Object.keys(portals).length > 0 ? { portals } : { portals: undefined }),
|
|
572
|
+
};
|
|
573
|
+
}, options);
|
|
574
|
+
}
|
|
524
575
|
export function resolveEnvProxyEntry(config, provider) {
|
|
525
576
|
const proxy = normalizeEnvProxyConfig(config?.proxy);
|
|
526
577
|
const resolved = {
|
package/dist/lib/env-auth.js
CHANGED
|
@@ -27,40 +27,6 @@ const DEFAULT_DEVICE_POLL_INTERVAL_SECONDS = 5;
|
|
|
27
27
|
function normalizeBaseUrl(baseUrl) {
|
|
28
28
|
return baseUrl.replace(/\/+$/, '');
|
|
29
29
|
}
|
|
30
|
-
function buildDeviceVerificationPathFromApiBaseUrl(apiBaseUrl) {
|
|
31
|
-
const url = new URL(apiBaseUrl);
|
|
32
|
-
const subappMatch = url.pathname.match(/^(.*)\/api\/__app\/([^/]+)\/?$/);
|
|
33
|
-
if (subappMatch) {
|
|
34
|
-
const publicPath = (subappMatch[1] || '').replace(/\/+$/, '');
|
|
35
|
-
return `${publicPath}/settings/apps/${subappMatch[2]}/idpOAuth/device`;
|
|
36
|
-
}
|
|
37
|
-
const appMatch = url.pathname.match(/^(.*)\/api\/?$/);
|
|
38
|
-
if (appMatch) {
|
|
39
|
-
const publicPath = (appMatch[1] || '').replace(/\/+$/, '');
|
|
40
|
-
return `${publicPath}/settings/idpOAuth/device`;
|
|
41
|
-
}
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
export function resolveDeviceVerificationUrlForApiBaseUrl(verificationUrl, apiBaseUrl) {
|
|
45
|
-
try {
|
|
46
|
-
const devicePath = buildDeviceVerificationPathFromApiBaseUrl(apiBaseUrl);
|
|
47
|
-
if (!devicePath) {
|
|
48
|
-
return verificationUrl;
|
|
49
|
-
}
|
|
50
|
-
const originalUrl = new URL(verificationUrl);
|
|
51
|
-
if (!originalUrl.pathname.endsWith('/idpOAuth/device')) {
|
|
52
|
-
return verificationUrl;
|
|
53
|
-
}
|
|
54
|
-
const publicUrl = new URL(apiBaseUrl);
|
|
55
|
-
publicUrl.pathname = devicePath;
|
|
56
|
-
publicUrl.search = originalUrl.search;
|
|
57
|
-
publicUrl.hash = originalUrl.hash;
|
|
58
|
-
return publicUrl.toString();
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return verificationUrl;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
30
|
export function getOauthMetadataUrl(baseUrl) {
|
|
65
31
|
return `${normalizeBaseUrl(baseUrl)}/.well-known/oauth-authorization-server`;
|
|
66
32
|
}
|
|
@@ -973,7 +939,7 @@ export async function resolveServerRequestTarget(options) {
|
|
|
973
939
|
: `Use --api-base-url or run \`nb init --ui --env ${envName}\` first.`,
|
|
974
940
|
].join('\n'));
|
|
975
941
|
}
|
|
976
|
-
return { baseUrl, token };
|
|
942
|
+
return { baseUrl, token, envName };
|
|
977
943
|
}
|
|
978
944
|
export async function authenticateEnvWithBasic(options) {
|
|
979
945
|
const envName = options.envName ?? (await getCurrentEnvName({ scope: options.scope }));
|
|
@@ -1051,7 +1017,7 @@ async function authenticateEnvWithOauthDevice(options) {
|
|
|
1051
1017
|
baseUrl: options.baseUrl,
|
|
1052
1018
|
});
|
|
1053
1019
|
stopTask();
|
|
1054
|
-
const verificationUrl =
|
|
1020
|
+
const verificationUrl = deviceAuthorization.verification_uri_complete || deviceAuthorization.verification_uri;
|
|
1055
1021
|
const browser = await browserOpener(verificationUrl);
|
|
1056
1022
|
cleanupBrowserOpenTarget = browser.cleanup;
|
|
1057
1023
|
if (!browser.opened) {
|
|
@@ -19,6 +19,7 @@ export const ENV_STRING_CONFIG_FLAG_MAP = {
|
|
|
19
19
|
'app-public-path': 'appPublicPath',
|
|
20
20
|
'cdn-base-url': 'cdnBaseUrl',
|
|
21
21
|
'env-file': 'envFile',
|
|
22
|
+
'app-client-entry-mode': 'appClientEntryMode',
|
|
22
23
|
'app-port': 'appPort',
|
|
23
24
|
'app-key': 'appKey',
|
|
24
25
|
timezone: 'timezone',
|
package/dist/lib/env-config.js
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
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 { normalizeAppClientEntryMode } from './app-client-entry-mode.js';
|
|
9
10
|
import { normalizeEnvProxyConfig } from './env-proxy-config.js';
|
|
10
11
|
import { resolveAppPublicPath } from './app-public-path.js';
|
|
12
|
+
import { normalizeEnvPortalsConfig } from './env-portal-config.js';
|
|
11
13
|
const STRING_ENV_CONFIG_KEYS = [
|
|
12
14
|
'source',
|
|
13
15
|
'downloadVersion',
|
|
@@ -36,8 +38,6 @@ const STRING_ENV_CONFIG_KEYS = [
|
|
|
36
38
|
'dbSchema',
|
|
37
39
|
'dbTablePrefix',
|
|
38
40
|
'lang',
|
|
39
|
-
'portalType',
|
|
40
|
-
'portalName',
|
|
41
41
|
'portalTemplate',
|
|
42
42
|
'rootUsername',
|
|
43
43
|
'rootEmail',
|
|
@@ -86,6 +86,10 @@ export function buildStoredEnvConfig(input) {
|
|
|
86
86
|
envConfig[key] = key === 'appPublicPath' ? resolveAppPublicPath(value) : value;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
const appClientEntryMode = normalizeAppClientEntryMode(input.appClientEntryMode);
|
|
90
|
+
if (appClientEntryMode) {
|
|
91
|
+
envConfig.appClientEntryMode = appClientEntryMode;
|
|
92
|
+
}
|
|
89
93
|
const setupState = resolveSetupState(input.setupState);
|
|
90
94
|
if (setupState) {
|
|
91
95
|
envConfig.setupState = setupState;
|
|
@@ -118,5 +122,9 @@ export function buildStoredEnvConfig(input) {
|
|
|
118
122
|
if (proxy) {
|
|
119
123
|
envConfig.proxy = proxy;
|
|
120
124
|
}
|
|
125
|
+
const portals = normalizeEnvPortalsConfig(input.portals);
|
|
126
|
+
if (portals) {
|
|
127
|
+
envConfig.portals = portals;
|
|
128
|
+
}
|
|
121
129
|
return envConfig;
|
|
122
130
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
function trimValue(value) {
|
|
10
|
+
return String(value ?? '').trim();
|
|
11
|
+
}
|
|
12
|
+
function readRecord(value) {
|
|
13
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
14
|
+
}
|
|
15
|
+
export function normalizeEnvPortalsConfig(value) {
|
|
16
|
+
const input = readRecord(value);
|
|
17
|
+
const portals = {};
|
|
18
|
+
for (const [portal, rawEntry] of Object.entries(input)) {
|
|
19
|
+
const portalName = trimValue(portal);
|
|
20
|
+
if (!portalName) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const entry = readRecord(rawEntry);
|
|
24
|
+
const portalPath = trimValue(entry.path);
|
|
25
|
+
if (portalPath) {
|
|
26
|
+
portals[portalName] = { path: portalPath };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return Object.keys(portals).length > 0 ? portals : undefined;
|
|
30
|
+
}
|
package/dist/lib/env-proxy.js
CHANGED
|
@@ -22,7 +22,7 @@ const DEFAULT_PLUGIN_STATICS_PATH = '/static/plugins/';
|
|
|
22
22
|
const DEFAULT_MODERN_CLIENT_PREFIX = 'v';
|
|
23
23
|
const SETTINGS_CLIENT_PREFIX = 'settings';
|
|
24
24
|
const DEFAULT_APP_CLIENT_ENTRY_MODE = 'legacy-default';
|
|
25
|
-
const APP_CLIENT_ENTRY_MODES = new Set(['legacy-default', 'modern-default', 'modern-only']);
|
|
25
|
+
const APP_CLIENT_ENTRY_MODES = new Set(['legacy-default', 'modern-default', 'modern-only', 'settings-default']);
|
|
26
26
|
const DEFAULT_API_CLIENT_STORAGE_PREFIX = 'NOCOBASE_';
|
|
27
27
|
const DEFAULT_API_CLIENT_STORAGE_TYPE = 'localStorage';
|
|
28
28
|
const DEFAULT_ESM_CDN_BASE_URL = 'https://esm.sh';
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
+
import { normalizeAppClientEntryMode } from './app-client-entry-mode.js';
|
|
11
12
|
import { resolveEnvKind } from './auth-store.js';
|
|
12
13
|
import { resolveConfiguredEnvPath } from './cli-home.js';
|
|
13
14
|
import { resolveDockerEnvFileArg, resolveDockerEnvFilePath } from "./docker-env-file.js";
|
|
@@ -17,6 +18,14 @@ export const DEFAULT_MANAGED_ENV_FILE_VALUES = {
|
|
|
17
18
|
APP_PROCESS_ADAPTER: 'local',
|
|
18
19
|
APP_CLIENT_ENTRY_MODE: 'modern-only',
|
|
19
20
|
};
|
|
21
|
+
function buildManagedEnvFileDefaults(config, defaults = DEFAULT_MANAGED_ENV_FILE_VALUES) {
|
|
22
|
+
return {
|
|
23
|
+
...defaults,
|
|
24
|
+
APP_CLIENT_ENTRY_MODE: normalizeAppClientEntryMode(config?.appClientEntryMode) ??
|
|
25
|
+
trimValue(defaults.APP_CLIENT_ENTRY_MODE) ??
|
|
26
|
+
DEFAULT_MANAGED_ENV_FILE_VALUES.APP_CLIENT_ENTRY_MODE,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
20
29
|
function trimValue(value) {
|
|
21
30
|
const text = String(value ?? '').trim();
|
|
22
31
|
return text || undefined;
|
|
@@ -36,6 +45,29 @@ function stripWrappingQuotes(value) {
|
|
|
36
45
|
}
|
|
37
46
|
return value;
|
|
38
47
|
}
|
|
48
|
+
function escapeRegExp(value) {
|
|
49
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
50
|
+
}
|
|
51
|
+
function upsertSimpleEnvContent(content, values) {
|
|
52
|
+
let nextContent = content;
|
|
53
|
+
const missingEntries = new Map(Object.entries(values).filter(([, value]) => trimValue(value)));
|
|
54
|
+
for (const key of Array.from(missingEntries.keys())) {
|
|
55
|
+
const pattern = new RegExp(`^\\s*(?:export\\s+)?${escapeRegExp(key)}\\s*=.*$`, 'gm');
|
|
56
|
+
if (!pattern.test(nextContent)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
nextContent = nextContent.replace(pattern, `${key}=${values[key]}`);
|
|
60
|
+
missingEntries.delete(key);
|
|
61
|
+
}
|
|
62
|
+
if (missingEntries.size === 0) {
|
|
63
|
+
return nextContent;
|
|
64
|
+
}
|
|
65
|
+
const separator = nextContent && !nextContent.endsWith('\n') ? '\n' : '';
|
|
66
|
+
const appended = Array.from(missingEntries.entries())
|
|
67
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
68
|
+
.join('\n');
|
|
69
|
+
return `${nextContent}${separator}${appended}\n`;
|
|
70
|
+
}
|
|
39
71
|
export function parseSimpleEnvFile(content) {
|
|
40
72
|
const values = {};
|
|
41
73
|
for (const rawLine of content.split(/\r?\n/)) {
|
|
@@ -112,7 +144,8 @@ export async function ensureManagedEnvFileDefaults(envName, config, defaults = D
|
|
|
112
144
|
}
|
|
113
145
|
}
|
|
114
146
|
const existing = parseSimpleEnvFile(content);
|
|
115
|
-
const
|
|
147
|
+
const resolvedDefaults = buildManagedEnvFileDefaults(config, defaults);
|
|
148
|
+
const missingEntries = Object.entries(resolvedDefaults).filter(([key, value]) => trimValue(value) && !existing[key]);
|
|
116
149
|
if (missingEntries.length === 0) {
|
|
117
150
|
return envFilePath;
|
|
118
151
|
}
|
|
@@ -122,6 +155,29 @@ export async function ensureManagedEnvFileDefaults(envName, config, defaults = D
|
|
|
122
155
|
await writeFile(envFilePath, nextContent, 'utf8');
|
|
123
156
|
return envFilePath;
|
|
124
157
|
}
|
|
158
|
+
export async function upsertManagedEnvFileValues(envName, config, values) {
|
|
159
|
+
const envFilePath = resolveManagedEnvFilePathFromConfig(envName, config);
|
|
160
|
+
if (!envFilePath) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
let content = '';
|
|
164
|
+
try {
|
|
165
|
+
content = await readFile(envFilePath, 'utf8');
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
|
|
169
|
+
if (code !== 'ENOENT') {
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const nextContent = upsertSimpleEnvContent(content, values);
|
|
174
|
+
if (nextContent === content) {
|
|
175
|
+
return envFilePath;
|
|
176
|
+
}
|
|
177
|
+
await mkdir(path.dirname(envFilePath), { recursive: true });
|
|
178
|
+
await writeFile(envFilePath, nextContent, 'utf8');
|
|
179
|
+
return envFilePath;
|
|
180
|
+
}
|
|
125
181
|
export async function resolveManagedRuntimeEnvFilePath(runtime) {
|
|
126
182
|
if (runtime.kind === 'local') {
|
|
127
183
|
return resolveManagedLocalEnvFilePath(runtime);
|
|
@@ -29,8 +29,6 @@ export function buildInitAppEnvVarsFromConfig(config, options = {}) {
|
|
|
29
29
|
put('INIT_ROOT_PASSWORD', config?.rootPassword);
|
|
30
30
|
put('INIT_ROOT_NICKNAME', config?.rootNickname);
|
|
31
31
|
if (options.includePortal !== false) {
|
|
32
|
-
put('INIT_PORTAL_TYPE', config?.portalType);
|
|
33
|
-
put('INIT_PORTAL_NAME', config?.portalName);
|
|
34
32
|
put('INIT_PORTAL_TEMPLATE', config?.portalTemplate);
|
|
35
33
|
}
|
|
36
34
|
return out;
|
|
@@ -6,7 +6,6 @@
|
|
|
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 { readFile, writeFile } from 'node:fs/promises';
|
|
10
9
|
import path from 'node:path';
|
|
11
10
|
import { executeApiRequest } from './api-client.js';
|
|
12
11
|
import { translateCli } from './cli-locale.js';
|
|
@@ -99,22 +98,6 @@ export function mergePortalConfigIntoOptions(config, currentOptions) {
|
|
|
99
98
|
}
|
|
100
99
|
return nextOptions;
|
|
101
100
|
}
|
|
102
|
-
export async function readPortalConfig(portalDir) {
|
|
103
|
-
const configPath = path.join(portalDir, 'portal.config.json');
|
|
104
|
-
const data = JSON.parse(await readFile(configPath, 'utf-8'));
|
|
105
|
-
const config = readObject(data);
|
|
106
|
-
const git = readObject(config.git);
|
|
107
|
-
return buildPortalConfig({
|
|
108
|
-
portal: path.basename(portalDir),
|
|
109
|
-
sourceStorage: trimValue(config.sourceStorage),
|
|
110
|
-
gitRepo: trimValue(git.repo),
|
|
111
|
-
gitBranch: trimValue(git.branch),
|
|
112
|
-
gitPath: trimValue(git.path),
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
export async function writePortalConfig(portalDir, config) {
|
|
116
|
-
await writeFile(path.join(portalDir, 'portal.config.json'), `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
|
|
117
|
-
}
|
|
118
101
|
export async function syncPortalConfigToRemote(options) {
|
|
119
102
|
const apiRequest = options.apiRequest ?? executeApiRequest;
|
|
120
103
|
const response = await apiRequest({
|