@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
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
import { Command } from '@oclif/core';
|
|
10
|
+
import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runDockerNocoBaseCommand, runLocalNocoBaseCommand, } from '../lib/app-runtime.js';
|
|
11
|
+
import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../lib/env-guard.js';
|
|
12
|
+
import { announceTargetEnv } from '../lib/ui.js';
|
|
13
|
+
const SILENT_LIKE_PASSTHROUGH_FLAGS = new Set(['--help', '-h', '--silent']);
|
|
14
|
+
const SILENT_RUNTIME_ENV_VARS = {
|
|
15
|
+
LOGGER_SILENT: 'true',
|
|
16
|
+
NODE_NO_WARNINGS: '1',
|
|
17
|
+
};
|
|
18
|
+
const SILENT_STDERR_FILTERS = [
|
|
19
|
+
/^\(node:\d+\) \[DEP0040\] DeprecationWarning: The `punycode` module is deprecated\..*$/,
|
|
20
|
+
/^\(Use `node --trace-deprecation .*$/,
|
|
21
|
+
/^About to overwrite ArrayBuffer\.prototype properties /,
|
|
22
|
+
];
|
|
23
|
+
function parseBridgeArgv(argv) {
|
|
24
|
+
let requestedEnv;
|
|
25
|
+
let yes = false;
|
|
26
|
+
const passthroughArgs = [];
|
|
27
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
28
|
+
const token = argv[index];
|
|
29
|
+
if (token === '--') {
|
|
30
|
+
passthroughArgs.push(...argv.slice(index + 1));
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
if (token === '--env') {
|
|
34
|
+
const value = argv[index + 1];
|
|
35
|
+
if (!value || value === '--') {
|
|
36
|
+
throw new Error('Missing value for `--env`.');
|
|
37
|
+
}
|
|
38
|
+
requestedEnv = value.trim() || undefined;
|
|
39
|
+
index += 1;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (token.startsWith('--env=')) {
|
|
43
|
+
requestedEnv = token.slice('--env='.length).trim() || undefined;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (token === '-e') {
|
|
47
|
+
const value = argv[index + 1];
|
|
48
|
+
if (!value || value === '--') {
|
|
49
|
+
throw new Error('Missing value for `-e`.');
|
|
50
|
+
}
|
|
51
|
+
requestedEnv = value.trim() || undefined;
|
|
52
|
+
index += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (token.startsWith('-e') && token.length > 2) {
|
|
56
|
+
requestedEnv = token.slice(2).trim() || undefined;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (token === '--yes') {
|
|
60
|
+
yes = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
passthroughArgs.push(...argv.slice(index));
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
requestedEnv,
|
|
68
|
+
yes,
|
|
69
|
+
passthroughArgs,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function formatHttpEnvError(envName) {
|
|
73
|
+
return [
|
|
74
|
+
`Can't run \`nb v1\` for "${envName}" yet.`,
|
|
75
|
+
'This env only has an API connection, so the v1 bridge is not available here.',
|
|
76
|
+
'Use a local or Docker env instead.',
|
|
77
|
+
].join('\n');
|
|
78
|
+
}
|
|
79
|
+
function formatSshEnvError(envName) {
|
|
80
|
+
return [
|
|
81
|
+
`Can't run \`nb v1\` for "${envName}" yet.`,
|
|
82
|
+
'SSH env support is reserved but not implemented yet.',
|
|
83
|
+
'Use a local or Docker env right now.',
|
|
84
|
+
].join('\n');
|
|
85
|
+
}
|
|
86
|
+
function hasSilentLikePassthrough(args) {
|
|
87
|
+
return args.some((arg) => SILENT_LIKE_PASSTHROUGH_FLAGS.has(arg));
|
|
88
|
+
}
|
|
89
|
+
function shouldFilterSilentStderrLine(line) {
|
|
90
|
+
const normalized = line.replace(/\r$/, '');
|
|
91
|
+
return SILENT_STDERR_FILTERS.some((pattern) => pattern.test(normalized));
|
|
92
|
+
}
|
|
93
|
+
function createSilentBridgeOptions() {
|
|
94
|
+
let pendingStderr = '';
|
|
95
|
+
const flushBufferedStderr = (force) => {
|
|
96
|
+
while (true) {
|
|
97
|
+
const newlineIndex = pendingStderr.indexOf('\n');
|
|
98
|
+
if (newlineIndex === -1) {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
const line = pendingStderr.slice(0, newlineIndex);
|
|
102
|
+
pendingStderr = pendingStderr.slice(newlineIndex + 1);
|
|
103
|
+
if (!shouldFilterSilentStderrLine(line)) {
|
|
104
|
+
process.stderr.write(`${line}\n`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (force && pendingStderr) {
|
|
108
|
+
if (!shouldFilterSilentStderrLine(pendingStderr)) {
|
|
109
|
+
process.stderr.write(pendingStderr);
|
|
110
|
+
}
|
|
111
|
+
pendingStderr = '';
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
commandOptions: {
|
|
116
|
+
stdio: 'pipe',
|
|
117
|
+
env: {
|
|
118
|
+
...SILENT_RUNTIME_ENV_VARS,
|
|
119
|
+
},
|
|
120
|
+
onStdout: (chunk) => {
|
|
121
|
+
process.stdout.write(chunk);
|
|
122
|
+
},
|
|
123
|
+
onStderr: (chunk) => {
|
|
124
|
+
pendingStderr += chunk;
|
|
125
|
+
flushBufferedStderr(false);
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
flush: () => {
|
|
129
|
+
flushBufferedStderr(true);
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export default class V1 extends Command {
|
|
134
|
+
static hidden = true;
|
|
135
|
+
static strict = false;
|
|
136
|
+
static summary = 'Forward commands to the selected env through the v1 bridge';
|
|
137
|
+
static description = 'Forward v1-compatible commands to the selected env. Defaults to the current env when `--env` is omitted. Local envs run `nocobase-v1`, and Docker envs run inside the saved app container. Bridge flags (`--env`, `--yes`) must appear before the forwarded command. Use `--` when the forwarded command needs the same flag names.';
|
|
138
|
+
static examples = [
|
|
139
|
+
'<%= config.bin %> <%= command.id %> build',
|
|
140
|
+
'<%= config.bin %> <%= command.id %> --env local pm list',
|
|
141
|
+
'<%= config.bin %> <%= command.id %> --env docker-local -- pm enable @nocobase/plugin-sample --yes',
|
|
142
|
+
];
|
|
143
|
+
async run() {
|
|
144
|
+
const originalArgv = [...this.argv];
|
|
145
|
+
await this.parse({ strict: false, flags: {}, args: {} }, []);
|
|
146
|
+
this.argv = originalArgv;
|
|
147
|
+
let parsed;
|
|
148
|
+
try {
|
|
149
|
+
parsed = parseBridgeArgv(this.argv);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
153
|
+
this.error(message);
|
|
154
|
+
}
|
|
155
|
+
const { requestedEnv, yes, passthroughArgs } = parsed;
|
|
156
|
+
if (passthroughArgs.length === 0) {
|
|
157
|
+
this.error('Pass at least one v1 command to forward.');
|
|
158
|
+
}
|
|
159
|
+
const explicitEnvSelection = Boolean(requestedEnv && hasExplicitEnvSelection(this.argv));
|
|
160
|
+
if (explicitEnvSelection) {
|
|
161
|
+
const confirmed = await ensureCrossEnvConfirmed({
|
|
162
|
+
command: this,
|
|
163
|
+
requestedEnv,
|
|
164
|
+
yes,
|
|
165
|
+
});
|
|
166
|
+
if (!confirmed) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const runtime = await resolveManagedAppRuntime(requestedEnv);
|
|
171
|
+
if (!runtime) {
|
|
172
|
+
this.error(formatMissingManagedAppEnvMessage(requestedEnv));
|
|
173
|
+
}
|
|
174
|
+
const silentLike = hasSilentLikePassthrough(passthroughArgs);
|
|
175
|
+
const silentBridge = silentLike ? createSilentBridgeOptions() : undefined;
|
|
176
|
+
if (!silentLike) {
|
|
177
|
+
announceTargetEnv(runtime.envName);
|
|
178
|
+
}
|
|
179
|
+
if (runtime.kind === 'local') {
|
|
180
|
+
try {
|
|
181
|
+
await runLocalNocoBaseCommand(runtime, passthroughArgs, silentBridge?.commandOptions);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
185
|
+
this.error(message);
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
silentBridge?.flush();
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (runtime.kind === 'docker') {
|
|
193
|
+
try {
|
|
194
|
+
await runDockerNocoBaseCommand(runtime.containerName, passthroughArgs, silentBridge?.commandOptions);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
198
|
+
this.error(message);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
silentBridge?.flush();
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (runtime.kind === 'http') {
|
|
206
|
+
this.error(formatHttpEnvError(runtime.envName));
|
|
207
|
+
}
|
|
208
|
+
this.error(formatSshEnvError(runtime.envName));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -32,6 +32,18 @@ function localSourceLabel(source) {
|
|
|
32
32
|
function trimValue(value) {
|
|
33
33
|
return String(value ?? '').trim();
|
|
34
34
|
}
|
|
35
|
+
function pushOptionalEnvArg(args, key, value) {
|
|
36
|
+
if (typeof value === 'string') {
|
|
37
|
+
if (!value) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
args.push('-e', `${key}=${value}`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === 'boolean') {
|
|
44
|
+
args.push('-e', `${key}=${String(value)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
35
47
|
function normalizeDockerPlatform(value) {
|
|
36
48
|
const text = trimValue(value);
|
|
37
49
|
if (!text || text === 'auto') {
|
|
@@ -109,6 +121,9 @@ export async function buildSavedDockerRunArgs(runtime) {
|
|
|
109
121
|
const dbDatabase = trimValue(config.dbDatabase);
|
|
110
122
|
const dbUser = trimValue(config.dbUser);
|
|
111
123
|
const dbPassword = trimValue(config.dbPassword);
|
|
124
|
+
const dbSchema = trimValue(config.dbSchema);
|
|
125
|
+
const dbTablePrefix = trimValue(config.dbTablePrefix);
|
|
126
|
+
const dbUnderscored = typeof config.dbUnderscored === 'boolean' ? config.dbUnderscored : undefined;
|
|
112
127
|
const dockerRegistry = trimValue(config.dockerRegistry) || DEFAULT_DOCKER_REGISTRY;
|
|
113
128
|
const version = trimValue(config.downloadVersion) || DEFAULT_DOCKER_VERSION;
|
|
114
129
|
const imageRef = resolveDockerImageRef(dockerRegistry, version, {
|
|
@@ -163,7 +178,11 @@ export async function buildSavedDockerRunArgs(runtime) {
|
|
|
163
178
|
if (envFile) {
|
|
164
179
|
args.push('--env-file', envFile);
|
|
165
180
|
}
|
|
166
|
-
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}
|
|
181
|
+
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}`);
|
|
182
|
+
pushOptionalEnvArg(args, 'DB_SCHEMA', dbSchema || undefined);
|
|
183
|
+
pushOptionalEnvArg(args, 'DB_TABLE_PREFIX', dbTablePrefix || undefined);
|
|
184
|
+
pushOptionalEnvArg(args, 'DB_UNDERSCORED', dbUnderscored);
|
|
185
|
+
args.push(imageRef);
|
|
167
186
|
return {
|
|
168
187
|
appPort: appPort || undefined,
|
|
169
188
|
storagePath,
|
package/dist/lib/app-runtime.js
CHANGED
|
@@ -119,8 +119,13 @@ export async function runLocalNocoBaseCommand(runtime, args, options) {
|
|
|
119
119
|
const envVars = await buildRuntimeEnvVars(runtime);
|
|
120
120
|
await runNocoBaseCommand(args, {
|
|
121
121
|
cwd: runtime.projectRoot,
|
|
122
|
-
env:
|
|
122
|
+
env: {
|
|
123
|
+
...envVars,
|
|
124
|
+
...options?.env,
|
|
125
|
+
},
|
|
123
126
|
stdio: options?.stdio,
|
|
127
|
+
onStdout: options?.onStdout,
|
|
128
|
+
onStderr: options?.onStderr,
|
|
124
129
|
});
|
|
125
130
|
}
|
|
126
131
|
export async function dockerContainerExists(containerName) {
|
|
@@ -163,9 +168,13 @@ export async function stopDockerContainer(containerName, options) {
|
|
|
163
168
|
});
|
|
164
169
|
return 'stopped';
|
|
165
170
|
}
|
|
166
|
-
export async function runDockerNocoBaseCommand(containerName, args) {
|
|
167
|
-
await startDockerContainer(containerName);
|
|
168
|
-
|
|
171
|
+
export async function runDockerNocoBaseCommand(containerName, args, options) {
|
|
172
|
+
await startDockerContainer(containerName, { stdio: options?.stdio });
|
|
173
|
+
const dockerEnvArgs = Object.entries(options?.env ?? {}).flatMap(([key, value]) => ['-e', `${key}=${value}`]);
|
|
174
|
+
await run('docker', ['exec', ...dockerEnvArgs, '-w', DOCKER_APP_WORKDIR, containerName, 'yarn', 'nocobase', ...args], {
|
|
169
175
|
errorName: 'docker exec',
|
|
176
|
+
stdio: options?.stdio,
|
|
177
|
+
onStdout: options?.onStdout,
|
|
178
|
+
onStderr: options?.onStderr,
|
|
170
179
|
});
|
|
171
180
|
}
|
package/dist/lib/auth-store.js
CHANGED
|
@@ -159,6 +159,9 @@ export class Env {
|
|
|
159
159
|
get auth() {
|
|
160
160
|
return this.config.auth;
|
|
161
161
|
}
|
|
162
|
+
get authType() {
|
|
163
|
+
return resolveConfiguredAuthType(this.config);
|
|
164
|
+
}
|
|
162
165
|
get runtime() {
|
|
163
166
|
return this.config.runtime;
|
|
164
167
|
}
|
|
@@ -210,6 +213,9 @@ export class Env {
|
|
|
210
213
|
put('DB_DATABASE', this.config.dbDatabase);
|
|
211
214
|
put('DB_USER', this.config.dbUser);
|
|
212
215
|
put('DB_PASSWORD', this.config.dbPassword);
|
|
216
|
+
put('DB_SCHEMA', this.config.dbSchema);
|
|
217
|
+
put('DB_TABLE_PREFIX', this.config.dbTablePrefix);
|
|
218
|
+
put('DB_UNDERSCORED', this.config.dbUnderscored);
|
|
213
219
|
return out;
|
|
214
220
|
}
|
|
215
221
|
}
|
|
@@ -253,27 +259,38 @@ async function writeEnv(envName, updater, options = {}) {
|
|
|
253
259
|
config.envs[envName] = updater(previous);
|
|
254
260
|
await saveAuthConfig(config, options);
|
|
255
261
|
}
|
|
262
|
+
function normalizeConfiguredAuthType(value) {
|
|
263
|
+
return value === 'token' || value === 'oauth' ? value : undefined;
|
|
264
|
+
}
|
|
265
|
+
export function resolveConfiguredAuthType(config) {
|
|
266
|
+
return normalizeConfiguredAuthType(config?.authType) ?? normalizeConfiguredAuthType(config?.auth?.type);
|
|
267
|
+
}
|
|
256
268
|
export async function upsertEnv(envName, config, options = {}) {
|
|
257
269
|
await writeEnv(envName, (previous) => {
|
|
258
|
-
const { apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, accessToken, ...rest } = config;
|
|
270
|
+
const { apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, accessToken, authType, ...rest } = config;
|
|
259
271
|
const nextApiBaseUrl = readEnvApiBaseUrl(config);
|
|
260
272
|
const previousApiBaseUrl = readEnvApiBaseUrl(previous);
|
|
261
273
|
const baseUrlChanged = previousApiBaseUrl !== nextApiBaseUrl;
|
|
274
|
+
const previousAuthType = resolveConfiguredAuthType(previous);
|
|
275
|
+
const requestedAuthType = normalizeConfiguredAuthType(authType);
|
|
276
|
+
const nextAuthType = requestedAuthType ?? (accessToken ? 'token' : previousAuthType);
|
|
262
277
|
const nextAuth = accessToken
|
|
263
278
|
? {
|
|
264
279
|
type: 'token',
|
|
265
280
|
accessToken,
|
|
266
281
|
}
|
|
267
|
-
: baseUrlChanged || previous?.auth?.type === 'token'
|
|
282
|
+
: nextAuthType === 'token' || baseUrlChanged || previous?.auth?.type === 'token'
|
|
268
283
|
? undefined
|
|
269
284
|
: previous?.auth;
|
|
270
285
|
const authChanged = !areAuthConfigsEquivalent(previous?.auth, nextAuth);
|
|
286
|
+
const authTypeChanged = previousAuthType !== nextAuthType;
|
|
271
287
|
return {
|
|
272
288
|
...previous,
|
|
273
289
|
apiBaseUrl: nextApiBaseUrl,
|
|
290
|
+
authType: nextAuthType,
|
|
274
291
|
auth: nextAuth,
|
|
275
292
|
...rest,
|
|
276
|
-
runtime: baseUrlChanged || authChanged ? undefined : previous?.runtime,
|
|
293
|
+
runtime: baseUrlChanged || authChanged || authTypeChanged ? undefined : previous?.runtime,
|
|
277
294
|
};
|
|
278
295
|
}, options);
|
|
279
296
|
}
|
|
@@ -282,26 +299,32 @@ export async function updateEnvConnection(envName, updates, options = {}) {
|
|
|
282
299
|
const nextApiBaseUrl = readEnvApiBaseUrl(updates) ?? readEnvApiBaseUrl(previous);
|
|
283
300
|
const previousApiBaseUrl = readEnvApiBaseUrl(previous);
|
|
284
301
|
const baseUrlChanged = previousApiBaseUrl !== nextApiBaseUrl;
|
|
302
|
+
const previousAuthType = resolveConfiguredAuthType(previous);
|
|
303
|
+
const requestedAuthType = normalizeConfiguredAuthType(updates.authType);
|
|
304
|
+
const nextAuthType = requestedAuthType ?? (updates.accessToken ? 'token' : previousAuthType);
|
|
285
305
|
const nextAuth = updates.accessToken
|
|
286
306
|
? {
|
|
287
307
|
type: 'token',
|
|
288
308
|
accessToken: updates.accessToken,
|
|
289
309
|
}
|
|
290
|
-
: baseUrlChanged || previous?.auth?.type === 'token'
|
|
310
|
+
: nextAuthType === 'token' || baseUrlChanged || previous?.auth?.type === 'token'
|
|
291
311
|
? undefined
|
|
292
312
|
: previous?.auth;
|
|
293
313
|
const authChanged = !areAuthConfigsEquivalent(previous?.auth, nextAuth);
|
|
314
|
+
const authTypeChanged = previousAuthType !== nextAuthType;
|
|
294
315
|
return {
|
|
295
316
|
...previous,
|
|
296
317
|
...(nextApiBaseUrl !== undefined ? { apiBaseUrl: nextApiBaseUrl } : {}),
|
|
318
|
+
authType: nextAuthType,
|
|
297
319
|
auth: nextAuth,
|
|
298
|
-
runtime: baseUrlChanged || authChanged ? undefined : previous?.runtime,
|
|
320
|
+
runtime: baseUrlChanged || authChanged || authTypeChanged ? undefined : previous?.runtime,
|
|
299
321
|
};
|
|
300
322
|
}, options);
|
|
301
323
|
}
|
|
302
324
|
export async function setEnvOauthSession(envName, auth, options = {}) {
|
|
303
325
|
await writeEnv(envName, (previous) => ({
|
|
304
326
|
...previous,
|
|
327
|
+
authType: 'oauth',
|
|
305
328
|
auth,
|
|
306
329
|
runtime: options.preserveRuntime ? previous?.runtime : undefined,
|
|
307
330
|
}), options);
|
|
@@ -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/env-config.js
CHANGED
|
@@ -26,6 +26,8 @@ const STRING_ENV_CONFIG_KEYS = [
|
|
|
26
26
|
'dbDatabase',
|
|
27
27
|
'dbUser',
|
|
28
28
|
'dbPassword',
|
|
29
|
+
'dbSchema',
|
|
30
|
+
'dbTablePrefix',
|
|
29
31
|
'rootUsername',
|
|
30
32
|
'rootEmail',
|
|
31
33
|
'rootPassword',
|
|
@@ -36,6 +38,7 @@ const BOOLEAN_ENV_CONFIG_KEYS = [
|
|
|
36
38
|
'devDependencies',
|
|
37
39
|
'build',
|
|
38
40
|
'buildDts',
|
|
41
|
+
'dbUnderscored',
|
|
39
42
|
];
|
|
40
43
|
function trimConfigValue(value) {
|
|
41
44
|
const text = String(value ?? '').trim();
|
|
@@ -80,6 +83,9 @@ export function buildStoredEnvConfig(input) {
|
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
const authType = trimConfigValue(input.authType);
|
|
86
|
+
if (authType === 'token' || authType === 'oauth') {
|
|
87
|
+
envConfig.authType = authType;
|
|
88
|
+
}
|
|
83
89
|
const accessToken = trimConfigValue(input.accessToken);
|
|
84
90
|
if (authType === 'token' && accessToken) {
|
|
85
91
|
envConfig.accessToken = accessToken;
|