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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/bin/run.js +3 -2
  4. package/dist/commands/app/upgrade.js +38 -16
  5. package/dist/commands/backup/create.js +147 -0
  6. package/dist/commands/backup/index.js +20 -0
  7. package/dist/commands/backup/restore.js +105 -0
  8. package/dist/commands/config/delete.js +4 -0
  9. package/dist/commands/config/get.js +4 -0
  10. package/dist/commands/config/set.js +5 -1
  11. package/dist/commands/env/add.js +129 -15
  12. package/dist/commands/env/auth.js +145 -12
  13. package/dist/commands/env/info.js +52 -8
  14. package/dist/commands/env/list.js +2 -2
  15. package/dist/commands/env/shared.js +41 -3
  16. package/dist/commands/init.js +254 -136
  17. package/dist/commands/install.js +447 -272
  18. package/dist/commands/license/activate.js +6 -4
  19. package/dist/commands/source/publish.js +17 -0
  20. package/dist/commands/v1.js +210 -0
  21. package/dist/lib/app-managed-resources.js +20 -1
  22. package/dist/lib/app-runtime.js +13 -4
  23. package/dist/lib/auth-store.js +69 -18
  24. package/dist/lib/backup.js +171 -0
  25. package/dist/lib/bootstrap.js +23 -13
  26. package/dist/lib/cli-config.js +99 -4
  27. package/dist/lib/cli-locale.js +19 -7
  28. package/dist/lib/db-connection-check.js +61 -0
  29. package/dist/lib/env-auth.js +79 -0
  30. package/dist/lib/env-config.js +8 -1
  31. package/dist/lib/prompt-validators.js +23 -5
  32. package/dist/lib/prompt-web-ui.js +143 -19
  33. package/dist/lib/run-npm.js +166 -30
  34. package/dist/lib/skills-manager.js +74 -4
  35. package/dist/lib/source-publish.js +20 -1
  36. package/dist/lib/source-registry.js +2 -2
  37. package/dist/locale/en-US.json +36 -5
  38. package/dist/locale/zh-CN.json +36 -5
  39. package/package.json +6 -3
@@ -7,27 +7,26 @@
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 { spawn } from 'node:child_process';
11
10
  import crypto from 'node:crypto';
12
- import { mkdir } from 'node:fs/promises';
11
+ import { access, mkdir } from 'node:fs/promises';
13
12
  import path from 'node:path';
14
13
  import { exit } from 'node:process';
15
14
  import { runPromptCatalog, } from "../lib/prompt-catalog.js";
16
- import { applyCliLocale, localeText, resolveCliLocale, translateCli, } from "../lib/cli-locale.js";
15
+ import { applyCliLocale, localeText, resolveCliLocale, translateCli } from "../lib/cli-locale.js";
17
16
  import { resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRoot, resolveEnvRelativePath, } from '../lib/cli-home.js';
18
- import { defaultDockerContainerPrefix, defaultDockerNetworkName, } from '../lib/app-runtime.js';
19
- import { resolveDockerContainerPrefix, resolveDockerNetworkName, } from '../lib/cli-config.js';
20
- import { DEFAULT_DOCKER_VERSION, resolveDockerImageRef, } from "../lib/docker-image.js";
17
+ import { defaultDockerContainerPrefix, defaultDockerNetworkName } from '../lib/app-runtime.js';
18
+ import { resolveDockerContainerPrefix, resolveDockerNetworkName } from '../lib/cli-config.js';
19
+ import { DEFAULT_DOCKER_VERSION, resolveDockerImageRef } from "../lib/docker-image.js";
21
20
  import { findAvailableTcpPort, validateAvailableTcpPort, validateTcpPort, validateEnvKey, } from "../lib/prompt-validators.js";
22
- import { validateExternalDbConfig } from "../lib/db-connection-check.js";
21
+ import { validateExternalDbConfig, validateMysqlLowerCaseTableNamesCompatibility } from "../lib/db-connection-check.js";
23
22
  import { formatMissingManagedAppEnvMessage } from '../lib/app-runtime.js';
24
- import { run, runNocoBaseCommand } from '../lib/run-npm.js';
25
- import { printInfo, printStage, printVerbose, printWarning, setVerboseMode, } from '../lib/ui.js';
23
+ import { commandOutput, commandSucceeds, run, runNocoBaseCommand } from '../lib/run-npm.js';
24
+ import { printInfo, printStage, printVerbose, printWarning, setVerboseMode } from '../lib/ui.js';
26
25
  import { omitKeys, upperFirst } from "../lib/object-utils.js";
27
26
  import { getEnv, setCurrentEnv, upsertEnv } from '../lib/auth-store.js';
28
27
  import { buildStoredEnvConfig } from '../lib/env-config.js';
29
- import { resolveDockerEnvFileArg, } from "../lib/docker-env-file.js";
30
- import Download, { defaultDockerRegistryForLang, } from './download.js';
28
+ import { resolveDockerEnvFileArg } from "../lib/docker-env-file.js";
29
+ import Download, { defaultDockerRegistryForLang } from './download.js';
31
30
  import EnvAdd from "./env/add.js";
32
31
  const DEFAULT_INSTALL_ENV_NAME = 'local';
33
32
  const DEFAULT_INSTALL_LANG = 'en-US';
@@ -136,6 +135,20 @@ const INSTALL_LANGUAGE_OPTIONS = Object.entries(INSTALL_LANGUAGE_CODES).map(([va
136
135
  label: `${label} (${value})`,
137
136
  }));
138
137
  const installText = (key, values) => localeText(`commands.install.${key}`, values);
138
+ function formatDeferredAuthMessage(envName, authType) {
139
+ const normalizedAuthType = String(authType ?? '').trim();
140
+ const nextStep = `Authentication was skipped for env "${envName}". Run \`nb env auth ${envName}\` to finish setup.`;
141
+ if (normalizedAuthType === 'basic') {
142
+ return `${nextStep} You will be prompted for a username and password.`;
143
+ }
144
+ if (normalizedAuthType === 'token') {
145
+ return `${nextStep} You will be prompted for an access token.`;
146
+ }
147
+ if (normalizedAuthType === 'oauth') {
148
+ return `${nextStep} A browser sign-in flow will be started.`;
149
+ }
150
+ return nextStep;
151
+ }
139
152
  function argvHasToken(argv, tokens) {
140
153
  return tokens.some((t) => argv.includes(t));
141
154
  }
@@ -143,9 +156,7 @@ function isInstallDbDialect(value) {
143
156
  return INSTALL_DB_DIALECTS.includes(value);
144
157
  }
145
158
  function downloadVersionPromptValue(version) {
146
- return version === 'latest' || version === 'beta' || version === 'alpha'
147
- ? version
148
- : 'other';
159
+ return version === 'latest' || version === 'beta' || version === 'alpha' ? version : 'other';
149
160
  }
150
161
  function supportsBuiltinDbDialect(value) {
151
162
  const dialect = String(value ?? '').trim();
@@ -160,22 +171,16 @@ function defaultBuiltinDbImageForDialect(value) {
160
171
  const defaults = resolveCliLocale(process.env.NB_LOCALE) === 'zh-CN'
161
172
  ? DEFAULT_INSTALL_BUILTIN_DB_IMAGES_ZH_CN
162
173
  : DEFAULT_INSTALL_BUILTIN_DB_IMAGES;
163
- return supportsBuiltinDbDialect(dialect)
164
- ? defaults[dialect]
165
- : defaults.postgres;
174
+ return supportsBuiltinDbDialect(dialect) ? defaults[dialect] : defaults.postgres;
166
175
  }
167
176
  function defaultDbDatabaseForDialect(value) {
168
- return String(value ?? '').trim() === 'kingbase'
169
- ? 'kingbase'
170
- : DEFAULT_INSTALL_DB_DATABASE;
177
+ return String(value ?? '').trim() === 'kingbase' ? 'kingbase' : DEFAULT_INSTALL_DB_DATABASE;
171
178
  }
172
179
  function defaultDbHostForBuiltinDb(values) {
173
- return Boolean(values.builtinDb)
174
- ? DEFAULT_INSTALL_BUILTIN_DB_HOST
175
- : DEFAULT_INSTALL_DB_HOST;
180
+ return values.builtinDb ? DEFAULT_INSTALL_BUILTIN_DB_HOST : DEFAULT_INSTALL_DB_HOST;
176
181
  }
177
182
  function validateBuiltinDbEnabled(value, values) {
178
- if (!Boolean(value)) {
183
+ if (!value) {
179
184
  return undefined;
180
185
  }
181
186
  const dialect = String(values.dbDialect ?? 'postgres').trim() || 'postgres';
@@ -192,7 +197,14 @@ async function validateExternalDbPromptField(value, values) {
192
197
  if (typeof value === 'string' && value.trim() === '') {
193
198
  return undefined;
194
199
  }
195
- return await validateExternalDbConfig(values);
200
+ const connectionError = await validateExternalDbConfig(values);
201
+ if (connectionError) {
202
+ return connectionError;
203
+ }
204
+ if (!Object.prototype.hasOwnProperty.call(values, 'dbUnderscored')) {
205
+ return undefined;
206
+ }
207
+ return await validateMysqlLowerCaseTableNamesCompatibility(values);
196
208
  }
197
209
  function defaultInstallAppRootPath(envName) {
198
210
  const name = String(envName ?? DEFAULT_INSTALL_ENV_NAME).trim() || DEFAULT_INSTALL_ENV_NAME;
@@ -211,53 +223,39 @@ function pickPresetKeys(source, keys) {
211
223
  }
212
224
  return out;
213
225
  }
214
- async function commandSucceeds(command, args, options) {
215
- return await new Promise((resolve) => {
216
- const child = spawn(command, args, {
217
- cwd: options?.cwd,
218
- env: {
219
- ...process.env,
220
- ...options?.env,
221
- },
222
- stdio: 'ignore',
223
- });
224
- child.once('error', () => resolve(false));
225
- child.once('close', (code) => resolve(code === 0));
226
- });
226
+ function optionalEnvString(value) {
227
+ const text = String(value ?? '').trim();
228
+ return text || undefined;
227
229
  }
228
- async function commandOutput(command, args, options) {
229
- return await new Promise((resolve, reject) => {
230
- const child = spawn(command, args, {
231
- cwd: options?.cwd,
232
- env: {
233
- ...process.env,
234
- ...options?.env,
235
- },
236
- stdio: ['ignore', 'pipe', 'pipe'],
237
- });
238
- let stdout = '';
239
- let stderr = '';
240
- child.stdout.setEncoding('utf8');
241
- child.stderr.setEncoding('utf8');
242
- child.stdout.on('data', (chunk) => {
243
- stdout += chunk;
244
- });
245
- child.stderr.on('data', (chunk) => {
246
- stderr += chunk;
247
- });
248
- child.once('error', reject);
249
- child.once('close', (code, signal) => {
250
- if (code === 0) {
251
- resolve(stdout);
252
- return;
253
- }
254
- if (signal) {
255
- reject(new Error(`${command} exited due to signal ${signal}`));
256
- return;
257
- }
258
- reject(new Error(`${command} exited with code ${code}: ${stderr.trim()}`));
259
- });
260
- });
230
+ function optionalEnvBoolean(value) {
231
+ if (value === undefined || value === null) {
232
+ return undefined;
233
+ }
234
+ return Boolean(value);
235
+ }
236
+ function pushOptionalEnvArg(args, key, value) {
237
+ if (typeof value === 'string') {
238
+ if (!value) {
239
+ return;
240
+ }
241
+ args.push('-e', `${key}=${value}`);
242
+ return;
243
+ }
244
+ if (typeof value === 'boolean') {
245
+ args.push('-e', `${key}=${String(value)}`);
246
+ }
247
+ }
248
+ function setOptionalEnvVar(out, key, value) {
249
+ if (typeof value === 'string') {
250
+ if (!value) {
251
+ return;
252
+ }
253
+ out[key] = value;
254
+ return;
255
+ }
256
+ if (typeof value === 'boolean') {
257
+ out[key] = String(value);
258
+ }
261
259
  }
262
260
  export default class Install extends Command {
263
261
  ensuredDockerNetworks = new Set();
@@ -279,7 +277,7 @@ export default class Install extends Command {
279
277
  '<%= config.bin %> <%= command.id %> --env app1 --root-nickname "Super Admin"',
280
278
  '<%= config.bin %> <%= command.id %> --env myenv --app-root-path=./myenv/source/ --storage-path=./myenv/storage/',
281
279
  '<%= config.bin %> <%= command.id %> --env dev -y --app-root-path=./dev/source/',
282
- '<%= config.bin %> <%= command.id %> --env dev -y --fetch-source --app-root-path=./dev/source/',
280
+ '<%= config.bin %> <%= command.id %> --env dev -y --skip-download --source git --app-root-path=./dev/source/',
283
281
  ];
284
282
  static flags = {
285
283
  yes: Flags.boolean({
@@ -314,14 +312,24 @@ export default class Install extends Command {
314
312
  }),
315
313
  'auth-type': Flags.string({
316
314
  char: 'a',
317
- description: 'Authentication: token (API key) or oauth (browser login via `nb env auth`)',
318
- options: ['token', 'oauth'],
315
+ description: 'Authentication: basic (username/password login), token (API key), or oauth (browser login via `nb env auth`)',
316
+ options: ['basic', 'token', 'oauth'],
319
317
  }),
320
318
  'access-token': Flags.string({
321
319
  char: 't',
322
320
  aliases: ['token'],
323
321
  description: 'API key or access token when using --auth-type token',
324
322
  }),
323
+ username: Flags.string({
324
+ description: 'Username when using --auth-type basic',
325
+ }),
326
+ password: Flags.string({
327
+ description: 'Password when using --auth-type basic',
328
+ }),
329
+ 'skip-auth': Flags.boolean({
330
+ description: 'Save the env auth mode now and finish authentication later with `nb env auth`',
331
+ default: false,
332
+ }),
325
333
  lang: Flags.string({ description: 'Language for the installed NocoBase app', char: 'l', required: false }),
326
334
  force: Flags.boolean({
327
335
  description: 'Reconfigure an existing env and replace conflicting runtime resources when needed',
@@ -380,8 +388,19 @@ export default class Install extends Command {
380
388
  'db-password': Flags.string({
381
389
  description: 'Database password for the app',
382
390
  }),
383
- 'fetch-source': Flags.boolean({
384
- description: 'Download NocoBase app files or pull a Docker image before installing',
391
+ 'db-schema': Flags.string({
392
+ description: 'Database schema for the app',
393
+ }),
394
+ 'db-table-prefix': Flags.string({
395
+ description: 'Database table prefix for the app',
396
+ }),
397
+ 'db-underscored': Flags.boolean({
398
+ allowNo: true,
399
+ description: 'Use underscored database naming for the app',
400
+ default: false,
401
+ }),
402
+ 'skip-download': Flags.boolean({
403
+ description: 'Skip the download step and reuse an existing local app directory or Docker image',
385
404
  default: false,
386
405
  }),
387
406
  ...omitKeys(Download.flags, ['yes']),
@@ -428,12 +447,6 @@ export default class Install extends Command {
428
447
  placeholder: installText('prompts.storagePath.placeholder'),
429
448
  initialValue: (values) => defaultInstallStoragePath(values.env ?? values.appName),
430
449
  },
431
- fetchSource: {
432
- type: 'boolean',
433
- message: installText('prompts.fetchSource.message'),
434
- initialValue: true,
435
- yesInitialValue: true,
436
- },
437
450
  };
438
451
  static dbPrompts = {
439
452
  dbDialect: {
@@ -461,8 +474,7 @@ export default class Install extends Command {
461
474
  message: installText('prompts.builtinDbImage.message'),
462
475
  placeholder: installText('prompts.builtinDbImage.placeholder'),
463
476
  initialValue: (values) => defaultBuiltinDbImageForDialect(values.dbDialect),
464
- hidden: (values) => !Boolean(values.builtinDb)
465
- || !supportsBuiltinDbDialect(values.dbDialect),
477
+ hidden: (values) => !values.builtinDb || !supportsBuiltinDbDialect(values.dbDialect),
466
478
  required: true,
467
479
  },
468
480
  dbHost: {
@@ -482,8 +494,7 @@ export default class Install extends Command {
482
494
  initialValue: (values) => defaultDbPortForDialect(values.dbDialect),
483
495
  required: true,
484
496
  validate: Install.validateDbPort,
485
- hidden: (values) => Boolean(values.builtinDb)
486
- && String(values.source ?? '').trim() === 'docker',
497
+ hidden: (values) => Boolean(values.builtinDb) && String(values.source ?? '').trim() === 'docker',
487
498
  },
488
499
  dbDatabase: {
489
500
  type: 'text',
@@ -508,6 +519,27 @@ export default class Install extends Command {
508
519
  required: true,
509
520
  validate: validateExternalDbPromptField,
510
521
  },
522
+ dbSchema: {
523
+ type: 'text',
524
+ message: installText('prompts.dbSchema.message'),
525
+ placeholder: installText('prompts.dbSchema.placeholder'),
526
+ hidden: (values) => String(values.dbDialect ?? '').trim() !== 'postgres',
527
+ },
528
+ dbTablePrefix: {
529
+ type: 'text',
530
+ message: installText('prompts.dbTablePrefix.message'),
531
+ placeholder: installText('prompts.dbTablePrefix.placeholder'),
532
+ },
533
+ dbUnderscored: {
534
+ type: 'boolean',
535
+ message: installText('prompts.dbUnderscored.message'),
536
+ initialValue: false,
537
+ yesInitialValue: false,
538
+ validate: (value, values) => validateExternalDbPromptField(value, {
539
+ ...values,
540
+ dbUnderscored: Boolean(value),
541
+ }),
542
+ },
511
543
  };
512
544
  static rootUserPrompts = {
513
545
  rootUsername: {
@@ -602,9 +634,8 @@ export default class Install extends Command {
602
634
  * Booleans with defaults are only preset when the user passed the flag on argv (see `download`).
603
635
  * Does not include `env` — use {@link buildEnvPresetValuesFromFlags} for {@link Install.envPrompts}.
604
636
  */
605
- static buildPresetValuesFromFlags(flags) {
637
+ static buildPresetValuesFromFlags(flags, argv = process.argv.slice(2)) {
606
638
  const preset = {};
607
- const argv = process.argv.slice(2);
608
639
  const apiBaseUrl = Install.toOptionalPromptString(flags['api-base-url']);
609
640
  if (apiBaseUrl) {
610
641
  preset.apiBaseUrl = apiBaseUrl;
@@ -621,9 +652,18 @@ export default class Install extends Command {
621
652
  preset.authType = authType;
622
653
  }
623
654
  }
655
+ if (flags['skip-auth']) {
656
+ preset.skipAuth = true;
657
+ }
624
658
  if (flags['access-token'] !== undefined || flags.token !== undefined) {
625
659
  preset.accessToken = String(flags['access-token'] ?? flags.token ?? '');
626
660
  }
661
+ if (flags.username !== undefined) {
662
+ preset.username = String(flags.username ?? '').trim();
663
+ }
664
+ if (flags.password !== undefined) {
665
+ preset.password = String(flags.password ?? '');
666
+ }
627
667
  if (flags.lang !== undefined) {
628
668
  const v = String(flags.lang).trim();
629
669
  if (v) {
@@ -663,8 +703,15 @@ export default class Install extends Command {
663
703
  if (flags['root-nickname'] !== undefined) {
664
704
  preset.rootNickname = String(flags['root-nickname'] ?? '').trim();
665
705
  }
666
- if (argvHasToken(argv, ['--fetch-source'])) {
667
- preset.fetchSource = flags['fetch-source'];
706
+ if (argvHasToken(argv, ['--skip-download'])) {
707
+ preset.skipDownload = flags['skip-download'];
708
+ if (flags['skip-download']) {
709
+ preset.dockerSave = false;
710
+ preset.replace = false;
711
+ preset.devDependencies = false;
712
+ preset.build = false;
713
+ preset.buildDts = false;
714
+ }
668
715
  }
669
716
  if (argvHasToken(argv, ['--builtin-db', '--no-builtin-db'])) {
670
717
  preset.builtinDb = flags['builtin-db'];
@@ -709,20 +756,34 @@ export default class Install extends Command {
709
756
  if (flags['db-password'] !== undefined) {
710
757
  preset.dbPassword = String(flags['db-password'] ?? '');
711
758
  }
759
+ if (flags['db-schema'] !== undefined) {
760
+ const v = String(flags['db-schema'] ?? '').trim();
761
+ if (v) {
762
+ preset.dbSchema = v;
763
+ }
764
+ }
765
+ if (flags['db-table-prefix'] !== undefined) {
766
+ const v = String(flags['db-table-prefix'] ?? '').trim();
767
+ if (v) {
768
+ preset.dbTablePrefix = v;
769
+ }
770
+ }
771
+ if (argvHasToken(argv, ['--db-underscored', '--no-db-underscored'])) {
772
+ preset.dbUnderscored = flags['db-underscored'];
773
+ }
712
774
  return preset;
713
775
  }
714
- static buildAppPresetValuesFromFlags(flags) {
715
- return pickPresetKeys(Install.buildPresetValuesFromFlags(flags), [
776
+ static buildAppPresetValuesFromFlags(flags, argv = process.argv.slice(2)) {
777
+ return pickPresetKeys(Install.buildPresetValuesFromFlags(flags, argv), [
716
778
  'lang',
717
779
  'force',
718
780
  'appRootPath',
719
781
  'appPort',
720
782
  'storagePath',
721
- 'fetchSource',
722
783
  ]);
723
784
  }
724
- static buildDbPresetValuesFromFlags(flags) {
725
- return pickPresetKeys(Install.buildPresetValuesFromFlags(flags), [
785
+ static buildDbPresetValuesFromFlags(flags, argv = process.argv.slice(2)) {
786
+ return pickPresetKeys(Install.buildPresetValuesFromFlags(flags, argv), [
726
787
  'builtinDb',
727
788
  'dbDialect',
728
789
  'builtinDbImage',
@@ -731,23 +792,49 @@ export default class Install extends Command {
731
792
  'dbDatabase',
732
793
  'dbUser',
733
794
  'dbPassword',
795
+ 'dbSchema',
796
+ 'dbTablePrefix',
797
+ 'dbUnderscored',
734
798
  ]);
735
799
  }
736
- static buildRootPresetValuesFromFlags(flags) {
737
- return pickPresetKeys(Install.buildPresetValuesFromFlags(flags), [
800
+ static buildRootPresetValuesFromFlags(flags, argv = process.argv.slice(2)) {
801
+ return pickPresetKeys(Install.buildPresetValuesFromFlags(flags, argv), [
738
802
  'rootUsername',
739
803
  'rootEmail',
740
804
  'rootPassword',
741
805
  'rootNickname',
742
806
  ]);
743
807
  }
744
- static buildEnvAddPresetValuesFromFlags(flags) {
745
- return pickPresetKeys(Install.buildPresetValuesFromFlags(flags), [
808
+ static buildEnvAddPresetValuesFromFlags(flags, argv = process.argv.slice(2)) {
809
+ return pickPresetKeys(Install.buildPresetValuesFromFlags(flags, argv), [
746
810
  'apiBaseUrl',
747
811
  'authType',
812
+ 'username',
813
+ 'password',
748
814
  'accessToken',
749
815
  ]);
750
816
  }
817
+ buildEnvAddPromptsForInstall(parsed) {
818
+ const apiBaseUrlPrompt = {
819
+ ...EnvAdd.prompts.apiBaseUrl,
820
+ validate: undefined,
821
+ };
822
+ const prompts = {
823
+ ...EnvAdd.prompts,
824
+ apiBaseUrl: apiBaseUrlPrompt,
825
+ };
826
+ if (!parsed['skip-auth']) {
827
+ return prompts;
828
+ }
829
+ const accessTokenPrompt = {
830
+ ...EnvAdd.prompts.accessToken,
831
+ hidden: () => true,
832
+ };
833
+ return {
834
+ ...prompts,
835
+ accessToken: accessTokenPrompt,
836
+ };
837
+ }
751
838
  static toOptionalPromptString(value) {
752
839
  if (value === undefined || value === null) {
753
840
  return undefined;
@@ -813,9 +900,13 @@ export default class Install extends Command {
813
900
  if (validationError) {
814
901
  throw new Error(validationError);
815
902
  }
903
+ const compatibilityError = await validateMysqlLowerCaseTableNamesCompatibility(dbResults);
904
+ if (compatibilityError) {
905
+ throw new Error(compatibilityError);
906
+ }
816
907
  }
817
908
  static async readResumePortValidationContext(values) {
818
- if (!Boolean(values.resume)) {
909
+ if (!values.resume) {
819
910
  return undefined;
820
911
  }
821
912
  const envName = Install.toOptionalPromptString(values.env);
@@ -846,8 +937,7 @@ export default class Install extends Command {
846
937
  }
847
938
  static async isResumeManagedPortReuse(params) {
848
939
  if (params.target === 'app') {
849
- if ((params.context.source === 'npm' || params.context.source === 'git')
850
- && params.context.appRootPath) {
940
+ if ((params.context.source === 'npm' || params.context.source === 'git') && params.context.appRootPath) {
851
941
  return await Install.isLocalPm2ProcessUsingPort(params.context.appRootPath, params.port);
852
942
  }
853
943
  const containerName = Install.buildDockerAppContainerName(params.context.envName, params.context.dockerContainerPrefix);
@@ -863,19 +953,13 @@ export default class Install extends Command {
863
953
  if (!containerName || !port) {
864
954
  return false;
865
955
  }
866
- const exists = await commandSucceeds('docker', [
867
- 'container',
868
- 'inspect',
869
- containerName,
870
- ]);
956
+ const exists = await commandSucceeds('docker', ['container', 'inspect', containerName]);
871
957
  if (!exists) {
872
958
  return false;
873
959
  }
874
960
  try {
875
961
  const output = await commandOutput('docker', ['port', containerName]);
876
- return output
877
- .split(/\r?\n/)
878
- .some((line) => line.includes(`:${port}`));
962
+ return output.split(/\r?\n/).some((line) => line.includes(`:${port}`));
879
963
  }
880
964
  catch {
881
965
  return false;
@@ -917,39 +1001,34 @@ export default class Install extends Command {
917
1001
  const dbDatabase = Install.toOptionalPromptString(config.dbDatabase);
918
1002
  const dbUser = Install.toOptionalPromptString(config.dbUser);
919
1003
  const dbPassword = Install.toOptionalPromptString(config.dbPassword);
1004
+ const dbSchema = Install.toOptionalPromptString(config.dbSchema);
1005
+ const dbTablePrefix = Install.toOptionalPromptString(config.dbTablePrefix);
1006
+ const dbUnderscored = typeof config.dbUnderscored === 'boolean' ? config.dbUnderscored : undefined;
920
1007
  const builtinDbImage = Install.toOptionalPromptString(config.builtinDbImage);
921
1008
  const rootUsername = Install.toOptionalPromptString(config.rootUsername);
922
1009
  const rootEmail = Install.toOptionalPromptString(config.rootEmail);
923
1010
  const rootPassword = Install.toOptionalPromptString(config.rootPassword);
924
1011
  const rootNickname = Install.toOptionalPromptString(config.rootNickname);
925
1012
  const auth = config.auth;
1013
+ const savedAuthType = Install.toOptionalPromptString(config.authType) ?? Install.toOptionalPromptString(auth?.type);
926
1014
  const appPreset = {
927
1015
  ...(appRootPath ? { appRootPath } : {}),
928
1016
  ...(appPort ? { appPort } : {}),
929
1017
  ...(storagePath ? { storagePath } : {}),
930
- ...(source
931
- ? { fetchSource: true }
932
- : appRootPath
933
- ? { fetchSource: false }
934
- : {}),
935
1018
  };
936
1019
  const downloadPreset = {
937
1020
  ...(source ? { source } : {}),
938
1021
  ...(downloadVersion
939
1022
  ? {
940
1023
  version: downloadVersionPromptValue(downloadVersion),
941
- ...(downloadVersionPromptValue(downloadVersion) === 'other'
942
- ? { otherVersion: downloadVersion }
943
- : {}),
1024
+ ...(downloadVersionPromptValue(downloadVersion) === 'other' ? { otherVersion: downloadVersion } : {}),
944
1025
  }
945
1026
  : {}),
946
1027
  ...(dockerRegistry ? { dockerRegistry } : {}),
947
1028
  ...(dockerPlatform ? { dockerPlatform } : {}),
948
1029
  ...(gitUrl ? { gitUrl } : {}),
949
1030
  ...(npmRegistry ? { npmRegistry } : {}),
950
- ...(typeof config.devDependencies === 'boolean'
951
- ? { devDependencies: config.devDependencies }
952
- : {}),
1031
+ ...(typeof config.devDependencies === 'boolean' ? { devDependencies: config.devDependencies } : {}),
953
1032
  ...(typeof config.build === 'boolean' ? { build: config.build } : {}),
954
1033
  ...(typeof config.buildDts === 'boolean' ? { buildDts: config.buildDts } : {}),
955
1034
  };
@@ -962,6 +1041,9 @@ export default class Install extends Command {
962
1041
  ...(dbDatabase ? { dbDatabase } : {}),
963
1042
  ...(dbUser ? { dbUser } : {}),
964
1043
  ...(dbPassword ? { dbPassword } : {}),
1044
+ ...(dbSchema ? { dbSchema } : {}),
1045
+ ...(dbTablePrefix ? { dbTablePrefix } : {}),
1046
+ ...(dbUnderscored !== undefined ? { dbUnderscored } : {}),
965
1047
  };
966
1048
  const rootPreset = {
967
1049
  ...(rootUsername ? { rootUsername } : {}),
@@ -970,13 +1052,20 @@ export default class Install extends Command {
970
1052
  ...(rootNickname ? { rootNickname } : {}),
971
1053
  };
972
1054
  const envAddPreset = {};
973
- if (auth?.type === 'token') {
1055
+ if (savedAuthType === 'token') {
974
1056
  envAddPreset.authType = 'token';
975
1057
  if (Install.toOptionalPromptString(auth.accessToken)) {
976
1058
  envAddPreset.accessToken = String(auth.accessToken);
977
1059
  }
978
1060
  }
979
- else if (auth?.type === 'oauth') {
1061
+ else if (savedAuthType === 'basic') {
1062
+ envAddPreset.authType = 'basic';
1063
+ const authUsername = Install.toOptionalPromptString(config.authUsername) ?? rootUsername;
1064
+ if (authUsername) {
1065
+ envAddPreset.username = authUsername;
1066
+ }
1067
+ }
1068
+ else if (savedAuthType === 'oauth') {
980
1069
  envAddPreset.authType = 'oauth';
981
1070
  }
982
1071
  return {
@@ -1060,8 +1149,7 @@ export default class Install extends Command {
1060
1149
  }
1061
1150
  static shouldPublishBuiltinDbPortForValues(values) {
1062
1151
  const builtinDb = values.builtinDb === undefined ? true : Boolean(values.builtinDb);
1063
- return builtinDb
1064
- && Install.shouldPublishBuiltinDbPort(values.source);
1152
+ return builtinDb && Install.shouldPublishBuiltinDbPort(values.source);
1065
1153
  }
1066
1154
  static async buildDbPromptInitialValues(params) {
1067
1155
  if (params.flags['db-port'] !== undefined) {
@@ -1117,9 +1205,8 @@ export default class Install extends Command {
1117
1205
  * Resolve the effective preset `values` for the embedded download step.
1118
1206
  * Explicit download flags win; otherwise `-y` falls back to the docker + alpha quickstart path.
1119
1207
  */
1120
- static buildDownloadPresetValuesForInstall(flags, appResults, envName, yes) {
1208
+ static buildDownloadPresetValuesForInstall(flags, appResults, envName, yes, argv = process.argv.slice(2)) {
1121
1209
  const preset = {};
1122
- const argv = process.argv.slice(2);
1123
1210
  const appRoot = String(appResults.appRootPath ?? '').trim() || defaultInstallAppRootPath(envName);
1124
1211
  const lang = String(appResults.lang ?? DEFAULT_INSTALL_LANG).trim() || DEFAULT_INSTALL_LANG;
1125
1212
  preset.lang = lang;
@@ -1158,8 +1245,7 @@ export default class Install extends Command {
1158
1245
  }
1159
1246
  }
1160
1247
  if (flags['npm-registry'] !== undefined) {
1161
- preset.npmRegistry =
1162
- typeof flags['npm-registry'] === 'string' ? flags['npm-registry'] : '';
1248
+ preset.npmRegistry = typeof flags['npm-registry'] === 'string' ? flags['npm-registry'] : '';
1163
1249
  }
1164
1250
  if (flags.resume && !argvHasToken(argv, ['--replace', '-r'])) {
1165
1251
  preset.replace = true;
@@ -1203,15 +1289,11 @@ export default class Install extends Command {
1203
1289
  }
1204
1290
  static buildBuiltinDbContainerPrefix(containerPrefix) {
1205
1291
  const storedName = String(containerPrefix ?? '').trim();
1206
- return storedName
1207
- ? Install.sanitizeDockerResourceName(storedName)
1208
- : Install.defaultDockerContainerPrefix();
1292
+ return storedName ? Install.sanitizeDockerResourceName(storedName) : Install.defaultDockerContainerPrefix();
1209
1293
  }
1210
1294
  static buildManagedDockerNetworkName(networkName) {
1211
1295
  const storedName = String(networkName ?? '').trim();
1212
- return storedName
1213
- ? Install.sanitizeDockerResourceName(storedName)
1214
- : Install.defaultDockerNetworkName();
1296
+ return storedName ? Install.sanitizeDockerResourceName(storedName) : Install.defaultDockerNetworkName();
1215
1297
  }
1216
1298
  static buildBuiltinDbNetworkName(envName, networkName) {
1217
1299
  void envName;
@@ -1244,25 +1326,19 @@ export default class Install extends Command {
1244
1326
  }
1245
1327
  static buildBuiltinDbPlan(params) {
1246
1328
  const dbDialect = String(params.dbDialect ?? 'postgres').trim() || 'postgres';
1247
- const dbPort = String(params.dbPort ?? defaultDbPortForDialect(dbDialect)).trim()
1248
- || defaultDbPortForDialect(dbDialect);
1329
+ const dbPort = String(params.dbPort ?? defaultDbPortForDialect(dbDialect)).trim() || defaultDbPortForDialect(dbDialect);
1249
1330
  const defaultDbDatabase = defaultDbDatabaseForDialect(dbDialect);
1250
1331
  const networkName = Install.buildBuiltinDbNetworkName(params.envName, params.dockerNetworkName ?? params.workspaceName);
1251
1332
  const containerName = Install.buildBuiltinDbContainerName(params.envName, dbDialect, params.dockerContainerPrefix ?? params.workspaceName);
1252
1333
  const dbHostInput = String(params.dbHost ?? '').trim();
1253
1334
  const dbHost = Install.shouldPublishBuiltinDbPort(params.source)
1254
- ? (dbHostInput
1255
- && dbHostInput !== DEFAULT_INSTALL_BUILTIN_DB_HOST
1256
- && dbHostInput !== containerName
1335
+ ? dbHostInput && dbHostInput !== DEFAULT_INSTALL_BUILTIN_DB_HOST && dbHostInput !== containerName
1257
1336
  ? dbHostInput
1258
- : DEFAULT_INSTALL_DB_HOST)
1259
- : (dbHostInput
1260
- && dbHostInput !== DEFAULT_INSTALL_DB_HOST
1261
- && dbHostInput !== 'localhost'
1337
+ : DEFAULT_INSTALL_DB_HOST
1338
+ : dbHostInput && dbHostInput !== DEFAULT_INSTALL_DB_HOST && dbHostInput !== 'localhost'
1262
1339
  ? dbHostInput
1263
- : containerName);
1264
- const storagePath = resolveConfiguredEnvPath(params.storagePath)
1265
- ?? resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1340
+ : containerName;
1341
+ const storagePath = resolveConfiguredEnvPath(params.storagePath) ?? resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1266
1342
  if (dbDialect === 'postgres') {
1267
1343
  const image = String(params.builtinDbImage ?? '').trim() || defaultBuiltinDbImageForDialect(dbDialect);
1268
1344
  const dataDir = path.resolve(storagePath, 'db', 'postgres');
@@ -1293,12 +1369,9 @@ export default class Install extends Command {
1293
1369
  dbDialect,
1294
1370
  dbHost,
1295
1371
  dbPort,
1296
- dbDatabase: String(params.dbDatabase ?? defaultDbDatabase).trim()
1297
- || defaultDbDatabase,
1298
- dbUser: String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim()
1299
- || DEFAULT_INSTALL_DB_USER,
1300
- dbPassword: String(params.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD)
1301
- || DEFAULT_INSTALL_DB_PASSWORD,
1372
+ dbDatabase: String(params.dbDatabase ?? defaultDbDatabase).trim() || defaultDbDatabase,
1373
+ dbUser: String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER,
1374
+ dbPassword: String(params.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD,
1302
1375
  networkName,
1303
1376
  containerName,
1304
1377
  dataDir,
@@ -1484,11 +1557,7 @@ export default class Install extends Command {
1484
1557
  }
1485
1558
  }
1486
1559
  async dockerContainerExists(name) {
1487
- return await commandSucceeds('docker', [
1488
- 'container',
1489
- 'inspect',
1490
- name,
1491
- ]);
1560
+ return await commandSucceeds('docker', ['container', 'inspect', name]);
1492
1561
  }
1493
1562
  async removeDockerContainer(name) {
1494
1563
  await run('docker', ['rm', '-f', name], {
@@ -1538,8 +1607,7 @@ export default class Install extends Command {
1538
1607
  });
1539
1608
  }
1540
1609
  async startBuiltinDb(params) {
1541
- const storagePath = String(params.appResults.storagePath ?? '').trim()
1542
- || defaultInstallStoragePath(params.envName);
1610
+ const storagePath = String(params.appResults.storagePath ?? '').trim() || defaultInstallStoragePath(params.envName);
1543
1611
  const plan = Install.buildBuiltinDbPlan({
1544
1612
  envName: params.envName,
1545
1613
  workspaceName: params.workspaceName,
@@ -1577,25 +1645,25 @@ export default class Install extends Command {
1577
1645
  return plan;
1578
1646
  }
1579
1647
  static async buildDockerAppPlan(params) {
1580
- const dockerRegistry = String(downloadResultsValue(params.downloadResults, 'dockerRegistry') ?? '').trim()
1581
- || defaultDockerRegistryForLang(process.env.NB_LOCALE);
1648
+ const dockerRegistry = String(downloadResultsValue(params.downloadResults, 'dockerRegistry') ?? '').trim() ||
1649
+ defaultDockerRegistryForLang(process.env.NB_LOCALE);
1582
1650
  const version = String(downloadResultsValue(params.downloadResults, 'version') ?? '').trim() || DEFAULT_DOCKER_VERSION;
1583
1651
  const imageRef = resolveDockerImageRef(dockerRegistry, version, {
1584
1652
  defaultRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
1585
1653
  defaultVersion: DEFAULT_DOCKER_VERSION,
1586
1654
  });
1587
1655
  const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
1588
- const storagePath = resolveConfiguredEnvPath(String(params.appResults.storagePath ?? '').trim()
1589
- || defaultInstallStoragePath(params.envName))
1590
- ?? resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1656
+ const storagePath = resolveConfiguredEnvPath(String(params.appResults.storagePath ?? '').trim() || defaultInstallStoragePath(params.envName)) ?? resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1591
1657
  const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
1592
1658
  const dbHost = String(params.dbResults.dbHost ?? DEFAULT_INSTALL_DB_HOST).trim() || DEFAULT_INSTALL_DB_HOST;
1593
- const dbPort = String(params.dbResults.dbPort ?? defaultDbPortForDialect(dbDialect)).trim()
1594
- || defaultDbPortForDialect(dbDialect);
1595
- const dbDatabase = String(params.dbResults.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim()
1596
- || DEFAULT_INSTALL_DB_DATABASE;
1659
+ const dbPort = String(params.dbResults.dbPort ?? defaultDbPortForDialect(dbDialect)).trim() ||
1660
+ defaultDbPortForDialect(dbDialect);
1661
+ const dbDatabase = String(params.dbResults.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim() || DEFAULT_INSTALL_DB_DATABASE;
1597
1662
  const dbUser = String(params.dbResults.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER;
1598
1663
  const dbPassword = String(params.dbResults.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD;
1664
+ const dbSchema = optionalEnvString(params.dbResults.dbSchema);
1665
+ const dbTablePrefix = optionalEnvString(params.dbResults.dbTablePrefix);
1666
+ const dbUnderscored = optionalEnvBoolean(params.dbResults.dbUnderscored);
1599
1667
  const appKey = crypto.randomBytes(32).toString('hex');
1600
1668
  const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
1601
1669
  const containerName = Install.buildDockerAppContainerName(params.envName, params.dockerContainerPrefix ?? params.workspaceName);
@@ -1623,7 +1691,11 @@ export default class Install extends Command {
1623
1691
  for (const [key, value] of Object.entries(initEnvVars)) {
1624
1692
  args.push('-e', `${key}=${value}`);
1625
1693
  }
1626
- 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}:/app/nocobase/storage`, imageRef);
1694
+ 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}:/app/nocobase/storage`);
1695
+ pushOptionalEnvArg(args, 'DB_SCHEMA', dbSchema);
1696
+ pushOptionalEnvArg(args, 'DB_TABLE_PREFIX', dbTablePrefix);
1697
+ pushOptionalEnvArg(args, 'DB_UNDERSCORED', dbUnderscored);
1698
+ args.push(imageRef);
1627
1699
  return {
1628
1700
  source: 'docker',
1629
1701
  networkName: params.networkName,
@@ -1651,8 +1723,8 @@ export default class Install extends Command {
1651
1723
  return 'created';
1652
1724
  }
1653
1725
  async installDockerApp(params) {
1654
- const networkName = params.builtinDbPlan?.networkName
1655
- ?? Install.buildBuiltinDbNetworkName(params.envName, params.dockerNetworkName ?? params.workspaceName);
1726
+ const networkName = params.builtinDbPlan?.networkName ??
1727
+ Install.buildBuiltinDbNetworkName(params.envName, params.dockerNetworkName ?? params.workspaceName);
1656
1728
  await this.ensureDockerNetwork(networkName);
1657
1729
  const plan = await Install.buildDockerAppPlan({
1658
1730
  envName: params.envName,
@@ -1699,26 +1771,26 @@ export default class Install extends Command {
1699
1771
  Install.pushDownloadArgIfValue(argv, '--source', results.source);
1700
1772
  Install.pushDownloadArgIfValue(argv, '--version', downloadResultsValue(results, 'version'));
1701
1773
  Install.pushDownloadArgIfValue(argv, '--output-dir', source === 'npm' || source === 'git'
1702
- ? (resolveConfiguredEnvPath(results.outputDir)
1703
- ?? resolveConfiguredEnvPath(String(results.outputDir ?? '').trim() || defaultInstallAppRootPath(results.env)))
1774
+ ? resolveConfiguredEnvPath(results.outputDir) ??
1775
+ resolveConfiguredEnvPath(String(results.outputDir ?? '').trim() || defaultInstallAppRootPath(results.env))
1704
1776
  : results.outputDir);
1705
1777
  Install.pushDownloadArgIfValue(argv, '--git-url', results.gitUrl);
1706
1778
  Install.pushDownloadArgIfValue(argv, '--docker-registry', results.dockerRegistry);
1707
1779
  Install.pushDownloadArgIfValue(argv, '--docker-platform', results.dockerPlatform);
1708
1780
  Install.pushDownloadArgIfValue(argv, '--npm-registry', results.npmRegistry);
1709
- if (Boolean(results.replace)) {
1781
+ if (results.replace) {
1710
1782
  argv.push('--replace');
1711
1783
  }
1712
- if (Boolean(results.devDependencies)) {
1784
+ if (results.devDependencies) {
1713
1785
  argv.push('--dev-dependencies');
1714
1786
  }
1715
- if (Boolean(results.dockerSave)) {
1787
+ if (results.dockerSave) {
1716
1788
  argv.push('--docker-save');
1717
1789
  }
1718
- if (results.build !== undefined && !Boolean(results.build)) {
1790
+ if (results.build !== undefined && !results.build) {
1719
1791
  argv.push('--no-build');
1720
1792
  }
1721
- if (Boolean(results.buildDts)) {
1793
+ if (results.buildDts) {
1722
1794
  argv.push('--build-dts');
1723
1795
  }
1724
1796
  return argv;
@@ -1728,15 +1800,26 @@ export default class Install extends Command {
1728
1800
  if (projectRoot) {
1729
1801
  return projectRoot;
1730
1802
  }
1731
- const outputDir = String(params.downloadResults.outputDir ?? '').trim()
1732
- || String(params.appResults.appRootPath ?? '').trim()
1733
- || defaultInstallAppRootPath(params.envName);
1803
+ const outputDir = String(params.downloadResults.outputDir ?? '').trim() ||
1804
+ String(params.appResults.appRootPath ?? '').trim() ||
1805
+ defaultInstallAppRootPath(params.envName);
1734
1806
  return resolveConfiguredEnvPath(outputDir) ?? resolveEnvRelativePath(defaultInstallAppRootPath(params.envName));
1735
1807
  }
1736
1808
  static resolveLocalProjectConfigPath(params) {
1737
- return (String(params.downloadResults.outputDir ?? '').trim()
1738
- || String(params.appResults.appRootPath ?? '').trim()
1739
- || defaultInstallAppRootPath(params.envName));
1809
+ return (String(params.downloadResults.outputDir ?? '').trim() ||
1810
+ String(params.appResults.appRootPath ?? '').trim() ||
1811
+ defaultInstallAppRootPath(params.envName));
1812
+ }
1813
+ static buildSkipDownloadValues(envName, appResults) {
1814
+ const appRoot = String(appResults.appRootPath ?? '').trim() || defaultInstallAppRootPath(envName);
1815
+ return {
1816
+ outputDir: appRoot,
1817
+ replace: false,
1818
+ dockerSave: false,
1819
+ devDependencies: false,
1820
+ build: false,
1821
+ buildDts: false,
1822
+ };
1740
1823
  }
1741
1824
  commandStdio(verbose) {
1742
1825
  return verbose ? 'inherit' : 'ignore';
@@ -1746,7 +1829,41 @@ export default class Install extends Command {
1746
1829
  verbose: params.verbose,
1747
1830
  compactLog: true,
1748
1831
  });
1749
- return await this.config.runCommand('source:download', argv);
1832
+ return (await this.config.runCommand('source:download', argv));
1833
+ }
1834
+ async ensureSkippedDownloadInputsReady(params) {
1835
+ if (params.source === 'docker') {
1836
+ const dockerRegistry = String(downloadResultsValue(params.downloadResults, 'dockerRegistry') ?? '').trim() ||
1837
+ defaultDockerRegistryForLang(process.env.NB_LOCALE);
1838
+ const version = String(downloadResultsValue(params.downloadResults, 'version') ?? '').trim() || DEFAULT_DOCKER_VERSION;
1839
+ const imageRef = resolveDockerImageRef(dockerRegistry, version, {
1840
+ defaultRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
1841
+ defaultVersion: DEFAULT_DOCKER_VERSION,
1842
+ });
1843
+ const imageExists = await commandSucceeds('docker', ['image', 'inspect', imageRef]);
1844
+ if (!imageExists) {
1845
+ throw new Error(translateCli('commands.install.messages.skipDownloadDockerImageMissing', {
1846
+ imageRef,
1847
+ }));
1848
+ }
1849
+ return;
1850
+ }
1851
+ if (params.source === 'npm' || params.source === 'git') {
1852
+ const projectRoot = Install.resolveLocalProjectRoot({
1853
+ envName: params.envName,
1854
+ appResults: params.appResults,
1855
+ downloadResults: params.downloadResults,
1856
+ });
1857
+ try {
1858
+ await access(projectRoot);
1859
+ await access(path.join(projectRoot, 'package.json'));
1860
+ }
1861
+ catch {
1862
+ throw new Error(translateCli('commands.install.messages.skipDownloadLocalAppMissing', {
1863
+ projectRoot,
1864
+ }));
1865
+ }
1866
+ }
1750
1867
  }
1751
1868
  async downloadLocalApp(params) {
1752
1869
  const result = await this.downloadManagedSource({
@@ -1767,36 +1884,32 @@ export default class Install extends Command {
1767
1884
  return downloadedProjectRoot;
1768
1885
  }
1769
1886
  static buildLocalAppEnvVars(params) {
1770
- const configuredStoragePath = String(params.appResults.storagePath ?? '').trim()
1771
- || defaultInstallStoragePath(params.envName);
1772
- const storagePath = resolveConfiguredEnvPath(configuredStoragePath)
1773
- ?? resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1774
- const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim()
1775
- || 'postgres';
1887
+ const configuredStoragePath = String(params.appResults.storagePath ?? '').trim() || defaultInstallStoragePath(params.envName);
1888
+ const storagePath = resolveConfiguredEnvPath(configuredStoragePath) ??
1889
+ resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1890
+ const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
1776
1891
  const appKey = crypto.randomBytes(32).toString('hex');
1777
1892
  const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
1778
1893
  const env = {
1779
1894
  STORAGE_PATH: storagePath,
1780
- APP_PORT: String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim()
1781
- || DEFAULT_INSTALL_APP_PORT,
1895
+ APP_PORT: String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT,
1782
1896
  APP_KEY: appKey,
1783
1897
  TZ: timeZone,
1784
1898
  DB_DIALECT: dbDialect,
1785
- DB_HOST: String(params.dbResults.dbHost ?? DEFAULT_INSTALL_DB_HOST).trim()
1786
- || DEFAULT_INSTALL_DB_HOST,
1787
- DB_PORT: String(params.dbResults.dbPort ?? defaultDbPortForDialect(dbDialect)).trim()
1788
- || defaultDbPortForDialect(dbDialect),
1789
- DB_DATABASE: String(params.dbResults.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim()
1790
- || DEFAULT_INSTALL_DB_DATABASE,
1791
- DB_USER: String(params.dbResults.dbUser ?? DEFAULT_INSTALL_DB_USER).trim()
1792
- || DEFAULT_INSTALL_DB_USER,
1793
- DB_PASSWORD: String(params.dbResults.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD)
1794
- || DEFAULT_INSTALL_DB_PASSWORD,
1899
+ DB_HOST: String(params.dbResults.dbHost ?? DEFAULT_INSTALL_DB_HOST).trim() || DEFAULT_INSTALL_DB_HOST,
1900
+ DB_PORT: String(params.dbResults.dbPort ?? defaultDbPortForDialect(dbDialect)).trim() ||
1901
+ defaultDbPortForDialect(dbDialect),
1902
+ DB_DATABASE: String(params.dbResults.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim() || DEFAULT_INSTALL_DB_DATABASE,
1903
+ DB_USER: String(params.dbResults.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER,
1904
+ DB_PASSWORD: String(params.dbResults.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD,
1795
1905
  ...Install.buildInitAppEnvVars({
1796
1906
  appResults: params.appResults,
1797
1907
  rootResults: params.rootResults,
1798
1908
  }),
1799
1909
  };
1910
+ setOptionalEnvVar(env, 'DB_SCHEMA', optionalEnvString(params.dbResults.dbSchema));
1911
+ setOptionalEnvVar(env, 'DB_TABLE_PREFIX', optionalEnvString(params.dbResults.dbTablePrefix));
1912
+ setOptionalEnvVar(env, 'DB_UNDERSCORED', optionalEnvBoolean(params.dbResults.dbUnderscored));
1800
1913
  return env;
1801
1914
  }
1802
1915
  async startLocalApp(params) {
@@ -1838,10 +1951,8 @@ export default class Install extends Command {
1838
1951
  };
1839
1952
  }
1840
1953
  static resolveApiBaseUrl(params) {
1841
- const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim()
1842
- || DEFAULT_INSTALL_APP_PORT;
1843
- return (String(params.envAddResults.apiBaseUrl ?? '').trim()
1844
- || `http://127.0.0.1:${appPort}/api`);
1954
+ const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
1955
+ return String(params.envAddResults.apiBaseUrl ?? '').trim() || `http://127.0.0.1:${appPort}/api`;
1845
1956
  }
1846
1957
  static buildHealthCheckUrl(apiBaseUrl) {
1847
1958
  return `${apiBaseUrl.replace(/\/+$/, '')}/__health_check`;
@@ -1870,9 +1981,7 @@ export default class Install extends Command {
1870
1981
  const body = Install.formatHealthCheckMessage(text);
1871
1982
  return {
1872
1983
  ok: response.ok && text.trim().toLowerCase() === 'ok',
1873
- message: response.ok
1874
- ? `HTTP ${response.status}: ${body}`
1875
- : `HTTP ${response.status}: ${body}`,
1984
+ message: response.ok ? `HTTP ${response.status}: ${body}` : `HTTP ${response.status}: ${body}`,
1876
1985
  };
1877
1986
  }
1878
1987
  catch (error) {
@@ -1936,28 +2045,42 @@ export default class Install extends Command {
1936
2045
  if (!params.appReady) {
1937
2046
  return;
1938
2047
  }
1939
- const authType = String(params.envAddResults.authType ?? 'oauth').trim()
1940
- || 'oauth';
2048
+ const authType = String(params.envAddResults.authType ?? 'oauth').trim() || 'oauth';
2049
+ if (params.skipAuth) {
2050
+ printInfo(formatDeferredAuthMessage(params.envName, authType));
2051
+ return;
2052
+ }
1941
2053
  if (authType === 'oauth') {
1942
2054
  await this.config.runCommand('env:auth', [params.envName]);
1943
2055
  }
2056
+ else if (authType === 'basic') {
2057
+ const authArgv = [params.envName, '--auth-type', 'basic'];
2058
+ const username = String(params.envAddResults.username ?? '').trim();
2059
+ const password = String(params.envAddResults.password ?? '');
2060
+ if (username) {
2061
+ authArgv.push('--username', username);
2062
+ }
2063
+ if (password) {
2064
+ authArgv.push('--password', password);
2065
+ }
2066
+ await this.config.runCommand('env:auth', authArgv);
2067
+ }
1944
2068
  await this.config.runCommand('env:update', [params.envName]);
1945
2069
  }
1946
2070
  static buildSavedEnvConfig(params) {
1947
- const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim()
1948
- || DEFAULT_INSTALL_APP_PORT;
1949
- const storagePath = String(params.appResults.storagePath ?? '').trim()
1950
- || defaultInstallStoragePath(params.envName);
2071
+ const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
2072
+ const storagePath = String(params.appResults.storagePath ?? '').trim() || defaultInstallStoragePath(params.envName);
1951
2073
  const envFile = String(params.appResults.envFile ?? '').trim() || undefined;
1952
2074
  const apiBaseUrl = Install.resolveApiBaseUrl({
1953
2075
  appResults: params.appResults,
1954
2076
  envAddResults: params.envAddResults,
1955
2077
  });
1956
- const authType = String(params.envAddResults.authType ?? 'oauth').trim()
1957
- || 'oauth';
2078
+ const authType = String(params.envAddResults.authType ?? 'oauth').trim() || 'oauth';
2079
+ const authUsername = authType === 'basic' ? String(params.envAddResults.username ?? params.rootResults.rootUsername ?? '').trim() : '';
1958
2080
  return buildStoredEnvConfig({
1959
2081
  apiBaseUrl,
1960
2082
  authType,
2083
+ ...(authUsername ? { authUsername } : {}),
1961
2084
  accessToken: params.envAddResults.accessToken,
1962
2085
  source: downloadResultsValue(params.downloadResults, 'source'),
1963
2086
  downloadVersion: downloadResultsValue(params.downloadResults, 'version'),
@@ -1982,6 +2105,9 @@ export default class Install extends Command {
1982
2105
  dbDatabase: params.dbResults.dbDatabase,
1983
2106
  dbUser: params.dbResults.dbUser,
1984
2107
  dbPassword: params.dbResults.dbPassword,
2108
+ dbSchema: params.dbResults.dbSchema,
2109
+ dbTablePrefix: params.dbResults.dbTablePrefix,
2110
+ dbUnderscored: params.dbResults.dbUnderscored,
1985
2111
  rootUsername: params.rootResults.rootUsername,
1986
2112
  rootEmail: params.rootResults.rootEmail,
1987
2113
  rootPassword: params.rootResults.rootPassword,
@@ -1989,6 +2115,7 @@ export default class Install extends Command {
1989
2115
  });
1990
2116
  }
1991
2117
  async collectPromptResults(parsed, yes) {
2118
+ const commandArgv = this.argv ?? process.argv.slice(2);
1992
2119
  const resumePreset = await this.resolveResumePresetValues(parsed, yes);
1993
2120
  const envPreset = {
1994
2121
  ...(resumePreset?.envPreset ?? {}),
@@ -2004,7 +2131,7 @@ export default class Install extends Command {
2004
2131
  const envName = String(envResults.env ?? '').trim() || DEFAULT_INSTALL_ENV_NAME;
2005
2132
  const appPreset = {
2006
2133
  ...(resumePreset?.appPreset ?? {}),
2007
- ...Install.buildAppPresetValuesFromFlags(parsed),
2134
+ ...Install.buildAppPresetValuesFromFlags(parsed, commandArgv),
2008
2135
  };
2009
2136
  const appCatalog = Install.buildAppPromptsCatalog(envName, {
2010
2137
  resume: parsed.resume,
@@ -2014,52 +2141,58 @@ export default class Install extends Command {
2014
2141
  envName,
2015
2142
  flags: {
2016
2143
  ...parsed,
2017
- 'app-root-path': parsed['app-root-path']
2018
- ?? Install.toOptionalPromptString(appPreset.appRootPath),
2019
- 'app-port': parsed['app-port']
2020
- ?? Install.toOptionalPromptString(appPreset.appPort),
2021
- 'storage-path': parsed['storage-path']
2022
- ?? Install.toOptionalPromptString(appPreset.storagePath),
2144
+ 'app-root-path': parsed['app-root-path'] ?? Install.toOptionalPromptString(appPreset.appRootPath),
2145
+ 'app-port': parsed['app-port'] ?? Install.toOptionalPromptString(appPreset.appPort),
2146
+ 'storage-path': parsed['storage-path'] ?? Install.toOptionalPromptString(appPreset.storagePath),
2023
2147
  },
2024
2148
  }),
2025
2149
  values: appPreset,
2026
2150
  yesInitialValues: { resume: parsed.resume },
2027
2151
  yes,
2028
2152
  });
2029
- let downloadResults = {};
2030
- if (Boolean(appResults.fetchSource)) {
2031
- const downloadOpts = Install.buildDownloadPromptOptionsForInstall(appResults, envName);
2032
- downloadOpts.values = {
2033
- ...(resumePreset?.downloadPreset ?? {}),
2034
- ...downloadOpts.values,
2035
- ...Install.buildDownloadPresetValuesForInstall(parsed, appResults, envName, yes),
2036
- };
2037
- downloadOpts.yes = yes;
2038
- downloadResults = await runPromptCatalog(Download.prompts, downloadOpts);
2153
+ const downloadOpts = Install.buildDownloadPromptOptionsForInstall(appResults, envName);
2154
+ downloadOpts.values = {
2155
+ ...(resumePreset?.downloadPreset ?? {}),
2156
+ ...downloadOpts.values,
2157
+ ...Install.buildDownloadPresetValuesForInstall(parsed, appResults, envName, yes, commandArgv),
2158
+ ...(parsed['skip-download'] ? Install.buildSkipDownloadValues(envName, appResults) : {}),
2159
+ };
2160
+ downloadOpts.yes = yes;
2161
+ const downloadResults = await runPromptCatalog(Download.prompts, downloadOpts);
2162
+ if (parsed['skip-download']) {
2163
+ delete downloadResults.outputDir;
2164
+ delete downloadResults.replace;
2165
+ delete downloadResults.dockerSave;
2166
+ delete downloadResults.devDependencies;
2167
+ delete downloadResults.build;
2168
+ delete downloadResults.buildDts;
2039
2169
  }
2040
2170
  const dbPreset = {
2041
2171
  ...(resumePreset?.dbPreset ?? {}),
2042
- ...Install.buildDbPresetValuesFromFlags(parsed),
2172
+ ...Install.buildDbPresetValuesFromFlags(parsed, commandArgv),
2043
2173
  };
2044
- const dbResults = await runPromptCatalog(Install.buildDbPromptsCatalog(envName, downloadResults, {
2174
+ const promptedDbResults = await runPromptCatalog(Install.buildDbPromptsCatalog(envName, downloadResults, {
2045
2175
  resume: parsed.resume,
2046
2176
  }), {
2047
2177
  initialValues: {
2048
2178
  ...downloadResults,
2049
- ...await Install.buildDbPromptInitialValues({
2179
+ ...(await Install.buildDbPromptInitialValues({
2050
2180
  flags: {
2051
2181
  ...parsed,
2052
- 'db-port': parsed['db-port']
2053
- ?? Install.toOptionalPromptString(dbPreset.dbPort),
2182
+ 'db-port': parsed['db-port'] ?? Install.toOptionalPromptString(dbPreset.dbPort),
2054
2183
  },
2055
2184
  downloadResults,
2056
2185
  dbPreset,
2057
- }),
2186
+ })),
2058
2187
  },
2059
2188
  values: dbPreset,
2060
2189
  yes,
2061
2190
  });
2062
- const rootPreset = Install.buildRootPresetValuesFromFlags(parsed);
2191
+ const dbResults = {
2192
+ ...pickPresetKeys(dbPreset, ['dbSchema', 'dbTablePrefix', 'dbUnderscored']),
2193
+ ...promptedDbResults,
2194
+ };
2195
+ const rootPreset = Install.buildRootPresetValuesFromFlags(parsed, commandArgv);
2063
2196
  const rootResults = await runPromptCatalog(Install.rootUserPrompts, {
2064
2197
  initialValues: {},
2065
2198
  values: {
@@ -2068,24 +2201,45 @@ export default class Install extends Command {
2068
2201
  },
2069
2202
  yes,
2070
2203
  });
2071
- const envAddPromptsForInstall = {
2072
- ...EnvAdd.prompts,
2073
- apiBaseUrl: {
2074
- ...EnvAdd.prompts.apiBaseUrl,
2075
- validate: undefined,
2076
- },
2204
+ const envAddPromptsForInstall = this.buildEnvAddPromptsForInstall(parsed);
2205
+ const envAddResumePreset = resumePreset?.envAddPreset ?? {};
2206
+ const envAddFlagValues = Install.buildEnvAddPresetValuesFromFlags(parsed, commandArgv);
2207
+ const envAddPreset = {
2208
+ ...envAddResumePreset,
2209
+ ...envAddFlagValues,
2077
2210
  };
2078
- const envAddResults = await runPromptCatalog(envAddPromptsForInstall, {
2079
- initialValues: {
2080
- apiBaseUrl: `http://127.0.0.1:${appResults.appPort ?? DEFAULT_INSTALL_APP_PORT}/api`,
2081
- },
2211
+ const resolvedEnvAddAuthType = String(envAddPreset.authType ?? '').trim();
2212
+ const envAddInitialValues = {
2213
+ apiBaseUrl: `http://127.0.0.1:${appResults.appPort ?? DEFAULT_INSTALL_APP_PORT}/api`,
2214
+ ...envAddResumePreset,
2215
+ ...(!parsed['skip-auth'] && resolvedEnvAddAuthType === 'basic'
2216
+ ? {
2217
+ ...(!Object.prototype.hasOwnProperty.call(envAddResumePreset, 'username') &&
2218
+ !Object.prototype.hasOwnProperty.call(envAddFlagValues, 'username') &&
2219
+ rootResults.rootUsername !== undefined
2220
+ ? { username: String(rootResults.rootUsername).trim() }
2221
+ : {}),
2222
+ ...(!Object.prototype.hasOwnProperty.call(envAddFlagValues, 'password') &&
2223
+ rootResults.rootPassword !== undefined
2224
+ ? { password: String(rootResults.rootPassword ?? '') }
2225
+ : {}),
2226
+ }
2227
+ : {}),
2228
+ };
2229
+ const promptedEnvAddResults = await runPromptCatalog(envAddPromptsForInstall, {
2230
+ initialValues: envAddInitialValues,
2082
2231
  values: {
2083
2232
  name: envName,
2084
- ...(resumePreset?.envAddPreset ?? {}),
2085
- ...Install.buildEnvAddPresetValuesFromFlags(parsed),
2233
+ ...(parsed['skip-auth'] ? { skipAuth: true } : {}),
2234
+ ...envAddFlagValues,
2086
2235
  },
2087
2236
  yes,
2088
2237
  });
2238
+ const envAddResults = {
2239
+ ...pickPresetKeys(envAddInitialValues, ['username', 'password']),
2240
+ ...promptedEnvAddResults,
2241
+ ...pickPresetKeys(envAddFlagValues, ['username', 'password']),
2242
+ };
2089
2243
  return {
2090
2244
  envName,
2091
2245
  envResults,
@@ -2103,6 +2257,9 @@ export default class Install extends Command {
2103
2257
  const parsed = {
2104
2258
  ...flags,
2105
2259
  };
2260
+ if (parsed['skip-auth'] && (parsed['access-token'] !== undefined || parsed.token !== undefined)) {
2261
+ this.error('--skip-auth cannot be used with --access-token or --token.');
2262
+ }
2106
2263
  setVerboseMode(Boolean(parsed.verbose));
2107
2264
  const commandStdio = this.commandStdio(parsed.verbose);
2108
2265
  if (!parsed['no-intro']) {
@@ -2115,16 +2272,23 @@ export default class Install extends Command {
2115
2272
  : 'Resuming setup from the saved workspace config');
2116
2273
  }
2117
2274
  const promptResults = await this.collectPromptResults(parsed, flags.yes);
2118
- const { envName, appResults, downloadResults, dbResults, rootResults, envAddResults, } = promptResults;
2275
+ const { envName, appResults, downloadResults, dbResults, rootResults, envAddResults } = promptResults;
2119
2276
  const source = String(downloadResultsValue(downloadResults, 'source') ?? '').trim();
2120
- const usesDockerResources = Boolean(dbResults.builtinDb)
2121
- || (Boolean(appResults.fetchSource) && source === 'docker');
2277
+ const usesDockerResources = Boolean(dbResults.builtinDb) || source === 'docker';
2122
2278
  const dockerNetworkName = usesDockerResources
2123
2279
  ? await resolveDockerNetworkName({ scope: resolveDefaultConfigScope() })
2124
2280
  : undefined;
2125
2281
  const dockerContainerPrefix = usesDockerResources
2126
2282
  ? await resolveDockerContainerPrefix({ scope: resolveDefaultConfigScope() })
2127
2283
  : undefined;
2284
+ if (parsed['skip-download']) {
2285
+ await this.ensureSkippedDownloadInputsReady({
2286
+ source,
2287
+ envName,
2288
+ appResults,
2289
+ downloadResults,
2290
+ });
2291
+ }
2128
2292
  await Install.ensureExternalDbReadyForInstall(dbResults);
2129
2293
  if (!parsed.resume) {
2130
2294
  if (!parsed['skip-save-env-log']) {
@@ -2143,7 +2307,7 @@ export default class Install extends Command {
2143
2307
  }
2144
2308
  }
2145
2309
  let builtinDbPlan;
2146
- if (Boolean(dbResults.builtinDb)) {
2310
+ if (dbResults.builtinDb) {
2147
2311
  builtinDbPlan = await this.startBuiltinDb({
2148
2312
  envName,
2149
2313
  dockerNetworkName,
@@ -2163,14 +2327,16 @@ export default class Install extends Command {
2163
2327
  }
2164
2328
  let dockerAppPlan;
2165
2329
  let localAppPlan;
2166
- if (Boolean(appResults.fetchSource)) {
2330
+ if (source === 'docker' || source === 'npm' || source === 'git') {
2167
2331
  this.logStage('Preparing application');
2168
2332
  if (source === 'docker') {
2169
- await this.downloadManagedSource({
2170
- downloadResults,
2171
- verbose: parsed.verbose,
2172
- });
2173
- printInfo('Application image ready.');
2333
+ if (!parsed['skip-download']) {
2334
+ await this.downloadManagedSource({
2335
+ downloadResults,
2336
+ verbose: parsed.verbose,
2337
+ });
2338
+ printInfo('Application image ready.');
2339
+ }
2174
2340
  dockerAppPlan = await this.installDockerApp({
2175
2341
  envName,
2176
2342
  dockerNetworkName,
@@ -2188,13 +2354,21 @@ export default class Install extends Command {
2188
2354
  }
2189
2355
  else if (source === 'npm' || source === 'git') {
2190
2356
  const localSource = source === 'npm' ? 'npm' : 'git';
2191
- const projectRoot = await this.downloadLocalApp({
2192
- envName,
2193
- appResults,
2194
- downloadResults,
2195
- verbose: parsed.verbose,
2196
- });
2197
- printInfo('Application files ready.');
2357
+ const projectRoot = parsed['skip-download']
2358
+ ? Install.resolveLocalProjectRoot({
2359
+ envName,
2360
+ appResults,
2361
+ downloadResults,
2362
+ })
2363
+ : await this.downloadLocalApp({
2364
+ envName,
2365
+ appResults,
2366
+ downloadResults,
2367
+ verbose: parsed.verbose,
2368
+ });
2369
+ if (!parsed['skip-download']) {
2370
+ printInfo('Application files ready.');
2371
+ }
2198
2372
  localAppPlan = await this.startLocalApp({
2199
2373
  envName,
2200
2374
  source: localSource,
@@ -2235,6 +2409,7 @@ export default class Install extends Command {
2235
2409
  envName,
2236
2410
  envAddResults,
2237
2411
  appReady: Boolean(dockerAppPlan || localAppPlan),
2412
+ skipAuth: Boolean(parsed['skip-auth']),
2238
2413
  });
2239
2414
  if (!dockerAppPlan && !localAppPlan) {
2240
2415
  printInfo(`Install config for "${envName}" has been saved.`);