@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.
@@ -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}`, imageRef);
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,
@@ -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: envVars,
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
- await run('docker', ['exec', '-w', DOCKER_APP_WORKDIR, containerName, 'yarn', 'nocobase', ...args], {
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
  }
@@ -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
+ }
@@ -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
- printVerbose(`Waiting for health check: ${healthCheckUrl}`);
166
- updateTask(`Waiting for application readiness (${healthCheckUrl})`);
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
- printVerbose(`Checking swagger schema: ${swaggerUrl}`);
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
- updateTask('Loading command runtime...');
375
+ if (!options.quiet) {
376
+ updateTask('Loading command runtime...');
377
+ }
372
378
  try {
373
- printVerbose(`Runtime source: ${baseUrl}`);
374
- const document = await fetchSwaggerSchema(baseUrl, token, options.role, { envName });
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
- stopTask();
399
+ if (!options.quiet) {
400
+ stopTask();
401
+ }
392
402
  }
393
403
  }
@@ -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;