@nocobase/cli 2.1.0-beta.37 → 2.1.0-beta.40

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.
@@ -7,34 +7,18 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
- import { upsertEnv } from '../../lib/auth-store.js';
11
- import { ensureLocalPostinstall } from '../../lib/app-managed-resources.js';
12
- import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, startDockerContainer, stopDockerContainer, } from '../../lib/app-runtime.js';
13
- import { resolveConfiguredEnvPath } from '../../lib/cli-home.js';
14
- import { deriveBuiltinDbConnection } from '../../lib/builtin-db.js';
15
- import { resolveDockerEnvFileArg } from "../../lib/docker-env-file.js";
10
+ import { getCurrentEnvName, upsertEnv } from '../../lib/auth-store.js';
11
+ import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, } from '../../lib/app-runtime.js';
16
12
  import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
17
- import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_VERSION, resolveDockerImageRef, } from "../../lib/docker-image.js";
18
- import { commandSucceeds, run } from '../../lib/run-npm.js';
19
- import { announceTargetEnv, failTask, printInfo, startTask, stopTask, succeedTask, updateTask } from '../../lib/ui.js';
20
- const DOCKER_APP_STORAGE_DESTINATION = '/app/nocobase/storage';
21
- const APP_HEALTH_CHECK_INTERVAL_MS = 2_000;
22
- const APP_HEALTH_CHECK_TIMEOUT_MS = 600_000;
23
- const APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS = 5_000;
13
+ import { DEFAULT_DOCKER_REGISTRY } from "../../lib/docker-image.js";
14
+ import { confirm } from "../../lib/inquirer.js";
15
+ import { announceTargetEnv, isInteractiveTerminal, printInfo, printWarning, succeedTask } from '../../lib/ui.js';
24
16
  function trimValue(value) {
25
17
  return String(value ?? '').trim();
26
18
  }
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
- }
19
+ function normalizeEnvName(value) {
20
+ const text = trimValue(value);
21
+ return text || undefined;
38
22
  }
39
23
  function formatAppUrl(port) {
40
24
  const value = trimValue(port);
@@ -51,27 +35,21 @@ function formatDisplayUrl(apiBaseUrl, appPort) {
51
35
  }
52
36
  return value.replace(/\/api\/?$/, '');
53
37
  }
54
- function resolveApiBaseUrl(runtime) {
55
- const baseUrl = trimValue(runtime.env.baseUrl);
56
- if (baseUrl) {
57
- return baseUrl.replace(/\/+$/, '');
58
- }
59
- const appPort = runtime.env.appPort === undefined || runtime.env.appPort === null
60
- ? ''
61
- : trimValue(runtime.env.appPort);
62
- return appPort ? `http://127.0.0.1:${appPort}/api` : undefined;
63
- }
64
- function buildHealthCheckUrl(apiBaseUrl) {
65
- return `${apiBaseUrl.replace(/\/+$/, '')}/__health_check`;
38
+ function readEnvValue(env, key) {
39
+ return trimValue(env.config[key]);
66
40
  }
67
- function dockerRefLabel(source) {
68
- if (source === 'git') {
69
- return 'Git checkout';
41
+ function normalizeDockerPlatform(value) {
42
+ const text = trimValue(value);
43
+ if (!text || text === 'auto') {
44
+ return undefined;
70
45
  }
71
- if (source === 'npm') {
72
- return 'npm app';
46
+ if (text === 'linux/amd64' || text === 'linux/arm64') {
47
+ return text;
73
48
  }
74
- return 'local app';
49
+ return undefined;
50
+ }
51
+ function isDownloadableLocalRuntime(runtime) {
52
+ return runtime.kind === 'local' && (runtime.source === 'npm' || runtime.source === 'git');
75
53
  }
76
54
  function formatLocalDownloadFailure(envName, source, message) {
77
55
  const sourceLabel = source === 'git' ? 'the saved Git checkout' : 'the saved npm app';
@@ -82,16 +60,6 @@ function formatLocalDownloadFailure(envName, source, message) {
82
60
  `Details: ${message}`,
83
61
  ].join('\n');
84
62
  }
85
- function formatLocalStartFailure(envName, source, port, message) {
86
- const sourceLabel = dockerRefLabel(source);
87
- const portHint = trimValue(port) ? ` Expected app port: ${trimValue(port)}.` : '';
88
- const details = trimValue(message) ? ` Details: ${trimValue(message)}` : '';
89
- return [
90
- `Couldn't finish the upgrade for "${envName}".`,
91
- `The CLI updated ${sourceLabel}, but it could not start the upgraded app successfully.`,
92
- `Check the local dependencies, database connection, and saved env settings, then try again.${portHint}${details}`,
93
- ].join('\n');
94
- }
95
63
  function formatDockerDownloadFailure(envName, message) {
96
64
  return [
97
65
  `Couldn't refresh the Docker image for "${envName}".`,
@@ -100,122 +68,187 @@ function formatDockerDownloadFailure(envName, message) {
100
68
  `Details: ${message}`,
101
69
  ].join('\n');
102
70
  }
103
- function formatDockerStartFailure(envName, message) {
104
- return [
105
- `Couldn't finish the upgrade for "${envName}".`,
106
- 'The CLI was not able to start the upgraded Docker app successfully.',
107
- 'Check that the saved Docker image, container settings, and database connection are still valid, then try again.',
108
- `Details: ${message}`,
109
- ].join('\n');
71
+ function buildManagedActionArgv(envName, flags, options) {
72
+ const argv = ['--env', envName, '--yes'];
73
+ if (flags.verbose) {
74
+ argv.push('--verbose');
75
+ }
76
+ if (options?.quickstart) {
77
+ argv.push('--quickstart');
78
+ }
79
+ return argv;
110
80
  }
111
- function normalizeDockerPlatform(value) {
112
- const text = String(value ?? '').trim();
113
- if (!text || text === 'auto') {
114
- return undefined;
81
+ function shouldSkipDownload(flags) {
82
+ return Boolean(flags['skip-download'] || flags['skip-code-update']);
83
+ }
84
+ function buildUpgradeCliArgv(envName, flags, options) {
85
+ const argv = ['--env', envName];
86
+ if (shouldSkipDownload(flags)) {
87
+ argv.push('--skip-download');
115
88
  }
116
- if (text === 'linux/amd64' || text === 'linux/arm64') {
117
- return text;
89
+ const version = normalizeEnvName(flags.version);
90
+ if (version) {
91
+ argv.push('--version', version);
118
92
  }
119
- return undefined;
93
+ if (flags.verbose) {
94
+ argv.push('--verbose');
95
+ }
96
+ if (options?.yes ?? flags.yes) {
97
+ argv.push('--yes');
98
+ }
99
+ if (options?.force ?? flags.force) {
100
+ argv.push('--force');
101
+ }
102
+ return argv;
120
103
  }
121
- function readEnvValue(env, key) {
122
- if (key === 'appRootPath' || key === 'storagePath') {
123
- return trimValue(resolveConfiguredEnvPath(env.config[key]));
104
+ function buildUpgradeCliCommand(envName, flags, options) {
105
+ return ['nb', 'app', 'upgrade', ...buildUpgradeCliArgv(envName, flags, options)].join(' ');
106
+ }
107
+ function formatUpgradeOperationSummary(runtime, flags) {
108
+ const mayRunUpgradeMigrations = 'It may also run upgrade migrations.';
109
+ if (shouldSkipDownload(flags)) {
110
+ const sourceLabel = runtime.kind === 'docker'
111
+ ? 'saved Docker image'
112
+ : runtime.source === 'local'
113
+ ? 'saved local app path'
114
+ : runtime.source === 'git'
115
+ ? 'saved Git checkout'
116
+ : 'saved npm app';
117
+ return [
118
+ 'This operation will stop the app, skip source download and commercial plugin sync,',
119
+ `and start it again with the ${sourceLabel}.`,
120
+ mayRunUpgradeMigrations,
121
+ ].join(' ');
124
122
  }
125
- return trimValue(env.config[key]);
123
+ if (runtime.kind === 'docker') {
124
+ return [
125
+ 'This operation will stop the app, replace the saved Docker image,',
126
+ 'sync commercial plugins when applicable, and start the app again.',
127
+ mayRunUpgradeMigrations,
128
+ ].join(' ');
129
+ }
130
+ if (runtime.source === 'local') {
131
+ return [
132
+ 'This operation will stop the app, reuse the saved local app path,',
133
+ 'sync commercial plugins when applicable, and start the app again.',
134
+ mayRunUpgradeMigrations,
135
+ ].join(' ');
136
+ }
137
+ return [
138
+ 'This operation will stop the app, replace the saved source,',
139
+ 'sync commercial plugins when applicable, and start the app again.',
140
+ mayRunUpgradeMigrations,
141
+ ].join(' ');
126
142
  }
127
- async function sleep(ms) {
128
- await new Promise((resolve) => setTimeout(resolve, ms));
143
+ function formatUpgradePromptSummary(flags) {
144
+ if (shouldSkipDownload(flags)) {
145
+ return 'This will stop and restart the app, and may run upgrade migrations.';
146
+ }
147
+ return 'This will stop and restart the app, update the saved source or image, and may run upgrade migrations.';
129
148
  }
130
- async function requestAppHealthCheck(params) {
131
- const controller = new AbortController();
132
- const timeout = setTimeout(() => {
133
- controller.abort();
134
- }, params.requestTimeoutMs ?? APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS);
135
- try {
136
- const response = await (params.fetchImpl ?? fetch)(params.healthCheckUrl, {
137
- method: 'GET',
138
- signal: controller.signal,
139
- });
140
- const text = await response.text().catch(() => '');
141
- const body = text.replace(/\s+/g, ' ').trim() || 'No response yet';
142
- return {
143
- ok: response.ok && text.trim().toLowerCase() === 'ok',
144
- message: `HTTP ${response.status}: ${body}`,
145
- };
149
+ function formatUpgradeForceRequiredMessage(runtime, flags) {
150
+ return [
151
+ `\`nb app upgrade\` needs confirmation in non-interactive mode before upgrading "${runtime.envName}".`,
152
+ '',
153
+ formatUpgradeOperationSummary(runtime, flags),
154
+ '',
155
+ 'Interactive confirmation is unavailable in the current AI agent session, and the agent will not add `--force` on your behalf.',
156
+ '',
157
+ 'To continue:',
158
+ `- re-run \`${buildUpgradeCliCommand(runtime.envName, flags, { force: true })}\``,
159
+ `- or switch to an interactive terminal and re-run \`${buildUpgradeCliCommand(runtime.envName, flags, {
160
+ force: false,
161
+ })}\``,
162
+ ].join('\n');
163
+ }
164
+ function formatMissingUpgradeFlagList(options) {
165
+ const missingFlags = [
166
+ options.missingYes ? '`--yes`' : undefined,
167
+ options.missingForce ? '`--force`' : undefined,
168
+ ].filter(Boolean);
169
+ if (missingFlags.length <= 1) {
170
+ return missingFlags[0] ?? '';
146
171
  }
147
- catch (error) {
148
- if (error instanceof Error && error.name === 'AbortError') {
149
- return {
150
- ok: false,
151
- message: `No response within ${Math.ceil((params.requestTimeoutMs ?? APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS) / 1000)}s`,
152
- };
153
- }
154
- return {
155
- ok: false,
156
- message: error instanceof Error ? error.message : String(error),
157
- };
172
+ return `${missingFlags[0]} or ${missingFlags[1]}`;
173
+ }
174
+ function formatUpgradeCrossEnvConfirmationRequiredMessage(currentEnv, runtime, flags, options) {
175
+ return [
176
+ `Refusing to upgrade env "${runtime.envName}" because the current env is "${currentEnv}" and interactive confirmation is unavailable in the current AI agent session.`,
177
+ '',
178
+ formatUpgradeOperationSummary(runtime, flags),
179
+ '',
180
+ `For safety, the agent will not switch envs automatically and will not add ${formatMissingUpgradeFlagList(options)} on your behalf.`,
181
+ '',
182
+ 'To continue:',
183
+ `- run \`nb env use ${runtime.envName}\` yourself, then re-run \`${buildUpgradeCliCommand(runtime.envName, flags, {
184
+ yes: false,
185
+ force: true,
186
+ })}\``,
187
+ `- or re-run \`${buildUpgradeCliCommand(runtime.envName, flags, { yes: true, force: true })}\``,
188
+ ].join('\n');
189
+ }
190
+ function buildLicenseSyncArgv(envName, flags, options) {
191
+ const argv = ['--env', envName, '--yes', '--skip-if-no-license'];
192
+ if (flags.verbose) {
193
+ argv.push('--verbose');
158
194
  }
159
- finally {
160
- clearTimeout(timeout);
195
+ if (options?.version) {
196
+ argv.push('--version', options.version);
161
197
  }
198
+ return argv;
162
199
  }
163
- async function waitForAppHealthCheck(params) {
164
- if (!params.apiBaseUrl) {
165
- printInfo(`Skipping health check for "${params.envName}" because no local API URL is saved for this env.`);
166
- return;
200
+ function buildEnvUpdateArgv(envName, flags) {
201
+ const argv = [envName];
202
+ if (flags.verbose) {
203
+ argv.push('--verbose');
167
204
  }
168
- const healthCheckUrl = buildHealthCheckUrl(params.apiBaseUrl);
169
- const startedAt = Date.now();
170
- let lastMessage = 'No response yet';
171
- let spinnerActive = true;
172
- startTask(`Waiting for NocoBase to become ready for "${params.envName}"...`);
205
+ return argv;
206
+ }
207
+ function formatEnvUpdateWarning(envName, message) {
208
+ return [
209
+ `NocoBase was upgraded for "${envName}", but the CLI could not refresh the saved env runtime.`,
210
+ `Run \`nb env update ${envName}\` to refresh it manually.`,
211
+ `Details: ${message}`,
212
+ ].join(' ');
213
+ }
214
+ async function runWithSuppressedTargetEnvLog(task) {
215
+ const previousTargetEnv = process.env.NB_SKIP_TARGET_ENV_LOG;
216
+ process.env.NB_SKIP_TARGET_ENV_LOG = '1';
173
217
  try {
174
- while (Date.now() - startedAt < APP_HEALTH_CHECK_TIMEOUT_MS) {
175
- const result = await requestAppHealthCheck({
176
- healthCheckUrl,
177
- fetchImpl: params.fetchImpl,
178
- });
179
- if (result.ok) {
180
- stopTask();
181
- spinnerActive = false;
182
- return;
183
- }
184
- lastMessage = result.message;
185
- const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
186
- updateTask(`Waiting for NocoBase to become ready for "${params.envName}"... (${elapsedSeconds}s elapsed, last status: ${lastMessage})`);
187
- await sleep(APP_HEALTH_CHECK_INTERVAL_MS);
188
- }
218
+ return await task();
189
219
  }
190
220
  finally {
191
- if (spinnerActive) {
192
- stopTask();
221
+ if (previousTargetEnv === undefined) {
222
+ delete process.env.NB_SKIP_TARGET_ENV_LOG;
223
+ }
224
+ else {
225
+ process.env.NB_SKIP_TARGET_ENV_LOG = previousTargetEnv;
193
226
  }
194
227
  }
195
- const logHint = params.containerName
196
- ? ` You can inspect startup logs with: docker logs ${params.containerName}`
197
- : '';
198
- throw new Error(`The upgraded app for "${params.envName}" did not become ready in time. Expected \`${healthCheckUrl}\` to respond with \`ok\`, but the last status was: ${lastMessage}.${logHint}`);
199
- }
200
- async function dockerContainerExists(containerName) {
201
- return await commandSucceeds('docker', ['container', 'inspect', containerName]);
202
228
  }
203
- async function ensureDockerNetwork(name) {
204
- const exists = await commandSucceeds('docker', ['network', 'inspect', name]);
205
- if (exists) {
206
- return;
229
+ async function runWithSuppressedStartSuccessLog(task) {
230
+ const previousStartSuccess = process.env.NB_SKIP_APP_START_SUCCESS_LOG;
231
+ process.env.NB_SKIP_APP_START_SUCCESS_LOG = '1';
232
+ try {
233
+ return await task();
234
+ }
235
+ finally {
236
+ if (previousStartSuccess === undefined) {
237
+ delete process.env.NB_SKIP_APP_START_SUCCESS_LOG;
238
+ }
239
+ else {
240
+ process.env.NB_SKIP_APP_START_SUCCESS_LOG = previousStartSuccess;
241
+ }
207
242
  }
208
- await run('docker', ['network', 'create', name], {
209
- errorName: 'docker network create',
210
- stdio: 'ignore',
211
- });
212
243
  }
213
244
  export default class AppUpgrade extends Command {
214
245
  static hidden = false;
215
- static description = 'Upgrade the selected NocoBase app. Local npm/git installs refresh the saved source and restart with quickstart; Docker installs refresh the saved image and recreate the app container. Use --version to upgrade to a specific saved source version or image tag.';
246
+ static description = 'Upgrade the selected NocoBase app. The CLI stops the current app, optionally replaces the saved source or image, then starts the app again. Use --version to upgrade to a specific saved source version or image tag.';
216
247
  static examples = [
217
248
  '<%= config.bin %> <%= command.id %>',
249
+ '<%= config.bin %> <%= command.id %> --force',
218
250
  '<%= config.bin %> <%= command.id %> --env local',
251
+ '<%= config.bin %> <%= command.id %> --env local --force',
219
252
  '<%= config.bin %> <%= command.id %> --env local -s',
220
253
  '<%= config.bin %> <%= command.id %> --env local --version beta',
221
254
  '<%= config.bin %> <%= command.id %> --env local --verbose',
@@ -231,9 +264,20 @@ export default class AppUpgrade extends Command {
231
264
  description: 'Confirm using --env when it targets a different env than the current env',
232
265
  default: false,
233
266
  }),
234
- 'skip-code-update': Flags.boolean({
267
+ force: Flags.boolean({
268
+ char: 'f',
269
+ description: 'Skip the upgrade confirmation prompt',
270
+ default: false,
271
+ }),
272
+ 'skip-download': Flags.boolean({
235
273
  char: 's',
236
- description: 'Restart with the saved local code or Docker image without downloading updates first',
274
+ description: 'Restart with the saved local source or Docker image without downloading updates first',
275
+ required: false,
276
+ }),
277
+ 'skip-code-update': Flags.boolean({
278
+ hidden: true,
279
+ deprecated: true,
280
+ description: 'Deprecated alias for --skip-download',
237
281
  required: false,
238
282
  }),
239
283
  version: Flags.string({
@@ -247,9 +291,6 @@ export default class AppUpgrade extends Command {
247
291
  };
248
292
  static resolveUpgradeVersion(runtime, flags) {
249
293
  const requestedVersion = trimValue(flags.version);
250
- if (requestedVersion && flags['skip-code-update']) {
251
- throw new Error('`--version` and `--skip-code-update` cannot be used together. Use `--version` to download a specific upgrade target, or `--skip-code-update` to restart the saved code/image as-is.');
252
- }
253
294
  if (runtime.kind === 'local' && runtime.source === 'local') {
254
295
  if (requestedVersion) {
255
296
  throw new Error([
@@ -260,6 +301,11 @@ export default class AppUpgrade extends Command {
260
301
  }
261
302
  return {};
262
303
  }
304
+ if (shouldSkipDownload(flags)) {
305
+ return {
306
+ persistDownloadVersion: requestedVersion || undefined,
307
+ };
308
+ }
263
309
  const savedVersion = readEnvValue(runtime.env, 'downloadVersion');
264
310
  const downloadVersion = requestedVersion || savedVersion;
265
311
  if (!downloadVersion) {
@@ -276,15 +322,15 @@ export default class AppUpgrade extends Command {
276
322
  }
277
323
  static buildLocalDownloadArgv(runtime, downloadVersion, options) {
278
324
  const argv = ['-y', '--no-intro', '--source', runtime.source, '--replace'];
279
- const gitUrl = readEnvValue(runtime.env, 'gitUrl');
280
- const npmRegistry = readEnvValue(runtime.env, 'npmRegistry');
281
325
  if (options?.verbose) {
282
326
  argv.push('--verbose');
283
327
  }
284
328
  argv.push('--version', downloadVersion, '--output-dir', runtime.projectRoot);
329
+ const gitUrl = readEnvValue(runtime.env, 'gitUrl');
285
330
  if (gitUrl) {
286
331
  argv.push('--git-url', gitUrl);
287
332
  }
333
+ const npmRegistry = readEnvValue(runtime.env, 'npmRegistry');
288
334
  if (npmRegistry) {
289
335
  argv.push('--npm-registry', npmRegistry);
290
336
  }
@@ -299,278 +345,18 @@ export default class AppUpgrade extends Command {
299
345
  }
300
346
  return argv;
301
347
  }
302
- static buildDockerDownloadArgv(runtime, plan, options) {
348
+ static buildDockerDownloadArgv(runtime, downloadVersion, options) {
303
349
  const argv = ['-y', '--no-intro'];
304
350
  if (options?.verbose) {
305
351
  argv.push('--verbose');
306
352
  }
307
- argv.push('--source', 'docker', '--replace', '--docker-registry', plan.dockerRegistry, '--version', plan.downloadVersion);
353
+ argv.push('--source', 'docker', '--replace', '--docker-registry', readEnvValue(runtime.env, 'dockerRegistry') || DEFAULT_DOCKER_REGISTRY, '--version', downloadVersion);
308
354
  const dockerPlatform = normalizeDockerPlatform(runtime.env.config.dockerPlatform);
309
355
  if (dockerPlatform) {
310
356
  argv.push('--docker-platform', dockerPlatform);
311
357
  }
312
358
  return argv;
313
359
  }
314
- static buildLocalStartArgv(runtime) {
315
- const argv = ['start', '--quickstart'];
316
- const appPort = runtime.env.appPort === undefined || runtime.env.appPort === null
317
- ? ''
318
- : trimValue(runtime.env.appPort);
319
- if (appPort) {
320
- argv.push('--port', appPort);
321
- }
322
- argv.push('--daemon');
323
- return argv;
324
- }
325
- static async buildDockerUpgradePlan(runtime, downloadVersion) {
326
- const dockerRegistry = readEnvValue(runtime.env, 'dockerRegistry') || DEFAULT_DOCKER_REGISTRY;
327
- const appPort = runtime.env.appPort === undefined || runtime.env.appPort === null
328
- ? ''
329
- : trimValue(runtime.env.appPort);
330
- const storagePath = readEnvValue(runtime.env, 'storagePath');
331
- const envFile = await resolveDockerEnvFileArg(runtime.envName, runtime.env.config);
332
- const appKey = readEnvValue(runtime.env, 'appKey');
333
- const timeZone = readEnvValue(runtime.env, 'timezone');
334
- const builtinDbConnection = runtime.env.config.builtinDb ? deriveBuiltinDbConnection(runtime) : undefined;
335
- const dbDialect = builtinDbConnection?.dbDialect || readEnvValue(runtime.env, 'dbDialect');
336
- const dbHost = builtinDbConnection?.dbHost || readEnvValue(runtime.env, 'dbHost');
337
- const dbPort = builtinDbConnection?.dbPort || readEnvValue(runtime.env, 'dbPort');
338
- const dbDatabase = readEnvValue(runtime.env, 'dbDatabase');
339
- const dbUser = readEnvValue(runtime.env, 'dbUser');
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;
346
- const networkName = trimValue(runtime.dockerNetworkName || runtime.workspaceName);
347
- const missing = [];
348
- if (!networkName) {
349
- missing.push('docker.network');
350
- }
351
- if (!storagePath) {
352
- missing.push('storagePath');
353
- }
354
- if (!appKey) {
355
- missing.push('appKey');
356
- }
357
- if (!timeZone) {
358
- missing.push('timezone');
359
- }
360
- if (!dbDialect) {
361
- missing.push('dbDialect');
362
- }
363
- if (!dbHost) {
364
- missing.push('dbHost');
365
- }
366
- if (!dbPort) {
367
- missing.push('dbPort');
368
- }
369
- if (!dbDatabase) {
370
- missing.push('dbDatabase');
371
- }
372
- if (!dbUser) {
373
- missing.push('dbUser');
374
- }
375
- if (!dbPassword) {
376
- missing.push('dbPassword');
377
- }
378
- if (missing.length > 0) {
379
- throw new Error(`The saved Docker settings for "${runtime.envName}" are incomplete. Missing: ${missing.join(', ')}. Re-run \`nb init\` or \`nb env add\` to refresh this env config.`);
380
- }
381
- const imageRef = resolveDockerImageRef(dockerRegistry, downloadVersion, {
382
- defaultRegistry: DEFAULT_DOCKER_REGISTRY,
383
- defaultVersion: DEFAULT_DOCKER_VERSION,
384
- });
385
- const args = [
386
- 'run',
387
- '-d',
388
- '--name',
389
- runtime.containerName,
390
- '--restart',
391
- 'always',
392
- '--network',
393
- networkName,
394
- ];
395
- if (appPort) {
396
- args.push('-p', `${appPort}:80`);
397
- }
398
- if (envFile) {
399
- args.push('--env-file', envFile);
400
- }
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);
406
- return {
407
- containerName: runtime.containerName,
408
- networkName,
409
- dockerRegistry,
410
- downloadVersion,
411
- imageRef,
412
- appPort: appPort || undefined,
413
- storagePath,
414
- envFile,
415
- appKey,
416
- timeZone,
417
- dbDialect,
418
- dbHost,
419
- dbPort,
420
- dbDatabase,
421
- dbUser,
422
- dbPassword,
423
- args,
424
- };
425
- }
426
- static async upgradeLocal(runCommand, runtime, downloadVersion, flags, commandStdio) {
427
- const displayUrl = formatDisplayUrl(resolveApiBaseUrl(runtime), trimValue(runtime.env.appPort));
428
- startTask(`Stopping NocoBase for "${runtime.envName}" before upgrade...`);
429
- try {
430
- await runLocalNocoBaseCommand(runtime, ['pm2', 'kill'], {
431
- stdio: commandStdio,
432
- });
433
- succeedTask(`Stopped the current NocoBase process for "${runtime.envName}".`);
434
- }
435
- catch (error) {
436
- stopTask();
437
- const message = error instanceof Error ? error.message : String(error);
438
- printInfo(`No running background process was stopped for "${runtime.envName}". Continuing with the upgrade. (${message})`);
439
- }
440
- if (!flags['skip-code-update'] && (runtime.source === 'npm' || runtime.source === 'git')) {
441
- startTask(`Refreshing NocoBase files for "${runtime.envName}" from the saved ${runtime.source} source...`);
442
- try {
443
- await runCommand('source:download', AppUpgrade.buildLocalDownloadArgv(runtime, downloadVersion, {
444
- verbose: flags.verbose,
445
- }));
446
- succeedTask(`NocoBase files are up to date for "${runtime.envName}".`);
447
- }
448
- catch (error) {
449
- const message = error instanceof Error ? error.message : String(error);
450
- failTask(`Failed to refresh NocoBase files for "${runtime.envName}".`);
451
- throw new Error(formatLocalDownloadFailure(runtime.envName, runtime.source, message));
452
- }
453
- }
454
- else if (flags['skip-code-update']) {
455
- printInfo(`Skipping code download for "${runtime.envName}" (--skip-code-update).`);
456
- }
457
- else {
458
- printInfo(`Skipping code download for "${runtime.envName}" because this env is managed from an existing local app path.`);
459
- }
460
- await ensureLocalPostinstall(runtime, {
461
- verbose: flags.verbose,
462
- onStartTask: startTask,
463
- onSucceedTask: succeedTask,
464
- onFailTask: failTask,
465
- });
466
- startTask(`Starting upgraded NocoBase for "${runtime.envName}"...`);
467
- try {
468
- await runLocalNocoBaseCommand(runtime, AppUpgrade.buildLocalStartArgv(runtime), {
469
- stdio: commandStdio,
470
- });
471
- succeedTask(`Upgraded NocoBase is starting for "${runtime.envName}".`);
472
- }
473
- catch (error) {
474
- const message = error instanceof Error ? error.message : String(error);
475
- failTask(`Failed to start upgraded NocoBase for "${runtime.envName}".`);
476
- throw new Error(formatLocalStartFailure(runtime.envName, runtime.source, trimValue(runtime.env.appPort), message));
477
- }
478
- await waitForAppHealthCheck({
479
- envName: runtime.envName,
480
- apiBaseUrl: resolveApiBaseUrl(runtime),
481
- });
482
- return displayUrl;
483
- }
484
- static async upgradeDocker(runCommand, runtime, downloadVersion, flags, commandStdio) {
485
- const apiBaseUrl = resolveApiBaseUrl(runtime);
486
- const containerExists = await dockerContainerExists(runtime.containerName);
487
- if (!flags['skip-code-update']) {
488
- const plan = await AppUpgrade.buildDockerUpgradePlan(runtime, downloadVersion);
489
- startTask(`Refreshing the Docker image for "${runtime.envName}"...`);
490
- try {
491
- await runCommand('source:download', AppUpgrade.buildDockerDownloadArgv(runtime, plan, {
492
- verbose: flags.verbose,
493
- }));
494
- succeedTask(`Docker image is ready for "${runtime.envName}".`);
495
- }
496
- catch (error) {
497
- const message = error instanceof Error ? error.message : String(error);
498
- failTask(`Failed to refresh the Docker image for "${runtime.envName}".`);
499
- throw new Error(formatDockerDownloadFailure(runtime.envName, message));
500
- }
501
- }
502
- else {
503
- printInfo(`Skipping image download for "${runtime.envName}" (--skip-code-update).`);
504
- }
505
- if (containerExists) {
506
- startTask(`Stopping the current Docker app for "${runtime.envName}"...`);
507
- try {
508
- const state = await stopDockerContainer(runtime.containerName, {
509
- stdio: commandStdio,
510
- });
511
- succeedTask(state === 'already-stopped'
512
- ? `The current Docker app was already stopped for "${runtime.envName}".`
513
- : `Stopped the current Docker app for "${runtime.envName}".`);
514
- }
515
- catch (error) {
516
- stopTask();
517
- const message = error instanceof Error ? error.message : String(error);
518
- printInfo(`Could not stop the existing Docker container for "${runtime.envName}" cleanly. Continuing with container recreation. (${message})`);
519
- }
520
- }
521
- if (flags['skip-code-update'] && containerExists) {
522
- startTask(`Starting NocoBase for "${runtime.envName}" with the saved Docker image...`);
523
- try {
524
- const state = await startDockerContainer(runtime.containerName, {
525
- stdio: commandStdio,
526
- });
527
- succeedTask(state === 'already-running'
528
- ? `NocoBase is already running for "${runtime.envName}".`
529
- : `NocoBase is starting for "${runtime.envName}".`);
530
- }
531
- catch (error) {
532
- const message = error instanceof Error ? error.message : String(error);
533
- failTask(`Failed to start the Docker app for "${runtime.envName}".`);
534
- throw new Error(formatDockerStartFailure(runtime.envName, message));
535
- }
536
- }
537
- else {
538
- const plan = await AppUpgrade.buildDockerUpgradePlan(runtime, downloadVersion);
539
- const displayUrl = formatDisplayUrl(apiBaseUrl, plan.appPort);
540
- startTask(`Recreating the Docker app container for "${runtime.envName}"...`);
541
- try {
542
- if (containerExists) {
543
- await run('docker', ['rm', '-f', runtime.containerName], {
544
- errorName: 'docker rm',
545
- stdio: commandStdio,
546
- });
547
- }
548
- await ensureDockerNetwork(plan.networkName);
549
- await run('docker', plan.args, {
550
- errorName: 'docker run',
551
- stdio: commandStdio,
552
- });
553
- succeedTask(`Docker app container is ready for "${runtime.envName}".`);
554
- }
555
- catch (error) {
556
- const message = error instanceof Error ? error.message : String(error);
557
- failTask(`Failed to recreate the Docker app for "${runtime.envName}".`);
558
- throw new Error(formatDockerStartFailure(runtime.envName, message));
559
- }
560
- await waitForAppHealthCheck({
561
- envName: runtime.envName,
562
- apiBaseUrl,
563
- containerName: runtime.containerName,
564
- });
565
- return displayUrl;
566
- }
567
- await waitForAppHealthCheck({
568
- envName: runtime.envName,
569
- apiBaseUrl,
570
- containerName: runtime.containerName,
571
- });
572
- return formatDisplayUrl(apiBaseUrl, trimValue(runtime.env.appPort));
573
- }
574
360
  static async persistDownloadVersion(runtime, downloadVersion) {
575
361
  const { name: _name, ...envConfig } = runtime.env.config;
576
362
  try {
@@ -587,18 +373,8 @@ export default class AppUpgrade extends Command {
587
373
  async run() {
588
374
  const { flags } = await this.parse(AppUpgrade);
589
375
  const parsed = flags;
590
- const requestedEnv = parsed.env?.trim() || undefined;
591
- if (requestedEnv && hasExplicitEnvSelection(this.argv)) {
592
- const confirmed = await ensureCrossEnvConfirmed({
593
- command: this,
594
- requestedEnv,
595
- yes: parsed.yes,
596
- });
597
- if (!confirmed) {
598
- return;
599
- }
600
- }
601
- const commandStdio = parsed.verbose ? 'inherit' : 'ignore';
376
+ const requestedEnv = normalizeEnvName(parsed.env);
377
+ const explicitEnvSelection = Boolean(requestedEnv && hasExplicitEnvSelection(this.argv));
602
378
  const runtime = await resolveManagedAppRuntime(requestedEnv);
603
379
  if (!runtime) {
604
380
  this.error(formatMissingManagedAppEnvMessage(requestedEnv));
@@ -617,16 +393,123 @@ export default class AppUpgrade extends Command {
617
393
  'Use a local or Docker env if you need CLI-managed upgrades right now.',
618
394
  ].join('\n'));
619
395
  }
396
+ const interactiveTerminal = isInteractiveTerminal();
397
+ if (explicitEnvSelection) {
398
+ if (!interactiveTerminal) {
399
+ const currentEnv = normalizeEnvName(await getCurrentEnvName());
400
+ if (currentEnv && currentEnv !== requestedEnv) {
401
+ const missingYes = !parsed.yes;
402
+ const missingForce = !parsed.force;
403
+ if (missingYes || missingForce) {
404
+ this.error(formatUpgradeCrossEnvConfirmationRequiredMessage(currentEnv, runtime, parsed, {
405
+ missingYes,
406
+ missingForce,
407
+ }));
408
+ }
409
+ }
410
+ }
411
+ else {
412
+ const confirmed = await ensureCrossEnvConfirmed({
413
+ command: this,
414
+ requestedEnv,
415
+ yes: parsed.yes,
416
+ });
417
+ if (!confirmed) {
418
+ return;
419
+ }
420
+ }
421
+ }
422
+ if (!interactiveTerminal) {
423
+ if (!parsed.force) {
424
+ this.error(formatUpgradeForceRequiredMessage(runtime, parsed));
425
+ }
426
+ }
427
+ else if (!parsed.force) {
428
+ let confirmed = false;
429
+ try {
430
+ confirmed = await confirm({
431
+ message: `Upgrade "${runtime.envName}"? ${formatUpgradePromptSummary(parsed)}`,
432
+ default: false,
433
+ });
434
+ }
435
+ catch {
436
+ return;
437
+ }
438
+ if (!confirmed) {
439
+ return;
440
+ }
441
+ }
620
442
  announceTargetEnv(runtime.envName);
621
443
  try {
622
444
  const resolvedVersion = AppUpgrade.resolveUpgradeVersion(runtime, parsed);
445
+ const skipDownload = shouldSkipDownload(parsed);
623
446
  const runCommand = this.config.runCommand.bind(this.config);
624
- const displayUrl = runtime.kind === 'docker'
625
- ? await AppUpgrade.upgradeDocker(runCommand, runtime, resolvedVersion.downloadVersion, parsed, commandStdio)
626
- : await AppUpgrade.upgradeLocal(runCommand, runtime, resolvedVersion.downloadVersion, parsed, commandStdio);
447
+ await runWithSuppressedTargetEnvLog(async () => {
448
+ await runCommand('app:stop', buildManagedActionArgv(runtime.envName, parsed));
449
+ });
450
+ if (skipDownload) {
451
+ printInfo(`Skipping source download for "${runtime.envName}" (--skip-download).`);
452
+ printInfo(`Skipping commercial plugin sync for "${runtime.envName}" (--skip-download).`);
453
+ }
454
+ else if (runtime.kind === 'local' && runtime.source === 'local') {
455
+ printInfo(`Skipping source download for "${runtime.envName}" because this env is managed from an existing local app path.`);
456
+ }
457
+ else {
458
+ const downloadVersion = resolvedVersion.downloadVersion;
459
+ if (!downloadVersion) {
460
+ throw new Error(`Missing downloadVersion for "${runtime.envName}".`);
461
+ }
462
+ try {
463
+ if (runtime.kind === 'docker') {
464
+ await runCommand('source:download', AppUpgrade.buildDockerDownloadArgv(runtime, downloadVersion, {
465
+ verbose: parsed.verbose,
466
+ }));
467
+ }
468
+ else if (isDownloadableLocalRuntime(runtime)) {
469
+ await runCommand('source:download', AppUpgrade.buildLocalDownloadArgv(runtime, downloadVersion, {
470
+ verbose: parsed.verbose,
471
+ }));
472
+ }
473
+ else {
474
+ throw new Error(`Skipping source download for "${runtime.envName}" because this env is managed from an existing local app path.`);
475
+ }
476
+ }
477
+ catch (error) {
478
+ const message = error instanceof Error ? error.message : String(error);
479
+ if (runtime.kind === 'docker') {
480
+ throw new Error(formatDockerDownloadFailure(runtime.envName, message));
481
+ }
482
+ if (isDownloadableLocalRuntime(runtime)) {
483
+ throw new Error(formatLocalDownloadFailure(runtime.envName, runtime.source, message));
484
+ }
485
+ throw new Error(message);
486
+ }
487
+ }
488
+ if (!skipDownload) {
489
+ await runWithSuppressedTargetEnvLog(async () => {
490
+ await runCommand('license:plugins:sync', buildLicenseSyncArgv(runtime.envName, parsed, {
491
+ version: resolvedVersion.persistDownloadVersion,
492
+ }));
493
+ });
494
+ }
495
+ await runWithSuppressedTargetEnvLog(async () => {
496
+ await runWithSuppressedStartSuccessLog(async () => {
497
+ await runCommand('app:start', buildManagedActionArgv(runtime.envName, parsed, { quickstart: true }));
498
+ });
499
+ });
627
500
  if (resolvedVersion.persistDownloadVersion) {
628
501
  await AppUpgrade.persistDownloadVersion(runtime, resolvedVersion.persistDownloadVersion);
629
502
  }
503
+ try {
504
+ await runWithSuppressedTargetEnvLog(async () => {
505
+ await runCommand('env:update', buildEnvUpdateArgv(runtime.envName, parsed));
506
+ });
507
+ }
508
+ catch (error) {
509
+ const message = error instanceof Error ? error.message : String(error);
510
+ printWarning(formatEnvUpdateWarning(runtime.envName, message));
511
+ }
512
+ const displayUrl = formatDisplayUrl(runtime.env.baseUrl, runtime.env.appPort === undefined || runtime.env.appPort === null ? undefined : String(runtime.env.appPort));
630
513
  succeedTask(`NocoBase has been upgraded for "${runtime.envName}"${displayUrl ? ` at ${displayUrl}` : ''}.`);
631
514
  }
632
515
  catch (error) {