@nocobase/cli 2.1.0-beta.35 → 2.1.0-beta.36
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/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/env/add.js +63 -9
- package/dist/commands/env/auth.js +85 -11
- package/dist/commands/init.js +71 -13
- package/dist/commands/install.js +140 -11
- 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 +28 -5
- package/dist/lib/backup.js +171 -0
- package/dist/lib/bootstrap.js +23 -13
- package/dist/lib/env-config.js +6 -0
- package/dist/lib/run-npm.js +35 -9
- package/dist/lib/source-publish.js +20 -1
- package/dist/lib/source-registry.js +2 -2
- package/package.json +6 -3
|
@@ -24,6 +24,18 @@ const APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS = 5_000;
|
|
|
24
24
|
function trimValue(value) {
|
|
25
25
|
return String(value ?? '').trim();
|
|
26
26
|
}
|
|
27
|
+
function pushOptionalEnvArg(args, key, value) {
|
|
28
|
+
if (typeof value === 'string') {
|
|
29
|
+
if (!value) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
args.push('-e', `${key}=${value}`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (typeof value === 'boolean') {
|
|
36
|
+
args.push('-e', `${key}=${String(value)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
27
39
|
function formatAppUrl(port) {
|
|
28
40
|
const value = trimValue(port);
|
|
29
41
|
return value ? `http://127.0.0.1:${value}` : undefined;
|
|
@@ -262,10 +274,13 @@ export default class AppUpgrade extends Command {
|
|
|
262
274
|
persistDownloadVersion: requestedVersion || undefined,
|
|
263
275
|
};
|
|
264
276
|
}
|
|
265
|
-
static buildLocalDownloadArgv(runtime, downloadVersion) {
|
|
277
|
+
static buildLocalDownloadArgv(runtime, downloadVersion, options) {
|
|
266
278
|
const argv = ['-y', '--no-intro', '--source', runtime.source, '--replace'];
|
|
267
279
|
const gitUrl = readEnvValue(runtime.env, 'gitUrl');
|
|
268
280
|
const npmRegistry = readEnvValue(runtime.env, 'npmRegistry');
|
|
281
|
+
if (options?.verbose) {
|
|
282
|
+
argv.push('--verbose');
|
|
283
|
+
}
|
|
269
284
|
argv.push('--version', downloadVersion, '--output-dir', runtime.projectRoot);
|
|
270
285
|
if (gitUrl) {
|
|
271
286
|
argv.push('--git-url', gitUrl);
|
|
@@ -284,18 +299,12 @@ export default class AppUpgrade extends Command {
|
|
|
284
299
|
}
|
|
285
300
|
return argv;
|
|
286
301
|
}
|
|
287
|
-
static buildDockerDownloadArgv(runtime, plan) {
|
|
288
|
-
const argv = [
|
|
289
|
-
|
|
290
|
-
'--
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
'--replace',
|
|
294
|
-
'--docker-registry',
|
|
295
|
-
plan.dockerRegistry,
|
|
296
|
-
'--version',
|
|
297
|
-
plan.downloadVersion,
|
|
298
|
-
];
|
|
302
|
+
static buildDockerDownloadArgv(runtime, plan, options) {
|
|
303
|
+
const argv = ['-y', '--no-intro'];
|
|
304
|
+
if (options?.verbose) {
|
|
305
|
+
argv.push('--verbose');
|
|
306
|
+
}
|
|
307
|
+
argv.push('--source', 'docker', '--replace', '--docker-registry', plan.dockerRegistry, '--version', plan.downloadVersion);
|
|
299
308
|
const dockerPlatform = normalizeDockerPlatform(runtime.env.config.dockerPlatform);
|
|
300
309
|
if (dockerPlatform) {
|
|
301
310
|
argv.push('--docker-platform', dockerPlatform);
|
|
@@ -329,6 +338,11 @@ export default class AppUpgrade extends Command {
|
|
|
329
338
|
const dbDatabase = readEnvValue(runtime.env, 'dbDatabase');
|
|
330
339
|
const dbUser = readEnvValue(runtime.env, 'dbUser');
|
|
331
340
|
const dbPassword = readEnvValue(runtime.env, 'dbPassword');
|
|
341
|
+
const dbSchema = readEnvValue(runtime.env, 'dbSchema');
|
|
342
|
+
const dbTablePrefix = readEnvValue(runtime.env, 'dbTablePrefix');
|
|
343
|
+
const dbUnderscored = typeof runtime.env.config.dbUnderscored === 'boolean'
|
|
344
|
+
? runtime.env.config.dbUnderscored
|
|
345
|
+
: undefined;
|
|
332
346
|
const networkName = trimValue(runtime.dockerNetworkName || runtime.workspaceName);
|
|
333
347
|
const missing = [];
|
|
334
348
|
if (!networkName) {
|
|
@@ -384,7 +398,11 @@ export default class AppUpgrade extends Command {
|
|
|
384
398
|
if (envFile) {
|
|
385
399
|
args.push('--env-file', envFile);
|
|
386
400
|
}
|
|
387
|
-
args.push('-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}
|
|
401
|
+
args.push('-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}`);
|
|
402
|
+
pushOptionalEnvArg(args, 'DB_SCHEMA', dbSchema || undefined);
|
|
403
|
+
pushOptionalEnvArg(args, 'DB_TABLE_PREFIX', dbTablePrefix || undefined);
|
|
404
|
+
pushOptionalEnvArg(args, 'DB_UNDERSCORED', dbUnderscored);
|
|
405
|
+
args.push(imageRef);
|
|
388
406
|
return {
|
|
389
407
|
containerName: runtime.containerName,
|
|
390
408
|
networkName,
|
|
@@ -422,7 +440,9 @@ export default class AppUpgrade extends Command {
|
|
|
422
440
|
if (!flags['skip-code-update'] && (runtime.source === 'npm' || runtime.source === 'git')) {
|
|
423
441
|
startTask(`Refreshing NocoBase files for "${runtime.envName}" from the saved ${runtime.source} source...`);
|
|
424
442
|
try {
|
|
425
|
-
await runCommand('source:download', AppUpgrade.buildLocalDownloadArgv(runtime, downloadVersion
|
|
443
|
+
await runCommand('source:download', AppUpgrade.buildLocalDownloadArgv(runtime, downloadVersion, {
|
|
444
|
+
verbose: flags.verbose,
|
|
445
|
+
}));
|
|
426
446
|
succeedTask(`NocoBase files are up to date for "${runtime.envName}".`);
|
|
427
447
|
}
|
|
428
448
|
catch (error) {
|
|
@@ -468,7 +488,9 @@ export default class AppUpgrade extends Command {
|
|
|
468
488
|
const plan = await AppUpgrade.buildDockerUpgradePlan(runtime, downloadVersion);
|
|
469
489
|
startTask(`Refreshing the Docker image for "${runtime.envName}"...`);
|
|
470
490
|
try {
|
|
471
|
-
await runCommand('source:download', AppUpgrade.buildDockerDownloadArgv(runtime, plan
|
|
491
|
+
await runCommand('source:download', AppUpgrade.buildDockerDownloadArgv(runtime, plan, {
|
|
492
|
+
verbose: flags.verbose,
|
|
493
|
+
}));
|
|
472
494
|
succeedTask(`Docker image is ready for "${runtime.envName}".`);
|
|
473
495
|
}
|
|
474
496
|
catch (error) {
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
import { Command, Flags } from '@oclif/core';
|
|
10
|
+
import { BACKUP_CREATE_TIMEOUT_MS, BACKUP_POLL_INTERVAL_MS, BACKUP_RUNTIME_COMMANDS, buildBackupEnvArgv, ensureBackupRuntimeCommands, resolveBackupCreateOutputPath, resolveBackupTargetEnv, runBackupCliJsonCommand, sleep, } from '../../lib/backup.js';
|
|
11
|
+
import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
|
|
12
|
+
import { announceTargetEnv, failTask, startTask, succeedTask, updateTask } from '../../lib/ui.js';
|
|
13
|
+
function formatBackupCreateTimeoutError(envName, name) {
|
|
14
|
+
return [
|
|
15
|
+
`Backup "${name}" did not finish in time for "${envName}".`,
|
|
16
|
+
`Waited ${Math.floor(BACKUP_CREATE_TIMEOUT_MS / 1000)}s but it still reports \`inProgress: true\`.`,
|
|
17
|
+
].join(' ');
|
|
18
|
+
}
|
|
19
|
+
function readBackupCreateResult(response) {
|
|
20
|
+
const name = String(response.data?.name ?? '').trim();
|
|
21
|
+
if (!name) {
|
|
22
|
+
throw new Error('Backup creation did not return a backup name.');
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
name,
|
|
26
|
+
inProgress: Boolean(response.data?.inProgress),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function readBackupInProgress(response, name) {
|
|
30
|
+
const status = response.data?.[name];
|
|
31
|
+
if (!status || typeof status !== 'object') {
|
|
32
|
+
throw new Error(`Backup status did not include "${name}".`);
|
|
33
|
+
}
|
|
34
|
+
return Boolean(status.inProgress);
|
|
35
|
+
}
|
|
36
|
+
function readDownloadOutput(response) {
|
|
37
|
+
const output = String(response.data?.output ?? '').trim();
|
|
38
|
+
return output || undefined;
|
|
39
|
+
}
|
|
40
|
+
export default class BackupCreate extends Command {
|
|
41
|
+
static summary = 'Create a backup through the selected env and download it locally';
|
|
42
|
+
static examples = [
|
|
43
|
+
'<%= config.bin %> <%= command.id %>',
|
|
44
|
+
'<%= config.bin %> <%= command.id %> --output ./fixtures/base.nbdump',
|
|
45
|
+
'<%= config.bin %> <%= command.id %> --env e2e --output ./fixtures',
|
|
46
|
+
];
|
|
47
|
+
static flags = {
|
|
48
|
+
env: Flags.string({
|
|
49
|
+
char: 'e',
|
|
50
|
+
description: 'CLI env name to back up. Defaults to the current env when omitted',
|
|
51
|
+
}),
|
|
52
|
+
yes: Flags.boolean({
|
|
53
|
+
char: 'y',
|
|
54
|
+
description: 'Confirm using --env when it targets a different env than the current env',
|
|
55
|
+
default: false,
|
|
56
|
+
}),
|
|
57
|
+
output: Flags.string({
|
|
58
|
+
char: 'o',
|
|
59
|
+
description: 'Download path. When omitted, save to the current directory using the remote backup filename',
|
|
60
|
+
}),
|
|
61
|
+
'json-output': Flags.boolean({
|
|
62
|
+
char: 'j',
|
|
63
|
+
description: 'Print the final backup result as JSON',
|
|
64
|
+
default: false,
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
async run() {
|
|
68
|
+
const { flags } = await this.parse(BackupCreate);
|
|
69
|
+
const requestedEnv = flags.env?.trim() || undefined;
|
|
70
|
+
const explicitEnvSelection = Boolean(requestedEnv && hasExplicitEnvSelection(this.argv));
|
|
71
|
+
const jsonOutput = Boolean(flags['json-output']);
|
|
72
|
+
if (explicitEnvSelection) {
|
|
73
|
+
const confirmed = await ensureCrossEnvConfirmed({
|
|
74
|
+
command: this,
|
|
75
|
+
requestedEnv,
|
|
76
|
+
yes: flags.yes,
|
|
77
|
+
});
|
|
78
|
+
if (!confirmed) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const { envName, env } = await resolveBackupTargetEnv(requestedEnv);
|
|
83
|
+
const envArgv = buildBackupEnvArgv({
|
|
84
|
+
requestedEnv,
|
|
85
|
+
explicitEnvSelection,
|
|
86
|
+
yes: flags.yes,
|
|
87
|
+
});
|
|
88
|
+
if (!jsonOutput) {
|
|
89
|
+
announceTargetEnv(envName);
|
|
90
|
+
}
|
|
91
|
+
await ensureBackupRuntimeCommands({
|
|
92
|
+
envName,
|
|
93
|
+
env,
|
|
94
|
+
commandIds: [
|
|
95
|
+
BACKUP_RUNTIME_COMMANDS.create,
|
|
96
|
+
BACKUP_RUNTIME_COMMANDS.status,
|
|
97
|
+
BACKUP_RUNTIME_COMMANDS.download,
|
|
98
|
+
],
|
|
99
|
+
quiet: jsonOutput,
|
|
100
|
+
});
|
|
101
|
+
try {
|
|
102
|
+
if (!jsonOutput) {
|
|
103
|
+
startTask(`Creating backup for "${envName}"...`);
|
|
104
|
+
}
|
|
105
|
+
const createResponse = await runBackupCliJsonCommand(['api', 'backup', 'create', ...envArgv], { errorName: 'nb api backup create' });
|
|
106
|
+
const { name, inProgress } = readBackupCreateResult(createResponse);
|
|
107
|
+
const outputPath = await resolveBackupCreateOutputPath(flags.output, name);
|
|
108
|
+
const startedAt = Date.now();
|
|
109
|
+
let pending = inProgress;
|
|
110
|
+
while (pending) {
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
const elapsedMs = now - startedAt;
|
|
113
|
+
if (elapsedMs >= BACKUP_CREATE_TIMEOUT_MS) {
|
|
114
|
+
throw new Error(formatBackupCreateTimeoutError(envName, name));
|
|
115
|
+
}
|
|
116
|
+
const elapsedSeconds = Math.max(1, Math.floor(elapsedMs / 1000));
|
|
117
|
+
if (!jsonOutput) {
|
|
118
|
+
updateTask(`Waiting for backup "${name}" to finish for "${envName}"... (${elapsedSeconds}s elapsed)`);
|
|
119
|
+
}
|
|
120
|
+
await sleep(BACKUP_POLL_INTERVAL_MS);
|
|
121
|
+
const statusResponse = await runBackupCliJsonCommand(['api', 'backup', 'status', '--name', name, ...envArgv], { errorName: 'nb api backup status' });
|
|
122
|
+
pending = readBackupInProgress(statusResponse, name);
|
|
123
|
+
}
|
|
124
|
+
if (!jsonOutput) {
|
|
125
|
+
updateTask(`Downloading backup "${name}" for "${envName}"...`);
|
|
126
|
+
}
|
|
127
|
+
const downloadResponse = await runBackupCliJsonCommand(['api', 'backup', 'download', '--name', name, '--output', outputPath, ...envArgv], { errorName: 'nb api backup download' });
|
|
128
|
+
const savedPath = readDownloadOutput(downloadResponse) ?? outputPath;
|
|
129
|
+
const result = {
|
|
130
|
+
env: envName,
|
|
131
|
+
name,
|
|
132
|
+
output: savedPath,
|
|
133
|
+
};
|
|
134
|
+
if (jsonOutput) {
|
|
135
|
+
this.log(JSON.stringify(result, null, 2));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
succeedTask(`Backup saved to ${savedPath}`);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (!jsonOutput) {
|
|
142
|
+
failTask(`Failed to create backup for "${envName}".`);
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -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 { Command, loadHelpClass } from '@oclif/core';
|
|
10
|
+
export default class Backup extends Command {
|
|
11
|
+
static summary = 'Create or restore NocoBase backups';
|
|
12
|
+
async run() {
|
|
13
|
+
await this.parse(Backup);
|
|
14
|
+
const Help = await loadHelpClass(this.config);
|
|
15
|
+
await new Help(this.config, this.config.pjson.oclif.helpOptions ?? this.config.pjson.helpOptions).showHelp([
|
|
16
|
+
this.id ?? 'backup',
|
|
17
|
+
...this.argv,
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
import { Command, Flags } from '@oclif/core';
|
|
10
|
+
import { BACKUP_RUNTIME_COMMANDS, buildBackupEnvArgv, ensureBackupRuntimeCommands, resolveBackupRestoreFilePath, resolveBackupTargetEnv, resolveBackupWaitApiBaseUrl, runBackupCliCommand, } from '../../lib/backup.js';
|
|
11
|
+
import { waitForAppReady } from '../../lib/app-health.js';
|
|
12
|
+
import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
|
|
13
|
+
import { confirm } from "../../lib/inquirer.js";
|
|
14
|
+
import { announceTargetEnv, failTask, isInteractiveTerminal, startTask, stopTask, succeedTask } from '../../lib/ui.js';
|
|
15
|
+
async function confirmBackupRestore(envName, filePath, force) {
|
|
16
|
+
if (force) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
if (!isInteractiveTerminal()) {
|
|
20
|
+
throw new Error(`\`nb backup restore\` needs confirmation. Re-run with \`--force\` to restore ${filePath} into "${envName}" in non-interactive mode.`);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return await confirm({
|
|
24
|
+
message: `Restore backup "${filePath}" into "${envName}"? This will overwrite application data.`,
|
|
25
|
+
default: false,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export default class BackupRestore extends Command {
|
|
33
|
+
static summary = 'Restore a backup file into the selected env';
|
|
34
|
+
static examples = [
|
|
35
|
+
'<%= config.bin %> <%= command.id %> --file ./fixtures/base.nbdump --force',
|
|
36
|
+
'<%= config.bin %> <%= command.id %> --env e2e --file ./fixtures/base.nbdump --yes --force',
|
|
37
|
+
];
|
|
38
|
+
static flags = {
|
|
39
|
+
env: Flags.string({
|
|
40
|
+
char: 'e',
|
|
41
|
+
description: 'CLI env name to restore into. Defaults to the current env when omitted',
|
|
42
|
+
}),
|
|
43
|
+
yes: Flags.boolean({
|
|
44
|
+
char: 'y',
|
|
45
|
+
description: 'Confirm using --env when it targets a different env than the current env',
|
|
46
|
+
default: false,
|
|
47
|
+
}),
|
|
48
|
+
file: Flags.string({
|
|
49
|
+
char: 'f',
|
|
50
|
+
description: 'Local backup file to upload and restore',
|
|
51
|
+
required: true,
|
|
52
|
+
}),
|
|
53
|
+
force: Flags.boolean({
|
|
54
|
+
description: 'Confirm overwriting application data during restore',
|
|
55
|
+
default: false,
|
|
56
|
+
}),
|
|
57
|
+
};
|
|
58
|
+
async run() {
|
|
59
|
+
const { flags } = await this.parse(BackupRestore);
|
|
60
|
+
const requestedEnv = flags.env?.trim() || undefined;
|
|
61
|
+
const explicitEnvSelection = Boolean(requestedEnv && hasExplicitEnvSelection(this.argv));
|
|
62
|
+
if (explicitEnvSelection) {
|
|
63
|
+
const confirmed = await ensureCrossEnvConfirmed({
|
|
64
|
+
command: this,
|
|
65
|
+
requestedEnv,
|
|
66
|
+
yes: flags.yes,
|
|
67
|
+
});
|
|
68
|
+
if (!confirmed) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const filePath = await resolveBackupRestoreFilePath(flags.file);
|
|
73
|
+
const { envName, env } = await resolveBackupTargetEnv(requestedEnv);
|
|
74
|
+
const restoreConfirmed = await confirmBackupRestore(envName, filePath, flags.force);
|
|
75
|
+
if (!restoreConfirmed) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const envArgv = buildBackupEnvArgv({
|
|
79
|
+
requestedEnv,
|
|
80
|
+
explicitEnvSelection,
|
|
81
|
+
yes: flags.yes,
|
|
82
|
+
});
|
|
83
|
+
announceTargetEnv(envName);
|
|
84
|
+
await ensureBackupRuntimeCommands({
|
|
85
|
+
envName,
|
|
86
|
+
env,
|
|
87
|
+
commandIds: [BACKUP_RUNTIME_COMMANDS.restoreUpload],
|
|
88
|
+
});
|
|
89
|
+
startTask(`Restoring backup for "${envName}" from ${filePath}...`);
|
|
90
|
+
try {
|
|
91
|
+
await runBackupCliCommand(['api', 'backup', 'restore-upload', '--file', filePath, '--force', ...envArgv], { errorName: 'nb api backup restore-upload' });
|
|
92
|
+
stopTask();
|
|
93
|
+
await waitForAppReady({
|
|
94
|
+
envName,
|
|
95
|
+
apiBaseUrl: resolveBackupWaitApiBaseUrl(env),
|
|
96
|
+
});
|
|
97
|
+
succeedTask(`Backup restored for "${envName}" from ${filePath}`);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
stopTask();
|
|
101
|
+
failTask(`Failed to restore backup for "${envName}".`);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
package/dist/commands/env/add.js
CHANGED
|
@@ -13,7 +13,7 @@ import { buildStoredEnvConfig, } from '../../lib/env-config.js';
|
|
|
13
13
|
import { runPromptCatalog, } from '../../lib/prompt-catalog.js';
|
|
14
14
|
import { applyCliLocale, CLI_LOCALE_FLAG_DESCRIPTION, CLI_LOCALE_FLAG_OPTIONS, localeText, } from '../../lib/cli-locale.js';
|
|
15
15
|
import { validateApiBaseUrl } from '../../lib/prompt-validators.js';
|
|
16
|
-
import { printStage, printSuccess, printVerbose, setVerboseMode } from '../../lib/ui.js';
|
|
16
|
+
import { printInfo, printStage, printSuccess, printVerbose, setVerboseMode } from '../../lib/ui.js';
|
|
17
17
|
const ENV_RUNTIME_FLAG_MAP = {
|
|
18
18
|
source: 'source',
|
|
19
19
|
'download-version': 'downloadVersion',
|
|
@@ -33,6 +33,8 @@ const ENV_RUNTIME_FLAG_MAP = {
|
|
|
33
33
|
'db-database': 'dbDatabase',
|
|
34
34
|
'db-user': 'dbUser',
|
|
35
35
|
'db-password': 'dbPassword',
|
|
36
|
+
'db-schema': 'dbSchema',
|
|
37
|
+
'db-table-prefix': 'dbTablePrefix',
|
|
36
38
|
'root-username': 'rootUsername',
|
|
37
39
|
'root-email': 'rootEmail',
|
|
38
40
|
'root-password': 'rootPassword',
|
|
@@ -43,8 +45,26 @@ const ENV_BOOLEAN_RUNTIME_FLAG_MAP = {
|
|
|
43
45
|
'dev-dependencies': 'devDependencies',
|
|
44
46
|
build: 'build',
|
|
45
47
|
'build-dts': 'buildDts',
|
|
48
|
+
'db-underscored': 'dbUnderscored',
|
|
46
49
|
};
|
|
47
50
|
const envAddText = (key, values) => localeText(`commands.envAdd.${key}`, values);
|
|
51
|
+
const envAddAccessTokenPrompt = {
|
|
52
|
+
type: 'text',
|
|
53
|
+
message: envAddText('prompts.accessToken.message'),
|
|
54
|
+
required: true,
|
|
55
|
+
hidden: (values) => values.authType !== 'token' || values.skipAuth === true,
|
|
56
|
+
};
|
|
57
|
+
function formatDeferredAuthMessage(envName, authType) {
|
|
58
|
+
const normalizedAuthType = String(authType ?? '').trim();
|
|
59
|
+
const nextStep = `Authentication was skipped for env "${envName}". Run \`nb env auth ${envName}\` to finish setup.`;
|
|
60
|
+
if (normalizedAuthType === 'token') {
|
|
61
|
+
return `${nextStep} You will be prompted for an access token.`;
|
|
62
|
+
}
|
|
63
|
+
if (normalizedAuthType === 'oauth') {
|
|
64
|
+
return `${nextStep} A browser sign-in flow will be started.`;
|
|
65
|
+
}
|
|
66
|
+
return nextStep;
|
|
67
|
+
}
|
|
48
68
|
export default class EnvAdd extends Command {
|
|
49
69
|
static summary = 'Save a named NocoBase API endpoint (token or OAuth), then switch the CLI to use it';
|
|
50
70
|
static examples = [
|
|
@@ -97,6 +117,10 @@ export default class EnvAdd extends Command {
|
|
|
97
117
|
aliases: ['token'],
|
|
98
118
|
description: 'API key or access token when using --auth-type token (prompted in a TTY when omitted)',
|
|
99
119
|
}),
|
|
120
|
+
'skip-auth': Flags.boolean({
|
|
121
|
+
description: 'Save the env now and finish authentication later with `nb env auth`',
|
|
122
|
+
default: false,
|
|
123
|
+
}),
|
|
100
124
|
source: Flags.string({
|
|
101
125
|
hidden: true,
|
|
102
126
|
description: 'Application source saved with this env',
|
|
@@ -189,6 +213,19 @@ export default class EnvAdd extends Command {
|
|
|
189
213
|
hidden: true,
|
|
190
214
|
description: 'Database password saved with this env',
|
|
191
215
|
}),
|
|
216
|
+
'db-schema': Flags.string({
|
|
217
|
+
hidden: true,
|
|
218
|
+
description: 'Database schema saved with this env',
|
|
219
|
+
}),
|
|
220
|
+
'db-table-prefix': Flags.string({
|
|
221
|
+
hidden: true,
|
|
222
|
+
description: 'Database table prefix saved with this env',
|
|
223
|
+
}),
|
|
224
|
+
'db-underscored': Flags.boolean({
|
|
225
|
+
allowNo: true,
|
|
226
|
+
hidden: true,
|
|
227
|
+
description: 'Whether this env uses underscored database naming',
|
|
228
|
+
}),
|
|
192
229
|
'root-username': Flags.string({
|
|
193
230
|
hidden: true,
|
|
194
231
|
description: 'Initial root username saved with this env',
|
|
@@ -234,13 +271,7 @@ export default class EnvAdd extends Command {
|
|
|
234
271
|
initialValue: 'oauth',
|
|
235
272
|
required: true,
|
|
236
273
|
},
|
|
237
|
-
accessToken:
|
|
238
|
-
type: 'text',
|
|
239
|
-
message: envAddText('prompts.accessToken.message'),
|
|
240
|
-
placeholder: envAddText('prompts.accessToken.placeholder'),
|
|
241
|
-
required: true,
|
|
242
|
-
hidden: (values) => values.authType !== 'token',
|
|
243
|
-
},
|
|
274
|
+
accessToken: envAddAccessTokenPrompt,
|
|
244
275
|
};
|
|
245
276
|
buildPromptValues(nameArg, flags) {
|
|
246
277
|
const values = {};
|
|
@@ -255,6 +286,9 @@ export default class EnvAdd extends Command {
|
|
|
255
286
|
if (flags['auth-type']) {
|
|
256
287
|
values.authType = flags['auth-type'];
|
|
257
288
|
}
|
|
289
|
+
if (flags['skip-auth']) {
|
|
290
|
+
values.skipAuth = true;
|
|
291
|
+
}
|
|
258
292
|
const token = flags['access-token'] ?? flags.token;
|
|
259
293
|
if (typeof token === 'string' && token !== '') {
|
|
260
294
|
values.accessToken = token;
|
|
@@ -269,6 +303,18 @@ export default class EnvAdd extends Command {
|
|
|
269
303
|
}
|
|
270
304
|
return initialValues;
|
|
271
305
|
}
|
|
306
|
+
buildPromptCatalog(flags) {
|
|
307
|
+
if (!flags['skip-auth']) {
|
|
308
|
+
return EnvAdd.prompts;
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
...EnvAdd.prompts,
|
|
312
|
+
accessToken: {
|
|
313
|
+
...envAddAccessTokenPrompt,
|
|
314
|
+
hidden: () => true,
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
272
318
|
buildEnvConfig(results, flags) {
|
|
273
319
|
const envConfigInput = {
|
|
274
320
|
apiBaseUrl: results.apiBaseUrl,
|
|
@@ -288,12 +334,15 @@ export default class EnvAdd extends Command {
|
|
|
288
334
|
async run() {
|
|
289
335
|
const { args, flags } = await this.parse(EnvAdd);
|
|
290
336
|
const parsedFlags = flags;
|
|
337
|
+
if (parsedFlags['skip-auth'] && (parsedFlags['access-token'] !== undefined || parsedFlags.token !== undefined)) {
|
|
338
|
+
this.error('--skip-auth cannot be used with --access-token or --token.');
|
|
339
|
+
}
|
|
291
340
|
applyCliLocale(parsedFlags.locale);
|
|
292
341
|
setVerboseMode(parsedFlags.verbose);
|
|
293
342
|
if (!parsedFlags['no-intro']) {
|
|
294
343
|
printStage('Connect to NocoBase');
|
|
295
344
|
}
|
|
296
|
-
const results = await runPromptCatalog(
|
|
345
|
+
const results = await runPromptCatalog(this.buildPromptCatalog(parsedFlags), {
|
|
297
346
|
values: this.buildPromptValues(args.name, parsedFlags),
|
|
298
347
|
initialValues: this.buildPromptInitialValues(parsedFlags),
|
|
299
348
|
command: this,
|
|
@@ -303,6 +352,11 @@ export default class EnvAdd extends Command {
|
|
|
303
352
|
printVerbose(`Saving env "${envName}" globally.`);
|
|
304
353
|
await upsertEnv(envName, envConfig, { scope: resolveDefaultConfigScope() });
|
|
305
354
|
await setCurrentEnv(envName, { scope: resolveDefaultConfigScope() });
|
|
355
|
+
if (parsedFlags['skip-auth']) {
|
|
356
|
+
printSuccess(`✔ Env "${envName}" was saved.`);
|
|
357
|
+
printInfo(formatDeferredAuthMessage(envName, results.authType));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
306
360
|
if (results.authType === 'oauth') {
|
|
307
361
|
await this.config.runCommand('env:auth', [envName]);
|
|
308
362
|
}
|
|
@@ -7,15 +7,31 @@
|
|
|
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 } from '../../lib/auth-store.js';
|
|
10
|
+
import { getCurrentEnvName, getEnv, resolveConfiguredAuthType, updateEnvConnection, } from '../../lib/auth-store.js';
|
|
11
11
|
import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
|
|
12
12
|
import { authenticateEnvWithOauth } from '../../lib/env-auth.js';
|
|
13
|
-
import {
|
|
13
|
+
import { runPromptCatalog } from '../../lib/prompt-catalog.js';
|
|
14
|
+
import { failTask, printStage, startTask, stopTask, succeedTask } from '../../lib/ui.js';
|
|
15
|
+
import EnvAdd from "./add.js";
|
|
16
|
+
const envAuthPrompts = {
|
|
17
|
+
authType: EnvAdd.prompts.authType,
|
|
18
|
+
accessToken: EnvAdd.prompts.accessToken,
|
|
19
|
+
};
|
|
20
|
+
function resolveExplicitAuthType(value) {
|
|
21
|
+
return value === 'token' || value === 'oauth' ? value : undefined;
|
|
22
|
+
}
|
|
23
|
+
function formatMissingEnvMessage(envName) {
|
|
24
|
+
return [
|
|
25
|
+
`Env "${envName}" is not configured.`,
|
|
26
|
+
`Run \`nb env add ${envName} --api-base-url <url>\` first.`,
|
|
27
|
+
].join('\n');
|
|
28
|
+
}
|
|
14
29
|
export default class EnvAuth extends Command {
|
|
15
|
-
static summary = '
|
|
30
|
+
static summary = 'Authenticate a saved NocoBase environment with a token or OAuth';
|
|
16
31
|
static examples = [
|
|
17
32
|
'<%= config.bin %> <%= command.id %>',
|
|
18
33
|
'<%= config.bin %> <%= command.id %> prod',
|
|
34
|
+
'<%= config.bin %> <%= command.id %> prod --auth-type token --access-token <api-key>',
|
|
19
35
|
];
|
|
20
36
|
static args = {
|
|
21
37
|
name: Args.string({
|
|
@@ -30,6 +46,15 @@ export default class EnvAuth extends Command {
|
|
|
30
46
|
deprecated: true,
|
|
31
47
|
description: 'Environment name (same as the optional positional argument; for compatibility with -e/--env on other commands)',
|
|
32
48
|
}),
|
|
49
|
+
'auth-type': Flags.string({
|
|
50
|
+
char: 'a',
|
|
51
|
+
description: 'Authentication: token (API key) or oauth (browser login)',
|
|
52
|
+
options: ['token', 'oauth'],
|
|
53
|
+
}),
|
|
54
|
+
'access-token': Flags.string({
|
|
55
|
+
char: 't',
|
|
56
|
+
description: 'API key or access token when using token authentication',
|
|
57
|
+
}),
|
|
33
58
|
};
|
|
34
59
|
async run() {
|
|
35
60
|
const { args, flags } = await this.parse(EnvAuth);
|
|
@@ -38,18 +63,67 @@ export default class EnvAuth extends Command {
|
|
|
38
63
|
if (nameArg && nameFlag && nameArg !== nameFlag) {
|
|
39
64
|
this.error(`Environment name was provided both as the argument ("${nameArg}") and as --env ("${nameFlag}"). Please use only one.`);
|
|
40
65
|
}
|
|
66
|
+
if (flags['auth-type'] === 'oauth' && flags['access-token'] !== undefined) {
|
|
67
|
+
this.error('--access-token cannot be used with --auth-type oauth.');
|
|
68
|
+
}
|
|
41
69
|
const envName = nameArg || nameFlag || (await getCurrentEnvName({ scope: resolveDefaultConfigScope() }));
|
|
42
|
-
|
|
43
|
-
|
|
70
|
+
const env = await getEnv(envName, { scope: resolveDefaultConfigScope() });
|
|
71
|
+
if (!env) {
|
|
72
|
+
this.error(formatMissingEnvMessage(envName));
|
|
73
|
+
}
|
|
74
|
+
const tokenFromFlags = flags['access-token'];
|
|
75
|
+
const tokenFlagProvided = tokenFromFlags !== undefined;
|
|
76
|
+
const tokenProvided = typeof tokenFromFlags === 'string' && tokenFromFlags !== '';
|
|
77
|
+
if (tokenFlagProvided && !tokenProvided) {
|
|
78
|
+
this.error('--access-token cannot be empty.');
|
|
79
|
+
}
|
|
80
|
+
const explicitAuthType = resolveExplicitAuthType(flags['auth-type']);
|
|
81
|
+
const savedAuthType = resolveConfiguredAuthType(env.config);
|
|
82
|
+
const resolvedAuthType = explicitAuthType ?? (tokenProvided ? 'token' : savedAuthType);
|
|
83
|
+
const prompted = (resolvedAuthType === 'oauth'
|
|
84
|
+
? { authType: 'oauth' }
|
|
85
|
+
: resolvedAuthType === 'token' && tokenProvided
|
|
86
|
+
? { authType: 'token', accessToken: tokenFromFlags }
|
|
87
|
+
: await runPromptCatalog(envAuthPrompts, {
|
|
88
|
+
values: {
|
|
89
|
+
...(resolvedAuthType ? { authType: resolvedAuthType } : {}),
|
|
90
|
+
},
|
|
91
|
+
command: this,
|
|
92
|
+
})) ?? {};
|
|
93
|
+
const authType = resolveExplicitAuthType(prompted.authType ?? resolvedAuthType);
|
|
94
|
+
if (!authType) {
|
|
95
|
+
this.error('Choose an authentication type before continuing.');
|
|
96
|
+
}
|
|
97
|
+
printStage('Authenticating');
|
|
44
98
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
99
|
+
if (authType === 'token') {
|
|
100
|
+
const accessToken = String(prompted.accessToken ?? tokenFromFlags ?? '');
|
|
101
|
+
if (accessToken.trim() === '') {
|
|
102
|
+
this.error('--access-token cannot be empty.');
|
|
103
|
+
}
|
|
104
|
+
startTask(`Saving access token for "${envName}"...`);
|
|
105
|
+
await updateEnvConnection(envName, {
|
|
106
|
+
authType: 'token',
|
|
107
|
+
accessToken,
|
|
108
|
+
}, { scope: resolveDefaultConfigScope() });
|
|
109
|
+
stopTask();
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
startTask(`Starting browser sign-in for "${envName}"...`);
|
|
113
|
+
await updateEnvConnection(envName, {
|
|
114
|
+
authType: 'oauth',
|
|
115
|
+
}, { scope: resolveDefaultConfigScope() });
|
|
116
|
+
await authenticateEnvWithOauth({
|
|
117
|
+
envName,
|
|
118
|
+
scope: resolveDefaultConfigScope(),
|
|
119
|
+
});
|
|
120
|
+
stopTask();
|
|
121
|
+
}
|
|
122
|
+
await this.config.runCommand('env:update', [envName]);
|
|
123
|
+
succeedTask(`✔ Authenticated "${envName}".`);
|
|
50
124
|
}
|
|
51
125
|
catch (error) {
|
|
52
|
-
failTask(`
|
|
126
|
+
failTask(`Authentication failed for "${envName}".`);
|
|
53
127
|
throw error;
|
|
54
128
|
}
|
|
55
129
|
}
|