@nocobase/cli 2.1.0-beta.35 → 2.1.0-beta.37
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/bin/run.js +3 -2
- package/dist/commands/app/upgrade.js +38 -16
- package/dist/commands/backup/create.js +147 -0
- package/dist/commands/backup/index.js +20 -0
- package/dist/commands/backup/restore.js +105 -0
- package/dist/commands/config/delete.js +4 -0
- package/dist/commands/config/get.js +4 -0
- package/dist/commands/config/set.js +5 -1
- package/dist/commands/env/add.js +129 -15
- package/dist/commands/env/auth.js +145 -12
- package/dist/commands/env/info.js +52 -8
- package/dist/commands/env/list.js +2 -2
- package/dist/commands/env/shared.js +41 -3
- package/dist/commands/init.js +254 -136
- package/dist/commands/install.js +447 -272
- package/dist/commands/license/activate.js +6 -4
- package/dist/commands/source/publish.js +17 -0
- package/dist/commands/v1.js +210 -0
- package/dist/lib/app-managed-resources.js +20 -1
- package/dist/lib/app-runtime.js +13 -4
- package/dist/lib/auth-store.js +69 -18
- package/dist/lib/backup.js +171 -0
- package/dist/lib/bootstrap.js +23 -13
- package/dist/lib/cli-config.js +99 -4
- package/dist/lib/cli-locale.js +19 -7
- package/dist/lib/db-connection-check.js +61 -0
- package/dist/lib/env-auth.js +79 -0
- package/dist/lib/env-config.js +8 -1
- package/dist/lib/prompt-validators.js +23 -5
- package/dist/lib/prompt-web-ui.js +143 -19
- package/dist/lib/run-npm.js +166 -30
- package/dist/lib/skills-manager.js +74 -4
- package/dist/lib/source-publish.js +20 -1
- package/dist/lib/source-registry.js +2 -2
- package/dist/locale/en-US.json +36 -5
- package/dist/locale/zh-CN.json +36 -5
- package/package.json +6 -3
|
@@ -0,0 +1,171 @@
|
|
|
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 { promises as fs } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { getCurrentEnvName, getEnv, listEnvs } from './auth-store.js';
|
|
13
|
+
import { updateEnvRuntime } from './bootstrap.js';
|
|
14
|
+
import { resolveDefaultConfigScope } from './cli-home.js';
|
|
15
|
+
import { commandOutput } from './run-npm.js';
|
|
16
|
+
import { loadRuntimeSync } from './runtime-store.js';
|
|
17
|
+
import { failTask, startTask, succeedTask } from './ui.js';
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const CLI_PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
|
|
20
|
+
const CLI_CONFIG_FILE = path.join(CLI_PACKAGE_ROOT, 'nocobase-ctl.config.json');
|
|
21
|
+
const CLI_ENTRY_FILE = path.join(CLI_PACKAGE_ROOT, 'bin', 'run.js');
|
|
22
|
+
export const BACKUP_POLL_INTERVAL_MS = 2_000;
|
|
23
|
+
export const BACKUP_CREATE_TIMEOUT_MS = 600_000;
|
|
24
|
+
export const BACKUP_RUNTIME_COMMANDS = {
|
|
25
|
+
create: 'backup create',
|
|
26
|
+
status: 'backup status',
|
|
27
|
+
download: 'backup download',
|
|
28
|
+
restoreUpload: 'backup restore-upload',
|
|
29
|
+
};
|
|
30
|
+
function hasRequiredBackupCommands(runtime, commandIds) {
|
|
31
|
+
if (!runtime) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const available = new Set(runtime.commands.map((command) => command.commandId));
|
|
35
|
+
return commandIds.every((commandId) => available.has(commandId));
|
|
36
|
+
}
|
|
37
|
+
function formatMissingBackupRuntimeCommands(envName, commandIds) {
|
|
38
|
+
const missing = commandIds.map((commandId) => `nb api ${commandId}`).join(', ');
|
|
39
|
+
return [
|
|
40
|
+
`The selected env "${envName}" does not expose the backup API commands required by \`nb backup\`.`,
|
|
41
|
+
`Missing commands: ${missing}`,
|
|
42
|
+
'Enable or upgrade the backup/restore capability for that env, then try again.',
|
|
43
|
+
].join('\n');
|
|
44
|
+
}
|
|
45
|
+
export function buildBackupEnvArgv(options) {
|
|
46
|
+
const argv = [];
|
|
47
|
+
if (options.explicitEnvSelection && options.requestedEnv) {
|
|
48
|
+
argv.push('--env', options.requestedEnv);
|
|
49
|
+
}
|
|
50
|
+
if (options.yes || options.explicitEnvSelection) {
|
|
51
|
+
argv.push('--yes');
|
|
52
|
+
}
|
|
53
|
+
return argv;
|
|
54
|
+
}
|
|
55
|
+
export async function resolveBackupTargetEnv(requestedEnv) {
|
|
56
|
+
const scope = resolveDefaultConfigScope();
|
|
57
|
+
const envName = requestedEnv?.trim() || (await getCurrentEnvName({ scope }));
|
|
58
|
+
const env = await getEnv(envName, { scope });
|
|
59
|
+
if (env) {
|
|
60
|
+
return { scope, envName, env };
|
|
61
|
+
}
|
|
62
|
+
const { envs } = await listEnvs({ scope });
|
|
63
|
+
const configuredEnvNames = Object.keys(envs);
|
|
64
|
+
if (!configuredEnvNames.length) {
|
|
65
|
+
throw new Error('No env is configured. Run `nb env add <name> --api-base-url <url>` first.');
|
|
66
|
+
}
|
|
67
|
+
if (requestedEnv?.trim()) {
|
|
68
|
+
throw new Error(`Env "${envName}" is not configured. Run \`nb env add ${envName} --api-base-url <url>\` first.`);
|
|
69
|
+
}
|
|
70
|
+
throw new Error([
|
|
71
|
+
`Current env "${envName}" is not configured.`,
|
|
72
|
+
'Switch to an existing env with `nb env use <name>`, or add one with `nb env add <name> --api-base-url <url>`.',
|
|
73
|
+
].join('\n'));
|
|
74
|
+
}
|
|
75
|
+
export async function ensureBackupRuntimeCommands(params) {
|
|
76
|
+
const scope = resolveDefaultConfigScope();
|
|
77
|
+
const env = params.env ?? (await getEnv(params.envName, { scope }));
|
|
78
|
+
const currentRuntime = loadRuntimeSync(env?.runtime?.version, { scope });
|
|
79
|
+
if (hasRequiredBackupCommands(currentRuntime, params.commandIds)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!params.quiet) {
|
|
83
|
+
startTask(`Refreshing env runtime for "${params.envName}" to load backup commands...`);
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const runtime = await updateEnvRuntime({
|
|
87
|
+
envName: params.envName,
|
|
88
|
+
scope,
|
|
89
|
+
configFile: CLI_CONFIG_FILE,
|
|
90
|
+
quiet: params.quiet,
|
|
91
|
+
});
|
|
92
|
+
if (!hasRequiredBackupCommands(runtime, params.commandIds)) {
|
|
93
|
+
throw new Error(formatMissingBackupRuntimeCommands(params.envName, params.commandIds));
|
|
94
|
+
}
|
|
95
|
+
if (!params.quiet) {
|
|
96
|
+
succeedTask(`Env runtime is ready for backup commands in "${params.envName}".`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (!params.quiet) {
|
|
101
|
+
failTask(`Failed to refresh backup commands for "${params.envName}".`);
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export async function runBackupCliCommand(argv, options) {
|
|
107
|
+
return await commandOutput(process.execPath, [CLI_ENTRY_FILE, ...argv], {
|
|
108
|
+
errorName: options?.errorName ?? `nb ${argv.join(' ')}`,
|
|
109
|
+
env: {
|
|
110
|
+
NB_SKIP_STARTUP_UPDATE: '1',
|
|
111
|
+
// When the parent CLI already runs in tsx source mode, it sets
|
|
112
|
+
// `_NOCO_CLI_TSX_CHILD=1`. Clear it here so `bin/run.js` can re-exec
|
|
113
|
+
// itself with `--import tsx` again instead of trying to import `.ts`
|
|
114
|
+
// sources without the loader.
|
|
115
|
+
_NOCO_CLI_TSX_CHILD: '',
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
export async function runBackupCliJsonCommand(argv, options) {
|
|
120
|
+
const output = await runBackupCliCommand([...argv, '--json-output'], options);
|
|
121
|
+
try {
|
|
122
|
+
return JSON.parse(output);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
throw new Error(`Unexpected JSON output from ${options?.errorName ?? `nb ${argv.join(' ')}`}: ${output || '(empty output)'}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export async function resolveBackupCreateOutputPath(output, remoteName) {
|
|
129
|
+
const requestedOutput = String(output ?? '').trim();
|
|
130
|
+
if (!requestedOutput) {
|
|
131
|
+
return path.resolve(process.cwd(), remoteName);
|
|
132
|
+
}
|
|
133
|
+
const resolvedOutput = path.resolve(process.cwd(), requestedOutput);
|
|
134
|
+
try {
|
|
135
|
+
const stats = await fs.stat(resolvedOutput);
|
|
136
|
+
if (stats.isDirectory()) {
|
|
137
|
+
return path.join(resolvedOutput, remoteName);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Treat non-existing paths as an explicit target file path.
|
|
142
|
+
}
|
|
143
|
+
return resolvedOutput;
|
|
144
|
+
}
|
|
145
|
+
export async function resolveBackupRestoreFilePath(file) {
|
|
146
|
+
const resolvedFile = path.resolve(process.cwd(), file);
|
|
147
|
+
let stats;
|
|
148
|
+
try {
|
|
149
|
+
stats = await fs.stat(resolvedFile);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
throw new Error(`Backup file not found: ${resolvedFile}`);
|
|
153
|
+
}
|
|
154
|
+
if (!stats.isFile()) {
|
|
155
|
+
throw new Error(`Backup restore input must be a file: ${resolvedFile}`);
|
|
156
|
+
}
|
|
157
|
+
return resolvedFile;
|
|
158
|
+
}
|
|
159
|
+
export function resolveBackupWaitApiBaseUrl(env) {
|
|
160
|
+
const baseUrl = String(env.baseUrl ?? '').trim();
|
|
161
|
+
if (baseUrl) {
|
|
162
|
+
return baseUrl.replace(/\/+$/, '');
|
|
163
|
+
}
|
|
164
|
+
const appPort = env.appPort === undefined || env.appPort === null
|
|
165
|
+
? ''
|
|
166
|
+
: String(env.appPort).trim();
|
|
167
|
+
return appPort ? `http://127.0.0.1:${appPort}/api` : undefined;
|
|
168
|
+
}
|
|
169
|
+
export async function sleep(ms) {
|
|
170
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
171
|
+
}
|
package/dist/lib/bootstrap.js
CHANGED
|
@@ -137,7 +137,7 @@ function getSwaggerUrl(baseUrl) {
|
|
|
137
137
|
function getHealthCheckUrl(baseUrl) {
|
|
138
138
|
return `${baseUrl.replace(/\/+$/, '')}/__health_check`;
|
|
139
139
|
}
|
|
140
|
-
async function waitForServiceReady(baseUrl, token, role) {
|
|
140
|
+
async function waitForServiceReady(baseUrl, token, role, options) {
|
|
141
141
|
const healthCheckUrl = getHealthCheckUrl(baseUrl);
|
|
142
142
|
const startedAt = Date.now();
|
|
143
143
|
let notified = false;
|
|
@@ -162,18 +162,22 @@ async function waitForServiceReady(baseUrl, token, role) {
|
|
|
162
162
|
return;
|
|
163
163
|
}
|
|
164
164
|
if (!notified) {
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
if (!options?.quiet) {
|
|
166
|
+
printVerbose(`Waiting for health check: ${healthCheckUrl}`);
|
|
167
|
+
updateTask(`Waiting for application readiness (${healthCheckUrl})`);
|
|
168
|
+
}
|
|
167
169
|
notified = true;
|
|
168
170
|
}
|
|
169
171
|
await sleep(APP_RETRY_INTERVAL);
|
|
170
172
|
}
|
|
171
173
|
throw new Error(`The application did not become ready in time. Expected \`${healthCheckUrl}\` to respond with \`ok\`.`);
|
|
172
174
|
}
|
|
173
|
-
async function waitForSwaggerSchema(baseUrl, token, role) {
|
|
175
|
+
async function waitForSwaggerSchema(baseUrl, token, role, options) {
|
|
174
176
|
const swaggerUrl = getSwaggerUrl(baseUrl);
|
|
175
177
|
const startedAt = Date.now();
|
|
176
|
-
|
|
178
|
+
if (!options?.quiet) {
|
|
179
|
+
printVerbose(`Checking swagger schema: ${swaggerUrl}`);
|
|
180
|
+
}
|
|
177
181
|
while (Date.now() - startedAt < APP_RETRY_TIMEOUT) {
|
|
178
182
|
const response = await requestJson(swaggerUrl, { token, role });
|
|
179
183
|
if (response.ok) {
|
|
@@ -182,7 +186,7 @@ async function waitForSwaggerSchema(baseUrl, token, role) {
|
|
|
182
186
|
if (!shouldRetryAppAvailability(response)) {
|
|
183
187
|
return response;
|
|
184
188
|
}
|
|
185
|
-
await waitForServiceReady(baseUrl, token, role);
|
|
189
|
+
await waitForServiceReady(baseUrl, token, role, options);
|
|
186
190
|
}
|
|
187
191
|
return await requestJson(swaggerUrl, { token, role });
|
|
188
192
|
}
|
|
@@ -200,9 +204,9 @@ async function confirmEnableApiDoc() {
|
|
|
200
204
|
async function fetchSwaggerSchema(baseUrl, token, role, context = {}, options = {}) {
|
|
201
205
|
let response = options.retryAppAvailability === false
|
|
202
206
|
? await requestJson(getSwaggerUrl(baseUrl), { token, role })
|
|
203
|
-
: await waitForSwaggerSchema(baseUrl, token, role);
|
|
207
|
+
: await waitForSwaggerSchema(baseUrl, token, role, { quiet: options.quiet });
|
|
204
208
|
if (response.status === 404) {
|
|
205
|
-
if (options.allowEnableApiDoc === false) {
|
|
209
|
+
if (options.allowEnableApiDoc === false || options.quiet) {
|
|
206
210
|
throw new Error('`swagger:get` returned 404. Check the base URL and enable the `API documentation plugin` if needed.');
|
|
207
211
|
}
|
|
208
212
|
printInfo('The API documentation plugin is not enabled.');
|
|
@@ -257,7 +261,7 @@ export function formatSwaggerSchemaError(response, context) {
|
|
|
257
261
|
`Authentication failed while loading the command runtime from \`swagger:get\`${envLabel}.`,
|
|
258
262
|
`Base URL: ${context.baseUrl}`,
|
|
259
263
|
details,
|
|
260
|
-
'Update the API key with `nb env add <name> --api-base-url <url> --auth-type token --token <api-key>`, log in with `nb env auth <name>`, or rerun the command with `--token <api-key>`.',
|
|
264
|
+
'Update the API key with `nb env add <name> --api-base-url <url> --auth-type token --access-token <api-key>`, log in with `nb env auth <name>`, or rerun the command with `--access-token <api-key>`.',
|
|
261
265
|
commandHint,
|
|
262
266
|
].join('\n');
|
|
263
267
|
}
|
|
@@ -368,10 +372,14 @@ export async function updateEnvRuntime(options) {
|
|
|
368
372
|
.filter(Boolean)
|
|
369
373
|
.join('\n'));
|
|
370
374
|
}
|
|
371
|
-
|
|
375
|
+
if (!options.quiet) {
|
|
376
|
+
updateTask('Loading command runtime...');
|
|
377
|
+
}
|
|
372
378
|
try {
|
|
373
|
-
|
|
374
|
-
|
|
379
|
+
if (!options.quiet) {
|
|
380
|
+
printVerbose(`Runtime source: ${baseUrl}`);
|
|
381
|
+
}
|
|
382
|
+
const document = await fetchSwaggerSchema(baseUrl, token, options.role, { envName }, { quiet: options.quiet });
|
|
375
383
|
const runtime = await generateRuntime(document, options.configFile, baseUrl);
|
|
376
384
|
await saveRuntime(runtime, { scope: options.scope });
|
|
377
385
|
if (options.baseUrl !== undefined || options.token !== undefined) {
|
|
@@ -388,6 +396,8 @@ export async function updateEnvRuntime(options) {
|
|
|
388
396
|
return runtime;
|
|
389
397
|
}
|
|
390
398
|
finally {
|
|
391
|
-
|
|
399
|
+
if (!options.quiet) {
|
|
400
|
+
stopTask();
|
|
401
|
+
}
|
|
392
402
|
}
|
|
393
403
|
}
|
package/dist/lib/cli-config.js
CHANGED
|
@@ -8,13 +8,21 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { loadExactAuthConfig, saveAuthConfig } from './auth-store.js';
|
|
10
10
|
import { resolveDefaultConfigScope } from './cli-home.js';
|
|
11
|
+
import { CLI_LOCALE_FLAG_OPTIONS, normalizeCliLocale, resolveCliLocale } from './cli-locale.js';
|
|
11
12
|
export const DEFAULT_LICENSE_PKG_URL = 'https://pkg.nocobase.com/';
|
|
12
13
|
export const DEFAULT_DOCKER_NETWORK = 'nocobase';
|
|
13
14
|
export const DEFAULT_DOCKER_CONTAINER_PREFIX = 'nb';
|
|
15
|
+
export const DEFAULT_DOCKER_BIN = 'docker';
|
|
16
|
+
export const DEFAULT_GIT_BIN = 'git';
|
|
17
|
+
export const DEFAULT_YARN_BIN = 'yarn';
|
|
14
18
|
export const SUPPORTED_CLI_CONFIG_KEYS = [
|
|
19
|
+
'locale',
|
|
15
20
|
'license.pkg-url',
|
|
16
21
|
'docker.network',
|
|
17
22
|
'docker.container-prefix',
|
|
23
|
+
'bin.docker',
|
|
24
|
+
'bin.git',
|
|
25
|
+
'bin.yarn',
|
|
18
26
|
];
|
|
19
27
|
function trimValue(value) {
|
|
20
28
|
const text = String(value ?? '').trim();
|
|
@@ -36,11 +44,16 @@ export function assertSupportedCliConfigKey(value) {
|
|
|
36
44
|
}
|
|
37
45
|
function cloneSettings(config) {
|
|
38
46
|
return {
|
|
47
|
+
...(config.settings?.locale ? { locale: trimValue(config.settings.locale) } : {}),
|
|
39
48
|
license: config.settings?.license ? { ...config.settings.license } : undefined,
|
|
40
49
|
docker: config.settings?.docker ? { ...config.settings.docker } : undefined,
|
|
50
|
+
bin: config.settings?.bin ? { ...config.settings.bin } : undefined,
|
|
41
51
|
};
|
|
42
52
|
}
|
|
43
53
|
function pruneSettings(config) {
|
|
54
|
+
if (config.settings && !trimValue(config.settings.locale)) {
|
|
55
|
+
delete config.settings.locale;
|
|
56
|
+
}
|
|
44
57
|
const license = config.settings?.license;
|
|
45
58
|
if (license && !trimValue(license.pkgUrl)) {
|
|
46
59
|
delete config.settings?.license;
|
|
@@ -49,34 +62,56 @@ function pruneSettings(config) {
|
|
|
49
62
|
if (docker && !trimValue(docker.network) && !trimValue(docker.containerPrefix)) {
|
|
50
63
|
delete config.settings?.docker;
|
|
51
64
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
const bin = config.settings?.bin;
|
|
66
|
+
if (bin && !trimValue(bin.docker) && !trimValue(bin.git) && !trimValue(bin.yarn)) {
|
|
67
|
+
delete config.settings?.bin;
|
|
68
|
+
}
|
|
69
|
+
if (config.settings &&
|
|
70
|
+
!config.settings.locale &&
|
|
71
|
+
!config.settings.license &&
|
|
72
|
+
!config.settings.docker &&
|
|
73
|
+
!config.settings.bin) {
|
|
55
74
|
delete config.settings;
|
|
56
75
|
}
|
|
57
76
|
}
|
|
58
77
|
export function getExplicitCliConfigValue(config, key) {
|
|
59
78
|
switch (key) {
|
|
79
|
+
case 'locale':
|
|
80
|
+
return trimValue(config.settings?.locale);
|
|
60
81
|
case 'license.pkg-url':
|
|
61
82
|
return trimValue(config.settings?.license?.pkgUrl);
|
|
62
83
|
case 'docker.network':
|
|
63
84
|
return trimValue(config.settings?.docker?.network);
|
|
64
85
|
case 'docker.container-prefix':
|
|
65
86
|
return trimValue(config.settings?.docker?.containerPrefix);
|
|
87
|
+
case 'bin.docker':
|
|
88
|
+
return trimValue(config.settings?.bin?.docker);
|
|
89
|
+
case 'bin.git':
|
|
90
|
+
return trimValue(config.settings?.bin?.git);
|
|
91
|
+
case 'bin.yarn':
|
|
92
|
+
return trimValue(config.settings?.bin?.yarn);
|
|
66
93
|
}
|
|
67
94
|
}
|
|
68
95
|
export function getEffectiveCliConfigValue(config, key) {
|
|
69
96
|
const explicit = getExplicitCliConfigValue(config, key);
|
|
70
|
-
if (explicit) {
|
|
97
|
+
if (explicit && key !== 'locale') {
|
|
71
98
|
return explicit;
|
|
72
99
|
}
|
|
73
100
|
switch (key) {
|
|
101
|
+
case 'locale':
|
|
102
|
+
return resolveCliLocale(undefined, { configuredLocale: trimValue(config.settings?.locale) });
|
|
74
103
|
case 'license.pkg-url':
|
|
75
104
|
return DEFAULT_LICENSE_PKG_URL;
|
|
76
105
|
case 'docker.network':
|
|
77
106
|
return trimValue(config.name) || DEFAULT_DOCKER_NETWORK;
|
|
78
107
|
case 'docker.container-prefix':
|
|
79
108
|
return trimValue(config.name) || DEFAULT_DOCKER_CONTAINER_PREFIX;
|
|
109
|
+
case 'bin.docker':
|
|
110
|
+
return DEFAULT_DOCKER_BIN;
|
|
111
|
+
case 'bin.git':
|
|
112
|
+
return DEFAULT_GIT_BIN;
|
|
113
|
+
case 'bin.yarn':
|
|
114
|
+
return DEFAULT_YARN_BIN;
|
|
80
115
|
}
|
|
81
116
|
}
|
|
82
117
|
export function normalizeCliConfigValue(key, value) {
|
|
@@ -87,6 +122,13 @@ export function normalizeCliConfigValue(key, value) {
|
|
|
87
122
|
if (key === 'license.pkg-url') {
|
|
88
123
|
return normalized.replace(/\/+$/, '') + '/';
|
|
89
124
|
}
|
|
125
|
+
if (key === 'locale') {
|
|
126
|
+
const locale = normalizeCliLocale(normalized);
|
|
127
|
+
if (!locale) {
|
|
128
|
+
throw new Error(`Config key "${key}" must be one of: ${CLI_LOCALE_FLAG_OPTIONS.join(', ')}`);
|
|
129
|
+
}
|
|
130
|
+
return locale;
|
|
131
|
+
}
|
|
90
132
|
return normalized;
|
|
91
133
|
}
|
|
92
134
|
export async function loadCliConfig(options = {}) {
|
|
@@ -113,6 +155,9 @@ export async function setCliConfigValue(key, value, options = {}) {
|
|
|
113
155
|
const normalized = normalizeCliConfigValue(key, value);
|
|
114
156
|
config.settings = cloneSettings(config);
|
|
115
157
|
switch (key) {
|
|
158
|
+
case 'locale':
|
|
159
|
+
config.settings.locale = normalized;
|
|
160
|
+
break;
|
|
116
161
|
case 'license.pkg-url':
|
|
117
162
|
config.settings.license = {
|
|
118
163
|
...(config.settings.license ?? {}),
|
|
@@ -131,6 +176,24 @@ export async function setCliConfigValue(key, value, options = {}) {
|
|
|
131
176
|
containerPrefix: normalized,
|
|
132
177
|
};
|
|
133
178
|
break;
|
|
179
|
+
case 'bin.docker':
|
|
180
|
+
config.settings.bin = {
|
|
181
|
+
...(config.settings.bin ?? {}),
|
|
182
|
+
docker: normalized,
|
|
183
|
+
};
|
|
184
|
+
break;
|
|
185
|
+
case 'bin.git':
|
|
186
|
+
config.settings.bin = {
|
|
187
|
+
...(config.settings.bin ?? {}),
|
|
188
|
+
git: normalized,
|
|
189
|
+
};
|
|
190
|
+
break;
|
|
191
|
+
case 'bin.yarn':
|
|
192
|
+
config.settings.bin = {
|
|
193
|
+
...(config.settings.bin ?? {}),
|
|
194
|
+
yarn: normalized,
|
|
195
|
+
};
|
|
196
|
+
break;
|
|
134
197
|
}
|
|
135
198
|
pruneSettings(config);
|
|
136
199
|
await saveAuthConfig(config, scope);
|
|
@@ -145,6 +208,9 @@ export async function deleteCliConfigValue(key, options = {}) {
|
|
|
145
208
|
}
|
|
146
209
|
config.settings = cloneSettings(config);
|
|
147
210
|
switch (key) {
|
|
211
|
+
case 'locale':
|
|
212
|
+
delete config.settings.locale;
|
|
213
|
+
break;
|
|
148
214
|
case 'license.pkg-url':
|
|
149
215
|
if (config.settings.license) {
|
|
150
216
|
delete config.settings.license.pkgUrl;
|
|
@@ -160,6 +226,21 @@ export async function deleteCliConfigValue(key, options = {}) {
|
|
|
160
226
|
delete config.settings.docker.containerPrefix;
|
|
161
227
|
}
|
|
162
228
|
break;
|
|
229
|
+
case 'bin.docker':
|
|
230
|
+
if (config.settings.bin) {
|
|
231
|
+
delete config.settings.bin.docker;
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
case 'bin.git':
|
|
235
|
+
if (config.settings.bin) {
|
|
236
|
+
delete config.settings.bin.git;
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
case 'bin.yarn':
|
|
240
|
+
if (config.settings.bin) {
|
|
241
|
+
delete config.settings.bin.yarn;
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
163
244
|
}
|
|
164
245
|
pruneSettings(config);
|
|
165
246
|
await saveAuthConfig(config, scope);
|
|
@@ -174,3 +255,17 @@ export async function resolveDockerContainerPrefix(options = {}) {
|
|
|
174
255
|
export async function resolveLicensePkgUrlFromConfig(options = {}) {
|
|
175
256
|
return await getCliConfigValue('license.pkg-url', options);
|
|
176
257
|
}
|
|
258
|
+
const CONFIGURABLE_COMMAND_KEYS = {
|
|
259
|
+
docker: 'bin.docker',
|
|
260
|
+
git: 'bin.git',
|
|
261
|
+
yarn: 'bin.yarn',
|
|
262
|
+
};
|
|
263
|
+
export function isConfigurableCommandName(value) {
|
|
264
|
+
return Object.prototype.hasOwnProperty.call(CONFIGURABLE_COMMAND_KEYS, value);
|
|
265
|
+
}
|
|
266
|
+
export async function resolveConfiguredCommandName(commandName, options = {}) {
|
|
267
|
+
if (!isConfigurableCommandName(commandName)) {
|
|
268
|
+
return commandName;
|
|
269
|
+
}
|
|
270
|
+
return await getCliConfigValue(CONFIGURABLE_COMMAND_KEYS[commandName], options);
|
|
271
|
+
}
|
package/dist/lib/cli-locale.js
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
import { readFileSync } from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { resolveCliHomeDir } from './cli-home.js';
|
|
12
13
|
export const SUPPORTED_CLI_LOCALES = ['en-US', 'zh-CN'];
|
|
13
14
|
export const CLI_LOCALE_FLAG_OPTIONS = [...SUPPORTED_CLI_LOCALES];
|
|
14
15
|
export const CLI_LOCALE_FLAG_DESCRIPTION = 'Language for CLI prompts and the local setup UI.';
|
|
15
16
|
const DEFAULT_CLI_LOCALE = 'en-US';
|
|
16
17
|
const localeCache = {};
|
|
17
|
-
function normalizeCliLocale(value) {
|
|
18
|
+
export function normalizeCliLocale(value) {
|
|
18
19
|
const raw = String(value ?? '').trim();
|
|
19
20
|
if (!raw) {
|
|
20
21
|
return undefined;
|
|
@@ -28,6 +29,17 @@ function normalizeCliLocale(value) {
|
|
|
28
29
|
}
|
|
29
30
|
return undefined;
|
|
30
31
|
}
|
|
32
|
+
function readConfiguredCliLocale() {
|
|
33
|
+
try {
|
|
34
|
+
const configPath = path.join(resolveCliHomeDir(), 'config.json');
|
|
35
|
+
const content = readFileSync(configPath, 'utf8');
|
|
36
|
+
const parsed = JSON.parse(content);
|
|
37
|
+
return normalizeCliLocale(parsed.settings?.locale === undefined ? undefined : String(parsed.settings.locale));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
31
43
|
function loadLocaleMessages(locale) {
|
|
32
44
|
if (localeCache[locale]) {
|
|
33
45
|
return localeCache[locale];
|
|
@@ -65,9 +77,11 @@ function interpolateTemplate(template, values) {
|
|
|
65
77
|
return value === undefined || value === null ? '' : String(value);
|
|
66
78
|
});
|
|
67
79
|
}
|
|
68
|
-
export function detectCliLocale() {
|
|
80
|
+
export function detectCliLocale(configuredLocale) {
|
|
81
|
+
const resolvedConfiguredLocale = configuredLocale ?? readConfiguredCliLocale();
|
|
69
82
|
const candidates = [
|
|
70
83
|
process.env.NB_LOCALE,
|
|
84
|
+
resolvedConfiguredLocale,
|
|
71
85
|
process.env.LC_ALL,
|
|
72
86
|
process.env.LC_MESSAGES,
|
|
73
87
|
process.env.LANG,
|
|
@@ -81,8 +95,8 @@ export function detectCliLocale() {
|
|
|
81
95
|
}
|
|
82
96
|
return DEFAULT_CLI_LOCALE;
|
|
83
97
|
}
|
|
84
|
-
export function resolveCliLocale(preferred) {
|
|
85
|
-
return normalizeCliLocale(preferred) ?? detectCliLocale();
|
|
98
|
+
export function resolveCliLocale(preferred, options) {
|
|
99
|
+
return normalizeCliLocale(preferred) ?? detectCliLocale(options?.configuredLocale);
|
|
86
100
|
}
|
|
87
101
|
export function applyCliLocale(preferred) {
|
|
88
102
|
const locale = resolveCliLocale(preferred);
|
|
@@ -111,9 +125,7 @@ export function localeText(key, values, fallback) {
|
|
|
111
125
|
};
|
|
112
126
|
}
|
|
113
127
|
export function isLocalizedTextDef(value) {
|
|
114
|
-
return Boolean(value
|
|
115
|
-
&& typeof value === 'object'
|
|
116
|
-
&& typeof value.key === 'string');
|
|
128
|
+
return Boolean(value && typeof value === 'object' && typeof value.key === 'string');
|
|
117
129
|
}
|
|
118
130
|
export function resolveLocalizedText(text, options) {
|
|
119
131
|
if (text === undefined) {
|
|
@@ -10,6 +10,7 @@ import { translateCli } from "./cli-locale.js";
|
|
|
10
10
|
import { validateTcpPort } from "./prompt-validators.js";
|
|
11
11
|
const DB_CONNECTION_TIMEOUT_MS = 5_000;
|
|
12
12
|
const externalDbValidationCache = new Map();
|
|
13
|
+
const mysqlLowerCaseTableNamesCache = new Map();
|
|
13
14
|
function trimPromptValue(value) {
|
|
14
15
|
return String(value ?? '').trim();
|
|
15
16
|
}
|
|
@@ -117,6 +118,36 @@ async function checkMysqlFamilyConnection(config) {
|
|
|
117
118
|
await Promise.resolve(connection.end()).catch(() => undefined);
|
|
118
119
|
}
|
|
119
120
|
}
|
|
121
|
+
async function readMysqlFamilyLowerCaseTableNames(config) {
|
|
122
|
+
const { default: mysql } = await import('mysql2/promise');
|
|
123
|
+
const connection = await mysql.createConnection({
|
|
124
|
+
host: config.host,
|
|
125
|
+
port: config.port,
|
|
126
|
+
user: config.user,
|
|
127
|
+
password: config.password,
|
|
128
|
+
database: config.database,
|
|
129
|
+
connectTimeout: DB_CONNECTION_TIMEOUT_MS,
|
|
130
|
+
});
|
|
131
|
+
try {
|
|
132
|
+
const [rows] = await connection.query(`SHOW VARIABLES LIKE 'lower_case_table_names'`);
|
|
133
|
+
if (!Array.isArray(rows)) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
for (const row of rows) {
|
|
137
|
+
if (!row || typeof row !== 'object') {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const value = String(row.Value ?? '').trim();
|
|
141
|
+
if (value === '0' || value === '1' || value === '2') {
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
await Promise.resolve(connection.end()).catch(() => undefined);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
120
151
|
async function performExternalDbConnectionCheck(config) {
|
|
121
152
|
try {
|
|
122
153
|
switch (config.dialect) {
|
|
@@ -146,6 +177,19 @@ export async function checkExternalDbConnection(config) {
|
|
|
146
177
|
externalDbValidationCache.set(cacheKey, pending);
|
|
147
178
|
return await pending;
|
|
148
179
|
}
|
|
180
|
+
async function readMysqlLowerCaseTableNamesMode(config) {
|
|
181
|
+
if (config.dialect !== 'mysql' && config.dialect !== 'mariadb') {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const cacheKey = buildValidationCacheKey(config);
|
|
185
|
+
const cached = mysqlLowerCaseTableNamesCache.get(cacheKey);
|
|
186
|
+
if (cached) {
|
|
187
|
+
return await cached;
|
|
188
|
+
}
|
|
189
|
+
const pending = readMysqlFamilyLowerCaseTableNames(config);
|
|
190
|
+
mysqlLowerCaseTableNamesCache.set(cacheKey, pending);
|
|
191
|
+
return await pending;
|
|
192
|
+
}
|
|
149
193
|
export async function validateExternalDbConfig(values) {
|
|
150
194
|
const config = readExternalDbConnectionConfig(values);
|
|
151
195
|
if (!config) {
|
|
@@ -153,6 +197,23 @@ export async function validateExternalDbConfig(values) {
|
|
|
153
197
|
}
|
|
154
198
|
return await checkExternalDbConnection(config);
|
|
155
199
|
}
|
|
200
|
+
export async function validateMysqlLowerCaseTableNamesCompatibility(values) {
|
|
201
|
+
const config = readExternalDbConnectionConfig(values);
|
|
202
|
+
if (!config || (config.dialect !== 'mysql' && config.dialect !== 'mariadb')) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const mode = await readMysqlLowerCaseTableNamesMode(config);
|
|
207
|
+
if (mode === '1' && values.dbUnderscored !== true) {
|
|
208
|
+
return translateCli('validators.dbConnection.lowerCaseTableNamesRequiresUnderscored');
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
return formatDbConnectionError(config, error);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
156
216
|
export function clearExternalDbValidationCache() {
|
|
157
217
|
externalDbValidationCache.clear();
|
|
218
|
+
mysqlLowerCaseTableNamesCache.clear();
|
|
158
219
|
}
|