@nocobase/cli 2.1.0-alpha.40 → 2.1.0-alpha.46

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 (56) hide show
  1. package/README.md +20 -8
  2. package/README.zh-CN.md +20 -8
  3. package/bin/run.js +9 -37
  4. package/dist/commands/app/destroy.js +225 -0
  5. package/dist/commands/app/down.js +24 -254
  6. package/dist/commands/app/restart.js +12 -5
  7. package/dist/commands/app/shared.js +122 -0
  8. package/dist/commands/app/start.js +41 -59
  9. package/dist/commands/app/stop.js +72 -26
  10. package/dist/commands/app/upgrade.js +312 -429
  11. package/dist/commands/config/delete.js +8 -1
  12. package/dist/commands/config/get.js +5 -0
  13. package/dist/commands/config/set.js +10 -1
  14. package/dist/commands/db/ps.js +5 -18
  15. package/dist/commands/db/start.js +23 -8
  16. package/dist/commands/env/add.js +66 -6
  17. package/dist/commands/env/auth.js +86 -27
  18. package/dist/commands/env/info.js +52 -8
  19. package/dist/commands/env/list.js +6 -7
  20. package/dist/commands/env/shared.js +41 -3
  21. package/dist/commands/env/status.js +10 -7
  22. package/dist/commands/init.js +214 -136
  23. package/dist/commands/install.js +364 -294
  24. package/dist/commands/license/plugins/shared.js +9 -3
  25. package/dist/commands/license/plugins/sync.js +54 -25
  26. package/dist/commands/self/update.js +58 -1
  27. package/dist/commands/source/dev.js +5 -5
  28. package/dist/commands/source/download.js +29 -25
  29. package/dist/generated/command-registry.js +3 -2
  30. package/dist/lib/api-client.js +6 -0
  31. package/dist/lib/api-command-compat.js +641 -0
  32. package/dist/lib/app-health.js +31 -18
  33. package/dist/lib/app-managed-resources.js +1 -1
  34. package/dist/lib/app-runtime.js +2 -2
  35. package/dist/lib/auth-store.js +56 -19
  36. package/dist/lib/backup.js +3 -3
  37. package/dist/lib/bootstrap.js +14 -8
  38. package/dist/lib/cli-config.js +137 -4
  39. package/dist/lib/cli-entry-error.js +44 -0
  40. package/dist/lib/cli-locale.js +19 -7
  41. package/dist/lib/db-connection-check.js +61 -0
  42. package/dist/lib/docker-log-stream.js +45 -0
  43. package/dist/lib/env-auth.js +98 -11
  44. package/dist/lib/env-config.js +3 -2
  45. package/dist/lib/env-guard.js +1 -1
  46. package/dist/lib/generated-command.js +17 -0
  47. package/dist/lib/prompt-validators.js +23 -5
  48. package/dist/lib/prompt-web-ui.js +143 -19
  49. package/dist/lib/run-npm.js +133 -23
  50. package/dist/lib/skills-manager.js +80 -4
  51. package/dist/lib/source-publish.js +4 -1
  52. package/dist/lib/startup-update.js +74 -50
  53. package/dist/lib/ui.js +4 -1
  54. package/dist/locale/en-US.json +92 -5
  55. package/dist/locale/zh-CN.json +92 -5
  56. package/package.json +34 -2
@@ -7,27 +7,27 @@
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 { startDockerLogFollower } from '../lib/docker-log-stream.js';
30
+ import Download, { defaultDockerRegistryForLang } from './download.js';
31
31
  import EnvAdd from "./env/add.js";
32
32
  const DEFAULT_INSTALL_ENV_NAME = 'local';
33
33
  const DEFAULT_INSTALL_LANG = 'en-US';
@@ -139,6 +139,9 @@ const installText = (key, values) => localeText(`commands.install.${key}`, value
139
139
  function formatDeferredAuthMessage(envName, authType) {
140
140
  const normalizedAuthType = String(authType ?? '').trim();
141
141
  const nextStep = `Authentication was skipped for env "${envName}". Run \`nb env auth ${envName}\` to finish setup.`;
142
+ if (normalizedAuthType === 'basic') {
143
+ return `${nextStep} You will be prompted for a username and password.`;
144
+ }
142
145
  if (normalizedAuthType === 'token') {
143
146
  return `${nextStep} You will be prompted for an access token.`;
144
147
  }
@@ -154,9 +157,7 @@ function isInstallDbDialect(value) {
154
157
  return INSTALL_DB_DIALECTS.includes(value);
155
158
  }
156
159
  function downloadVersionPromptValue(version) {
157
- return version === 'latest' || version === 'beta' || version === 'alpha'
158
- ? version
159
- : 'other';
160
+ return version === 'latest' || version === 'beta' || version === 'alpha' ? version : 'other';
160
161
  }
161
162
  function supportsBuiltinDbDialect(value) {
162
163
  const dialect = String(value ?? '').trim();
@@ -171,22 +172,16 @@ function defaultBuiltinDbImageForDialect(value) {
171
172
  const defaults = resolveCliLocale(process.env.NB_LOCALE) === 'zh-CN'
172
173
  ? DEFAULT_INSTALL_BUILTIN_DB_IMAGES_ZH_CN
173
174
  : DEFAULT_INSTALL_BUILTIN_DB_IMAGES;
174
- return supportsBuiltinDbDialect(dialect)
175
- ? defaults[dialect]
176
- : defaults.postgres;
175
+ return supportsBuiltinDbDialect(dialect) ? defaults[dialect] : defaults.postgres;
177
176
  }
178
177
  function defaultDbDatabaseForDialect(value) {
179
- return String(value ?? '').trim() === 'kingbase'
180
- ? 'kingbase'
181
- : DEFAULT_INSTALL_DB_DATABASE;
178
+ return String(value ?? '').trim() === 'kingbase' ? 'kingbase' : DEFAULT_INSTALL_DB_DATABASE;
182
179
  }
183
180
  function defaultDbHostForBuiltinDb(values) {
184
- return Boolean(values.builtinDb)
185
- ? DEFAULT_INSTALL_BUILTIN_DB_HOST
186
- : DEFAULT_INSTALL_DB_HOST;
181
+ return values.builtinDb ? DEFAULT_INSTALL_BUILTIN_DB_HOST : DEFAULT_INSTALL_DB_HOST;
187
182
  }
188
183
  function validateBuiltinDbEnabled(value, values) {
189
- if (!Boolean(value)) {
184
+ if (!value) {
190
185
  return undefined;
191
186
  }
192
187
  const dialect = String(values.dbDialect ?? 'postgres').trim() || 'postgres';
@@ -203,7 +198,14 @@ async function validateExternalDbPromptField(value, values) {
203
198
  if (typeof value === 'string' && value.trim() === '') {
204
199
  return undefined;
205
200
  }
206
- return await validateExternalDbConfig(values);
201
+ const connectionError = await validateExternalDbConfig(values);
202
+ if (connectionError) {
203
+ return connectionError;
204
+ }
205
+ if (!Object.prototype.hasOwnProperty.call(values, 'dbUnderscored')) {
206
+ return undefined;
207
+ }
208
+ return await validateMysqlLowerCaseTableNamesCompatibility(values);
207
209
  }
208
210
  function defaultInstallAppRootPath(envName) {
209
211
  const name = String(envName ?? DEFAULT_INSTALL_ENV_NAME).trim() || DEFAULT_INSTALL_ENV_NAME;
@@ -222,54 +224,6 @@ function pickPresetKeys(source, keys) {
222
224
  }
223
225
  return out;
224
226
  }
225
- async function commandSucceeds(command, args, options) {
226
- return await new Promise((resolve) => {
227
- const child = spawn(command, args, {
228
- cwd: options?.cwd,
229
- env: {
230
- ...process.env,
231
- ...options?.env,
232
- },
233
- stdio: 'ignore',
234
- });
235
- child.once('error', () => resolve(false));
236
- child.once('close', (code) => resolve(code === 0));
237
- });
238
- }
239
- async function commandOutput(command, args, options) {
240
- return await new Promise((resolve, reject) => {
241
- const child = spawn(command, args, {
242
- cwd: options?.cwd,
243
- env: {
244
- ...process.env,
245
- ...options?.env,
246
- },
247
- stdio: ['ignore', 'pipe', 'pipe'],
248
- });
249
- let stdout = '';
250
- let stderr = '';
251
- child.stdout.setEncoding('utf8');
252
- child.stderr.setEncoding('utf8');
253
- child.stdout.on('data', (chunk) => {
254
- stdout += chunk;
255
- });
256
- child.stderr.on('data', (chunk) => {
257
- stderr += chunk;
258
- });
259
- child.once('error', reject);
260
- child.once('close', (code, signal) => {
261
- if (code === 0) {
262
- resolve(stdout);
263
- return;
264
- }
265
- if (signal) {
266
- reject(new Error(`${command} exited due to signal ${signal}`));
267
- return;
268
- }
269
- reject(new Error(`${command} exited with code ${code}: ${stderr.trim()}`));
270
- });
271
- });
272
- }
273
227
  function optionalEnvString(value) {
274
228
  const text = String(value ?? '').trim();
275
229
  return text || undefined;
@@ -324,7 +278,7 @@ export default class Install extends Command {
324
278
  '<%= config.bin %> <%= command.id %> --env app1 --root-nickname "Super Admin"',
325
279
  '<%= config.bin %> <%= command.id %> --env myenv --app-root-path=./myenv/source/ --storage-path=./myenv/storage/',
326
280
  '<%= config.bin %> <%= command.id %> --env dev -y --app-root-path=./dev/source/',
327
- '<%= config.bin %> <%= command.id %> --env dev -y --fetch-source --app-root-path=./dev/source/',
281
+ '<%= config.bin %> <%= command.id %> --env dev -y --skip-download --source git --app-root-path=./dev/source/',
328
282
  ];
329
283
  static flags = {
330
284
  yes: Flags.boolean({
@@ -359,14 +313,20 @@ export default class Install extends Command {
359
313
  }),
360
314
  'auth-type': Flags.string({
361
315
  char: 'a',
362
- description: 'Authentication: token (API key) or oauth (browser login via `nb env auth`)',
363
- options: ['token', 'oauth'],
316
+ description: 'Authentication: basic (username/password login), token (API key), or oauth (browser login via `nb env auth`)',
317
+ options: ['basic', 'token', 'oauth'],
364
318
  }),
365
319
  'access-token': Flags.string({
366
320
  char: 't',
367
321
  aliases: ['token'],
368
322
  description: 'API key or access token when using --auth-type token',
369
323
  }),
324
+ username: Flags.string({
325
+ description: 'Username when using --auth-type basic',
326
+ }),
327
+ password: Flags.string({
328
+ description: 'Password when using --auth-type basic',
329
+ }),
370
330
  'skip-auth': Flags.boolean({
371
331
  description: 'Save the env auth mode now and finish authentication later with `nb env auth`',
372
332
  default: false,
@@ -440,8 +400,8 @@ export default class Install extends Command {
440
400
  description: 'Use underscored database naming for the app',
441
401
  default: false,
442
402
  }),
443
- 'fetch-source': Flags.boolean({
444
- description: 'Download NocoBase app files or pull a Docker image before installing',
403
+ 'skip-download': Flags.boolean({
404
+ description: 'Skip the download step and reuse an existing local app directory or Docker image',
445
405
  default: false,
446
406
  }),
447
407
  ...omitKeys(Download.flags, ['yes']),
@@ -488,12 +448,6 @@ export default class Install extends Command {
488
448
  placeholder: installText('prompts.storagePath.placeholder'),
489
449
  initialValue: (values) => defaultInstallStoragePath(values.env ?? values.appName),
490
450
  },
491
- fetchSource: {
492
- type: 'boolean',
493
- message: installText('prompts.fetchSource.message'),
494
- initialValue: true,
495
- yesInitialValue: true,
496
- },
497
451
  };
498
452
  static dbPrompts = {
499
453
  dbDialect: {
@@ -521,8 +475,7 @@ export default class Install extends Command {
521
475
  message: installText('prompts.builtinDbImage.message'),
522
476
  placeholder: installText('prompts.builtinDbImage.placeholder'),
523
477
  initialValue: (values) => defaultBuiltinDbImageForDialect(values.dbDialect),
524
- hidden: (values) => !Boolean(values.builtinDb)
525
- || !supportsBuiltinDbDialect(values.dbDialect),
478
+ hidden: (values) => !values.builtinDb || !supportsBuiltinDbDialect(values.dbDialect),
526
479
  required: true,
527
480
  },
528
481
  dbHost: {
@@ -542,8 +495,7 @@ export default class Install extends Command {
542
495
  initialValue: (values) => defaultDbPortForDialect(values.dbDialect),
543
496
  required: true,
544
497
  validate: Install.validateDbPort,
545
- hidden: (values) => Boolean(values.builtinDb)
546
- && String(values.source ?? '').trim() === 'docker',
498
+ hidden: (values) => Boolean(values.builtinDb) && String(values.source ?? '').trim() === 'docker',
547
499
  },
548
500
  dbDatabase: {
549
501
  type: 'text',
@@ -568,6 +520,27 @@ export default class Install extends Command {
568
520
  required: true,
569
521
  validate: validateExternalDbPromptField,
570
522
  },
523
+ dbSchema: {
524
+ type: 'text',
525
+ message: installText('prompts.dbSchema.message'),
526
+ placeholder: installText('prompts.dbSchema.placeholder'),
527
+ hidden: (values) => String(values.dbDialect ?? '').trim() !== 'postgres',
528
+ },
529
+ dbTablePrefix: {
530
+ type: 'text',
531
+ message: installText('prompts.dbTablePrefix.message'),
532
+ placeholder: installText('prompts.dbTablePrefix.placeholder'),
533
+ },
534
+ dbUnderscored: {
535
+ type: 'boolean',
536
+ message: installText('prompts.dbUnderscored.message'),
537
+ initialValue: false,
538
+ yesInitialValue: false,
539
+ validate: (value, values) => validateExternalDbPromptField(value, {
540
+ ...values,
541
+ dbUnderscored: Boolean(value),
542
+ }),
543
+ },
571
544
  };
572
545
  static rootUserPrompts = {
573
546
  rootUsername: {
@@ -662,9 +635,8 @@ export default class Install extends Command {
662
635
  * Booleans with defaults are only preset when the user passed the flag on argv (see `download`).
663
636
  * Does not include `env` — use {@link buildEnvPresetValuesFromFlags} for {@link Install.envPrompts}.
664
637
  */
665
- static buildPresetValuesFromFlags(flags) {
638
+ static buildPresetValuesFromFlags(flags, argv = process.argv.slice(2)) {
666
639
  const preset = {};
667
- const argv = process.argv.slice(2);
668
640
  const apiBaseUrl = Install.toOptionalPromptString(flags['api-base-url']);
669
641
  if (apiBaseUrl) {
670
642
  preset.apiBaseUrl = apiBaseUrl;
@@ -687,6 +659,12 @@ export default class Install extends Command {
687
659
  if (flags['access-token'] !== undefined || flags.token !== undefined) {
688
660
  preset.accessToken = String(flags['access-token'] ?? flags.token ?? '');
689
661
  }
662
+ if (flags.username !== undefined) {
663
+ preset.username = String(flags.username ?? '').trim();
664
+ }
665
+ if (flags.password !== undefined) {
666
+ preset.password = String(flags.password ?? '');
667
+ }
690
668
  if (flags.lang !== undefined) {
691
669
  const v = String(flags.lang).trim();
692
670
  if (v) {
@@ -726,8 +704,15 @@ export default class Install extends Command {
726
704
  if (flags['root-nickname'] !== undefined) {
727
705
  preset.rootNickname = String(flags['root-nickname'] ?? '').trim();
728
706
  }
729
- if (argvHasToken(argv, ['--fetch-source'])) {
730
- preset.fetchSource = flags['fetch-source'];
707
+ if (argvHasToken(argv, ['--skip-download'])) {
708
+ preset.skipDownload = flags['skip-download'];
709
+ if (flags['skip-download']) {
710
+ preset.dockerSave = false;
711
+ preset.replace = false;
712
+ preset.devDependencies = false;
713
+ preset.build = false;
714
+ preset.buildDts = false;
715
+ }
731
716
  }
732
717
  if (argvHasToken(argv, ['--builtin-db', '--no-builtin-db'])) {
733
718
  preset.builtinDb = flags['builtin-db'];
@@ -789,18 +774,17 @@ export default class Install extends Command {
789
774
  }
790
775
  return preset;
791
776
  }
792
- static buildAppPresetValuesFromFlags(flags) {
793
- return pickPresetKeys(Install.buildPresetValuesFromFlags(flags), [
777
+ static buildAppPresetValuesFromFlags(flags, argv = process.argv.slice(2)) {
778
+ return pickPresetKeys(Install.buildPresetValuesFromFlags(flags, argv), [
794
779
  'lang',
795
780
  'force',
796
781
  'appRootPath',
797
782
  'appPort',
798
783
  'storagePath',
799
- 'fetchSource',
800
784
  ]);
801
785
  }
802
- static buildDbPresetValuesFromFlags(flags) {
803
- return pickPresetKeys(Install.buildPresetValuesFromFlags(flags), [
786
+ static buildDbPresetValuesFromFlags(flags, argv = process.argv.slice(2)) {
787
+ return pickPresetKeys(Install.buildPresetValuesFromFlags(flags, argv), [
804
788
  'builtinDb',
805
789
  'dbDialect',
806
790
  'builtinDbImage',
@@ -814,18 +798,20 @@ export default class Install extends Command {
814
798
  'dbUnderscored',
815
799
  ]);
816
800
  }
817
- static buildRootPresetValuesFromFlags(flags) {
818
- return pickPresetKeys(Install.buildPresetValuesFromFlags(flags), [
801
+ static buildRootPresetValuesFromFlags(flags, argv = process.argv.slice(2)) {
802
+ return pickPresetKeys(Install.buildPresetValuesFromFlags(flags, argv), [
819
803
  'rootUsername',
820
804
  'rootEmail',
821
805
  'rootPassword',
822
806
  'rootNickname',
823
807
  ]);
824
808
  }
825
- static buildEnvAddPresetValuesFromFlags(flags) {
826
- return pickPresetKeys(Install.buildPresetValuesFromFlags(flags), [
809
+ static buildEnvAddPresetValuesFromFlags(flags, argv = process.argv.slice(2)) {
810
+ return pickPresetKeys(Install.buildPresetValuesFromFlags(flags, argv), [
827
811
  'apiBaseUrl',
828
812
  'authType',
813
+ 'username',
814
+ 'password',
829
815
  'accessToken',
830
816
  ]);
831
817
  }
@@ -857,6 +843,18 @@ export default class Install extends Command {
857
843
  const text = String(value).trim();
858
844
  return text || undefined;
859
845
  }
846
+ static resolveManagedAppKey(value) {
847
+ return Install.toOptionalPromptString(value) ?? crypto.randomBytes(32).toString('hex');
848
+ }
849
+ static resolveManagedTimeZone(value) {
850
+ return Install.toOptionalPromptString(value) ?? (Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC');
851
+ }
852
+ async ensureManagedAppRuntimeConfig(params) {
853
+ const savedEnv = await getEnv(params.envName, { scope: resolveDefaultConfigScope() });
854
+ const savedConfig = savedEnv?.config;
855
+ params.appResults.appKey = Install.resolveManagedAppKey(params.appResults.appKey ?? savedConfig?.appKey);
856
+ params.appResults.timeZone = Install.resolveManagedTimeZone(params.appResults.timeZone ?? savedConfig?.timezone);
857
+ }
860
858
  static async validateAppPort(value, values) {
861
859
  const formatError = validateTcpPort(value);
862
860
  if (formatError) {
@@ -915,9 +913,13 @@ export default class Install extends Command {
915
913
  if (validationError) {
916
914
  throw new Error(validationError);
917
915
  }
916
+ const compatibilityError = await validateMysqlLowerCaseTableNamesCompatibility(dbResults);
917
+ if (compatibilityError) {
918
+ throw new Error(compatibilityError);
919
+ }
918
920
  }
919
921
  static async readResumePortValidationContext(values) {
920
- if (!Boolean(values.resume)) {
922
+ if (!values.resume) {
921
923
  return undefined;
922
924
  }
923
925
  const envName = Install.toOptionalPromptString(values.env);
@@ -948,8 +950,7 @@ export default class Install extends Command {
948
950
  }
949
951
  static async isResumeManagedPortReuse(params) {
950
952
  if (params.target === 'app') {
951
- if ((params.context.source === 'npm' || params.context.source === 'git')
952
- && params.context.appRootPath) {
953
+ if ((params.context.source === 'npm' || params.context.source === 'git') && params.context.appRootPath) {
953
954
  return await Install.isLocalPm2ProcessUsingPort(params.context.appRootPath, params.port);
954
955
  }
955
956
  const containerName = Install.buildDockerAppContainerName(params.context.envName, params.context.dockerContainerPrefix);
@@ -965,19 +966,13 @@ export default class Install extends Command {
965
966
  if (!containerName || !port) {
966
967
  return false;
967
968
  }
968
- const exists = await commandSucceeds('docker', [
969
- 'container',
970
- 'inspect',
971
- containerName,
972
- ]);
969
+ const exists = await commandSucceeds('docker', ['container', 'inspect', containerName]);
973
970
  if (!exists) {
974
971
  return false;
975
972
  }
976
973
  try {
977
974
  const output = await commandOutput('docker', ['port', containerName]);
978
- return output
979
- .split(/\r?\n/)
980
- .some((line) => line.includes(`:${port}`));
975
+ return output.split(/\r?\n/).some((line) => line.includes(`:${port}`));
981
976
  }
982
977
  catch {
983
978
  return false;
@@ -1033,29 +1028,20 @@ export default class Install extends Command {
1033
1028
  ...(appRootPath ? { appRootPath } : {}),
1034
1029
  ...(appPort ? { appPort } : {}),
1035
1030
  ...(storagePath ? { storagePath } : {}),
1036
- ...(source
1037
- ? { fetchSource: true }
1038
- : appRootPath
1039
- ? { fetchSource: false }
1040
- : {}),
1041
1031
  };
1042
1032
  const downloadPreset = {
1043
1033
  ...(source ? { source } : {}),
1044
1034
  ...(downloadVersion
1045
1035
  ? {
1046
1036
  version: downloadVersionPromptValue(downloadVersion),
1047
- ...(downloadVersionPromptValue(downloadVersion) === 'other'
1048
- ? { otherVersion: downloadVersion }
1049
- : {}),
1037
+ ...(downloadVersionPromptValue(downloadVersion) === 'other' ? { otherVersion: downloadVersion } : {}),
1050
1038
  }
1051
1039
  : {}),
1052
1040
  ...(dockerRegistry ? { dockerRegistry } : {}),
1053
1041
  ...(dockerPlatform ? { dockerPlatform } : {}),
1054
1042
  ...(gitUrl ? { gitUrl } : {}),
1055
1043
  ...(npmRegistry ? { npmRegistry } : {}),
1056
- ...(typeof config.devDependencies === 'boolean'
1057
- ? { devDependencies: config.devDependencies }
1058
- : {}),
1044
+ ...(typeof config.devDependencies === 'boolean' ? { devDependencies: config.devDependencies } : {}),
1059
1045
  ...(typeof config.build === 'boolean' ? { build: config.build } : {}),
1060
1046
  ...(typeof config.buildDts === 'boolean' ? { buildDts: config.buildDts } : {}),
1061
1047
  };
@@ -1085,6 +1071,13 @@ export default class Install extends Command {
1085
1071
  envAddPreset.accessToken = String(auth.accessToken);
1086
1072
  }
1087
1073
  }
1074
+ else if (savedAuthType === 'basic') {
1075
+ envAddPreset.authType = 'basic';
1076
+ const authUsername = Install.toOptionalPromptString(config.authUsername) ?? rootUsername;
1077
+ if (authUsername) {
1078
+ envAddPreset.username = authUsername;
1079
+ }
1080
+ }
1088
1081
  else if (savedAuthType === 'oauth') {
1089
1082
  envAddPreset.authType = 'oauth';
1090
1083
  }
@@ -1132,7 +1125,7 @@ export default class Install extends Command {
1132
1125
  throw new Error([
1133
1126
  `Cannot continue setup for "${env.name}" in non-interactive resume mode yet.`,
1134
1127
  `These setup-only flags are not saved in the env config: ${missingFlags.join(', ')}`,
1135
- `Run \`nb init --env ${env.name} --resume\` without \`--yes\`, or pass those flags again.`,
1128
+ `Run \`nb init --ui --env ${env.name} --resume\` without \`--yes\`, or pass those flags again.`,
1136
1129
  ].join('\n'));
1137
1130
  }
1138
1131
  }
@@ -1169,8 +1162,7 @@ export default class Install extends Command {
1169
1162
  }
1170
1163
  static shouldPublishBuiltinDbPortForValues(values) {
1171
1164
  const builtinDb = values.builtinDb === undefined ? true : Boolean(values.builtinDb);
1172
- return builtinDb
1173
- && Install.shouldPublishBuiltinDbPort(values.source);
1165
+ return builtinDb && Install.shouldPublishBuiltinDbPort(values.source);
1174
1166
  }
1175
1167
  static async buildDbPromptInitialValues(params) {
1176
1168
  if (params.flags['db-port'] !== undefined) {
@@ -1226,9 +1218,8 @@ export default class Install extends Command {
1226
1218
  * Resolve the effective preset `values` for the embedded download step.
1227
1219
  * Explicit download flags win; otherwise `-y` falls back to the docker + alpha quickstart path.
1228
1220
  */
1229
- static buildDownloadPresetValuesForInstall(flags, appResults, envName, yes) {
1221
+ static buildDownloadPresetValuesForInstall(flags, appResults, envName, yes, argv = process.argv.slice(2)) {
1230
1222
  const preset = {};
1231
- const argv = process.argv.slice(2);
1232
1223
  const appRoot = String(appResults.appRootPath ?? '').trim() || defaultInstallAppRootPath(envName);
1233
1224
  const lang = String(appResults.lang ?? DEFAULT_INSTALL_LANG).trim() || DEFAULT_INSTALL_LANG;
1234
1225
  preset.lang = lang;
@@ -1267,8 +1258,7 @@ export default class Install extends Command {
1267
1258
  }
1268
1259
  }
1269
1260
  if (flags['npm-registry'] !== undefined) {
1270
- preset.npmRegistry =
1271
- typeof flags['npm-registry'] === 'string' ? flags['npm-registry'] : '';
1261
+ preset.npmRegistry = typeof flags['npm-registry'] === 'string' ? flags['npm-registry'] : '';
1272
1262
  }
1273
1263
  if (flags.resume && !argvHasToken(argv, ['--replace', '-r'])) {
1274
1264
  preset.replace = true;
@@ -1312,15 +1302,11 @@ export default class Install extends Command {
1312
1302
  }
1313
1303
  static buildBuiltinDbContainerPrefix(containerPrefix) {
1314
1304
  const storedName = String(containerPrefix ?? '').trim();
1315
- return storedName
1316
- ? Install.sanitizeDockerResourceName(storedName)
1317
- : Install.defaultDockerContainerPrefix();
1305
+ return storedName ? Install.sanitizeDockerResourceName(storedName) : Install.defaultDockerContainerPrefix();
1318
1306
  }
1319
1307
  static buildManagedDockerNetworkName(networkName) {
1320
1308
  const storedName = String(networkName ?? '').trim();
1321
- return storedName
1322
- ? Install.sanitizeDockerResourceName(storedName)
1323
- : Install.defaultDockerNetworkName();
1309
+ return storedName ? Install.sanitizeDockerResourceName(storedName) : Install.defaultDockerNetworkName();
1324
1310
  }
1325
1311
  static buildBuiltinDbNetworkName(envName, networkName) {
1326
1312
  void envName;
@@ -1353,25 +1339,19 @@ export default class Install extends Command {
1353
1339
  }
1354
1340
  static buildBuiltinDbPlan(params) {
1355
1341
  const dbDialect = String(params.dbDialect ?? 'postgres').trim() || 'postgres';
1356
- const dbPort = String(params.dbPort ?? defaultDbPortForDialect(dbDialect)).trim()
1357
- || defaultDbPortForDialect(dbDialect);
1342
+ const dbPort = String(params.dbPort ?? defaultDbPortForDialect(dbDialect)).trim() || defaultDbPortForDialect(dbDialect);
1358
1343
  const defaultDbDatabase = defaultDbDatabaseForDialect(dbDialect);
1359
1344
  const networkName = Install.buildBuiltinDbNetworkName(params.envName, params.dockerNetworkName ?? params.workspaceName);
1360
1345
  const containerName = Install.buildBuiltinDbContainerName(params.envName, dbDialect, params.dockerContainerPrefix ?? params.workspaceName);
1361
1346
  const dbHostInput = String(params.dbHost ?? '').trim();
1362
1347
  const dbHost = Install.shouldPublishBuiltinDbPort(params.source)
1363
- ? (dbHostInput
1364
- && dbHostInput !== DEFAULT_INSTALL_BUILTIN_DB_HOST
1365
- && dbHostInput !== containerName
1348
+ ? dbHostInput && dbHostInput !== DEFAULT_INSTALL_BUILTIN_DB_HOST && dbHostInput !== containerName
1366
1349
  ? dbHostInput
1367
- : DEFAULT_INSTALL_DB_HOST)
1368
- : (dbHostInput
1369
- && dbHostInput !== DEFAULT_INSTALL_DB_HOST
1370
- && dbHostInput !== 'localhost'
1350
+ : DEFAULT_INSTALL_DB_HOST
1351
+ : dbHostInput && dbHostInput !== DEFAULT_INSTALL_DB_HOST && dbHostInput !== 'localhost'
1371
1352
  ? dbHostInput
1372
- : containerName);
1373
- const storagePath = resolveConfiguredEnvPath(params.storagePath)
1374
- ?? resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1353
+ : containerName;
1354
+ const storagePath = resolveConfiguredEnvPath(params.storagePath) ?? resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1375
1355
  if (dbDialect === 'postgres') {
1376
1356
  const image = String(params.builtinDbImage ?? '').trim() || defaultBuiltinDbImageForDialect(dbDialect);
1377
1357
  const dataDir = path.resolve(storagePath, 'db', 'postgres');
@@ -1402,12 +1382,9 @@ export default class Install extends Command {
1402
1382
  dbDialect,
1403
1383
  dbHost,
1404
1384
  dbPort,
1405
- dbDatabase: String(params.dbDatabase ?? defaultDbDatabase).trim()
1406
- || defaultDbDatabase,
1407
- dbUser: String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim()
1408
- || DEFAULT_INSTALL_DB_USER,
1409
- dbPassword: String(params.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD)
1410
- || DEFAULT_INSTALL_DB_PASSWORD,
1385
+ dbDatabase: String(params.dbDatabase ?? defaultDbDatabase).trim() || defaultDbDatabase,
1386
+ dbUser: String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER,
1387
+ dbPassword: String(params.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD,
1411
1388
  networkName,
1412
1389
  containerName,
1413
1390
  dataDir,
@@ -1593,11 +1570,7 @@ export default class Install extends Command {
1593
1570
  }
1594
1571
  }
1595
1572
  async dockerContainerExists(name) {
1596
- return await commandSucceeds('docker', [
1597
- 'container',
1598
- 'inspect',
1599
- name,
1600
- ]);
1573
+ return await commandSucceeds('docker', ['container', 'inspect', name]);
1601
1574
  }
1602
1575
  async removeDockerContainer(name) {
1603
1576
  await run('docker', ['rm', '-f', name], {
@@ -1647,8 +1620,7 @@ export default class Install extends Command {
1647
1620
  });
1648
1621
  }
1649
1622
  async startBuiltinDb(params) {
1650
- const storagePath = String(params.appResults.storagePath ?? '').trim()
1651
- || defaultInstallStoragePath(params.envName);
1623
+ const storagePath = String(params.appResults.storagePath ?? '').trim() || defaultInstallStoragePath(params.envName);
1652
1624
  const plan = Install.buildBuiltinDbPlan({
1653
1625
  envName: params.envName,
1654
1626
  workspaceName: params.workspaceName,
@@ -1686,30 +1658,27 @@ export default class Install extends Command {
1686
1658
  return plan;
1687
1659
  }
1688
1660
  static async buildDockerAppPlan(params) {
1689
- const dockerRegistry = String(downloadResultsValue(params.downloadResults, 'dockerRegistry') ?? '').trim()
1690
- || defaultDockerRegistryForLang(process.env.NB_LOCALE);
1661
+ const dockerRegistry = String(downloadResultsValue(params.downloadResults, 'dockerRegistry') ?? '').trim() ||
1662
+ defaultDockerRegistryForLang(process.env.NB_LOCALE);
1691
1663
  const version = String(downloadResultsValue(params.downloadResults, 'version') ?? '').trim() || DEFAULT_DOCKER_VERSION;
1692
1664
  const imageRef = resolveDockerImageRef(dockerRegistry, version, {
1693
1665
  defaultRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
1694
1666
  defaultVersion: DEFAULT_DOCKER_VERSION,
1695
1667
  });
1696
1668
  const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
1697
- const storagePath = resolveConfiguredEnvPath(String(params.appResults.storagePath ?? '').trim()
1698
- || defaultInstallStoragePath(params.envName))
1699
- ?? resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1669
+ const storagePath = resolveConfiguredEnvPath(String(params.appResults.storagePath ?? '').trim() || defaultInstallStoragePath(params.envName)) ?? resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1700
1670
  const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
1701
1671
  const dbHost = String(params.dbResults.dbHost ?? DEFAULT_INSTALL_DB_HOST).trim() || DEFAULT_INSTALL_DB_HOST;
1702
- const dbPort = String(params.dbResults.dbPort ?? defaultDbPortForDialect(dbDialect)).trim()
1703
- || defaultDbPortForDialect(dbDialect);
1704
- const dbDatabase = String(params.dbResults.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim()
1705
- || DEFAULT_INSTALL_DB_DATABASE;
1672
+ const dbPort = String(params.dbResults.dbPort ?? defaultDbPortForDialect(dbDialect)).trim() ||
1673
+ defaultDbPortForDialect(dbDialect);
1674
+ const dbDatabase = String(params.dbResults.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim() || DEFAULT_INSTALL_DB_DATABASE;
1706
1675
  const dbUser = String(params.dbResults.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER;
1707
1676
  const dbPassword = String(params.dbResults.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD;
1708
1677
  const dbSchema = optionalEnvString(params.dbResults.dbSchema);
1709
1678
  const dbTablePrefix = optionalEnvString(params.dbResults.dbTablePrefix);
1710
1679
  const dbUnderscored = optionalEnvBoolean(params.dbResults.dbUnderscored);
1711
- const appKey = crypto.randomBytes(32).toString('hex');
1712
- const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
1680
+ const appKey = Install.resolveManagedAppKey(params.appResults.appKey);
1681
+ const timeZone = Install.resolveManagedTimeZone(params.appResults.timeZone);
1713
1682
  const containerName = Install.buildDockerAppContainerName(params.envName, params.dockerContainerPrefix ?? params.workspaceName);
1714
1683
  const configuredEnvFile = String(params.appResults.envFile ?? '').trim();
1715
1684
  const envFile = await resolveDockerEnvFileArg(params.envName, configuredEnvFile ? { envFile: configuredEnvFile } : undefined);
@@ -1767,8 +1736,8 @@ export default class Install extends Command {
1767
1736
  return 'created';
1768
1737
  }
1769
1738
  async installDockerApp(params) {
1770
- const networkName = params.builtinDbPlan?.networkName
1771
- ?? Install.buildBuiltinDbNetworkName(params.envName, params.dockerNetworkName ?? params.workspaceName);
1739
+ const networkName = params.builtinDbPlan?.networkName ??
1740
+ Install.buildBuiltinDbNetworkName(params.envName, params.dockerNetworkName ?? params.workspaceName);
1772
1741
  await this.ensureDockerNetwork(networkName);
1773
1742
  const plan = await Install.buildDockerAppPlan({
1774
1743
  envName: params.envName,
@@ -1815,26 +1784,26 @@ export default class Install extends Command {
1815
1784
  Install.pushDownloadArgIfValue(argv, '--source', results.source);
1816
1785
  Install.pushDownloadArgIfValue(argv, '--version', downloadResultsValue(results, 'version'));
1817
1786
  Install.pushDownloadArgIfValue(argv, '--output-dir', source === 'npm' || source === 'git'
1818
- ? (resolveConfiguredEnvPath(results.outputDir)
1819
- ?? resolveConfiguredEnvPath(String(results.outputDir ?? '').trim() || defaultInstallAppRootPath(results.env)))
1787
+ ? resolveConfiguredEnvPath(results.outputDir) ??
1788
+ resolveConfiguredEnvPath(String(results.outputDir ?? '').trim() || defaultInstallAppRootPath(results.env))
1820
1789
  : results.outputDir);
1821
1790
  Install.pushDownloadArgIfValue(argv, '--git-url', results.gitUrl);
1822
1791
  Install.pushDownloadArgIfValue(argv, '--docker-registry', results.dockerRegistry);
1823
1792
  Install.pushDownloadArgIfValue(argv, '--docker-platform', results.dockerPlatform);
1824
1793
  Install.pushDownloadArgIfValue(argv, '--npm-registry', results.npmRegistry);
1825
- if (Boolean(results.replace)) {
1794
+ if (results.replace) {
1826
1795
  argv.push('--replace');
1827
1796
  }
1828
- if (Boolean(results.devDependencies)) {
1797
+ if (results.devDependencies) {
1829
1798
  argv.push('--dev-dependencies');
1830
1799
  }
1831
- if (Boolean(results.dockerSave)) {
1800
+ if (results.dockerSave) {
1832
1801
  argv.push('--docker-save');
1833
1802
  }
1834
- if (results.build !== undefined && !Boolean(results.build)) {
1803
+ if (results.build !== undefined && !results.build) {
1835
1804
  argv.push('--no-build');
1836
1805
  }
1837
- if (Boolean(results.buildDts)) {
1806
+ if (results.buildDts) {
1838
1807
  argv.push('--build-dts');
1839
1808
  }
1840
1809
  return argv;
@@ -1844,15 +1813,26 @@ export default class Install extends Command {
1844
1813
  if (projectRoot) {
1845
1814
  return projectRoot;
1846
1815
  }
1847
- const outputDir = String(params.downloadResults.outputDir ?? '').trim()
1848
- || String(params.appResults.appRootPath ?? '').trim()
1849
- || defaultInstallAppRootPath(params.envName);
1816
+ const outputDir = String(params.downloadResults.outputDir ?? '').trim() ||
1817
+ String(params.appResults.appRootPath ?? '').trim() ||
1818
+ defaultInstallAppRootPath(params.envName);
1850
1819
  return resolveConfiguredEnvPath(outputDir) ?? resolveEnvRelativePath(defaultInstallAppRootPath(params.envName));
1851
1820
  }
1852
1821
  static resolveLocalProjectConfigPath(params) {
1853
- return (String(params.downloadResults.outputDir ?? '').trim()
1854
- || String(params.appResults.appRootPath ?? '').trim()
1855
- || defaultInstallAppRootPath(params.envName));
1822
+ return (String(params.downloadResults.outputDir ?? '').trim() ||
1823
+ String(params.appResults.appRootPath ?? '').trim() ||
1824
+ defaultInstallAppRootPath(params.envName));
1825
+ }
1826
+ static buildSkipDownloadValues(envName, appResults) {
1827
+ const appRoot = String(appResults.appRootPath ?? '').trim() || defaultInstallAppRootPath(envName);
1828
+ return {
1829
+ outputDir: appRoot,
1830
+ replace: false,
1831
+ dockerSave: false,
1832
+ devDependencies: false,
1833
+ build: false,
1834
+ buildDts: false,
1835
+ };
1856
1836
  }
1857
1837
  commandStdio(verbose) {
1858
1838
  return verbose ? 'inherit' : 'ignore';
@@ -1862,7 +1842,41 @@ export default class Install extends Command {
1862
1842
  verbose: params.verbose,
1863
1843
  compactLog: true,
1864
1844
  });
1865
- return await this.config.runCommand('source:download', argv);
1845
+ return (await this.config.runCommand('source:download', argv));
1846
+ }
1847
+ async ensureSkippedDownloadInputsReady(params) {
1848
+ if (params.source === 'docker') {
1849
+ const dockerRegistry = String(downloadResultsValue(params.downloadResults, 'dockerRegistry') ?? '').trim() ||
1850
+ defaultDockerRegistryForLang(process.env.NB_LOCALE);
1851
+ const version = String(downloadResultsValue(params.downloadResults, 'version') ?? '').trim() || DEFAULT_DOCKER_VERSION;
1852
+ const imageRef = resolveDockerImageRef(dockerRegistry, version, {
1853
+ defaultRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
1854
+ defaultVersion: DEFAULT_DOCKER_VERSION,
1855
+ });
1856
+ const imageExists = await commandSucceeds('docker', ['image', 'inspect', imageRef]);
1857
+ if (!imageExists) {
1858
+ throw new Error(translateCli('commands.install.messages.skipDownloadDockerImageMissing', {
1859
+ imageRef,
1860
+ }));
1861
+ }
1862
+ return;
1863
+ }
1864
+ if (params.source === 'npm' || params.source === 'git') {
1865
+ const projectRoot = Install.resolveLocalProjectRoot({
1866
+ envName: params.envName,
1867
+ appResults: params.appResults,
1868
+ downloadResults: params.downloadResults,
1869
+ });
1870
+ try {
1871
+ await access(projectRoot);
1872
+ await access(path.join(projectRoot, 'package.json'));
1873
+ }
1874
+ catch {
1875
+ throw new Error(translateCli('commands.install.messages.skipDownloadLocalAppMissing', {
1876
+ projectRoot,
1877
+ }));
1878
+ }
1879
+ }
1866
1880
  }
1867
1881
  async downloadLocalApp(params) {
1868
1882
  const result = await this.downloadManagedSource({
@@ -1883,31 +1897,24 @@ export default class Install extends Command {
1883
1897
  return downloadedProjectRoot;
1884
1898
  }
1885
1899
  static buildLocalAppEnvVars(params) {
1886
- const configuredStoragePath = String(params.appResults.storagePath ?? '').trim()
1887
- || defaultInstallStoragePath(params.envName);
1888
- const storagePath = resolveConfiguredEnvPath(configuredStoragePath)
1889
- ?? resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1890
- const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim()
1891
- || 'postgres';
1892
- const appKey = crypto.randomBytes(32).toString('hex');
1893
- const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
1900
+ const configuredStoragePath = String(params.appResults.storagePath ?? '').trim() || defaultInstallStoragePath(params.envName);
1901
+ const storagePath = resolveConfiguredEnvPath(configuredStoragePath) ??
1902
+ resolveEnvRelativePath(defaultInstallStoragePath(params.envName));
1903
+ const dbDialect = String(params.dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
1904
+ const appKey = Install.resolveManagedAppKey(params.appResults.appKey);
1905
+ const timeZone = Install.resolveManagedTimeZone(params.appResults.timeZone);
1894
1906
  const env = {
1895
1907
  STORAGE_PATH: storagePath,
1896
- APP_PORT: String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim()
1897
- || DEFAULT_INSTALL_APP_PORT,
1908
+ APP_PORT: String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT,
1898
1909
  APP_KEY: appKey,
1899
1910
  TZ: timeZone,
1900
1911
  DB_DIALECT: dbDialect,
1901
- DB_HOST: String(params.dbResults.dbHost ?? DEFAULT_INSTALL_DB_HOST).trim()
1902
- || DEFAULT_INSTALL_DB_HOST,
1903
- DB_PORT: String(params.dbResults.dbPort ?? defaultDbPortForDialect(dbDialect)).trim()
1904
- || defaultDbPortForDialect(dbDialect),
1905
- DB_DATABASE: String(params.dbResults.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim()
1906
- || DEFAULT_INSTALL_DB_DATABASE,
1907
- DB_USER: String(params.dbResults.dbUser ?? DEFAULT_INSTALL_DB_USER).trim()
1908
- || DEFAULT_INSTALL_DB_USER,
1909
- DB_PASSWORD: String(params.dbResults.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD)
1910
- || DEFAULT_INSTALL_DB_PASSWORD,
1912
+ DB_HOST: String(params.dbResults.dbHost ?? DEFAULT_INSTALL_DB_HOST).trim() || DEFAULT_INSTALL_DB_HOST,
1913
+ DB_PORT: String(params.dbResults.dbPort ?? defaultDbPortForDialect(dbDialect)).trim() ||
1914
+ defaultDbPortForDialect(dbDialect),
1915
+ DB_DATABASE: String(params.dbResults.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim() || DEFAULT_INSTALL_DB_DATABASE,
1916
+ DB_USER: String(params.dbResults.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER,
1917
+ DB_PASSWORD: String(params.dbResults.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD,
1911
1918
  ...Install.buildInitAppEnvVars({
1912
1919
  appResults: params.appResults,
1913
1920
  rootResults: params.rootResults,
@@ -1957,10 +1964,8 @@ export default class Install extends Command {
1957
1964
  };
1958
1965
  }
1959
1966
  static resolveApiBaseUrl(params) {
1960
- const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim()
1961
- || DEFAULT_INSTALL_APP_PORT;
1962
- return (String(params.envAddResults.apiBaseUrl ?? '').trim()
1963
- || `http://127.0.0.1:${appPort}/api`);
1967
+ const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
1968
+ return String(params.envAddResults.apiBaseUrl ?? '').trim() || `http://127.0.0.1:${appPort}/api`;
1964
1969
  }
1965
1970
  static buildHealthCheckUrl(apiBaseUrl) {
1966
1971
  return `${apiBaseUrl.replace(/\/+$/, '')}/__health_check`;
@@ -1989,9 +1994,7 @@ export default class Install extends Command {
1989
1994
  const body = Install.formatHealthCheckMessage(text);
1990
1995
  return {
1991
1996
  ok: response.ok && text.trim().toLowerCase() === 'ok',
1992
- message: response.ok
1993
- ? `HTTP ${response.status}: ${body}`
1994
- : `HTTP ${response.status}: ${body}`,
1997
+ message: response.ok ? `HTTP ${response.status}: ${body}` : `HTTP ${response.status}: ${body}`,
1995
1998
  };
1996
1999
  }
1997
2000
  catch (error) {
@@ -2020,32 +2023,38 @@ export default class Install extends Command {
2020
2023
  let lastMessage = 'No response yet';
2021
2024
  let lastLoggedStatus = '';
2022
2025
  printInfo('Waiting for NocoBase to become ready...');
2023
- while (Date.now() - startedAt < timeoutMs) {
2024
- const result = await Install.requestAppHealthCheck({
2025
- healthCheckUrl,
2026
- fetchImpl,
2027
- requestTimeoutMs,
2028
- });
2029
- if (result.ok) {
2030
- return;
2031
- }
2032
- lastMessage = result.message;
2033
- const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
2034
- const statusLine = `Waiting for NocoBase to become ready... (${elapsedSeconds}s elapsed, last status: ${Install.formatHealthCheckMessage(lastMessage)})`;
2035
- if (statusLine !== lastLoggedStatus) {
2036
- printInfo(statusLine);
2037
- lastLoggedStatus = statusLine;
2038
- }
2039
- const remainingMs = timeoutMs - (Date.now() - startedAt);
2040
- if (remainingMs <= 0) {
2041
- break;
2026
+ const dockerLogFollower = options?.verbose && options.containerName ? startDockerLogFollower(options.containerName) : undefined;
2027
+ try {
2028
+ while (Date.now() - startedAt < timeoutMs) {
2029
+ const result = await Install.requestAppHealthCheck({
2030
+ healthCheckUrl,
2031
+ fetchImpl,
2032
+ requestTimeoutMs,
2033
+ });
2034
+ if (result.ok) {
2035
+ return;
2036
+ }
2037
+ lastMessage = result.message;
2038
+ const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
2039
+ const statusLine = `Waiting for NocoBase to become ready... (${elapsedSeconds}s elapsed, last status: ${Install.formatHealthCheckMessage(lastMessage)})`;
2040
+ if (statusLine !== lastLoggedStatus) {
2041
+ printInfo(statusLine);
2042
+ lastLoggedStatus = statusLine;
2043
+ }
2044
+ const remainingMs = timeoutMs - (Date.now() - startedAt);
2045
+ if (remainingMs <= 0) {
2046
+ break;
2047
+ }
2048
+ await Install.sleep(Math.min(intervalMs, remainingMs));
2042
2049
  }
2043
- await Install.sleep(Math.min(intervalMs, remainingMs));
2050
+ const logHint = options?.containerName
2051
+ ? ` You can inspect startup logs with: docker logs ${options.containerName}`
2052
+ : '';
2053
+ throw new Error(`The application did not become ready in time. Expected \`${healthCheckUrl}\` to respond with \`ok\`, but the last status was: ${Install.formatHealthCheckMessage(lastMessage)}.${logHint}`);
2054
+ }
2055
+ finally {
2056
+ await dockerLogFollower?.stop();
2044
2057
  }
2045
- const logHint = options?.containerName
2046
- ? ` You can inspect startup logs with: docker logs ${options.containerName}`
2047
- : '';
2048
- throw new Error(`The application did not become ready in time. Expected \`${healthCheckUrl}\` to respond with \`ok\`, but the last status was: ${Install.formatHealthCheckMessage(lastMessage)}.${logHint}`);
2049
2058
  }
2050
2059
  async saveInstalledEnv(params) {
2051
2060
  await upsertEnv(params.envName, Install.buildSavedEnvConfig(params), { scope: resolveDefaultConfigScope() });
@@ -2055,8 +2064,7 @@ export default class Install extends Command {
2055
2064
  if (!params.appReady) {
2056
2065
  return;
2057
2066
  }
2058
- const authType = String(params.envAddResults.authType ?? 'oauth').trim()
2059
- || 'oauth';
2067
+ const authType = String(params.envAddResults.authType ?? 'oauth').trim() || 'oauth';
2060
2068
  if (params.skipAuth) {
2061
2069
  printInfo(formatDeferredAuthMessage(params.envName, authType));
2062
2070
  return;
@@ -2064,23 +2072,34 @@ export default class Install extends Command {
2064
2072
  if (authType === 'oauth') {
2065
2073
  await this.config.runCommand('env:auth', [params.envName]);
2066
2074
  }
2075
+ else if (authType === 'basic') {
2076
+ const authArgv = [params.envName, '--auth-type', 'basic'];
2077
+ const username = String(params.envAddResults.username ?? '').trim();
2078
+ const password = String(params.envAddResults.password ?? '');
2079
+ if (username) {
2080
+ authArgv.push('--username', username);
2081
+ }
2082
+ if (password) {
2083
+ authArgv.push('--password', password);
2084
+ }
2085
+ await this.config.runCommand('env:auth', authArgv);
2086
+ }
2067
2087
  await this.config.runCommand('env:update', [params.envName]);
2068
2088
  }
2069
2089
  static buildSavedEnvConfig(params) {
2070
- const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim()
2071
- || DEFAULT_INSTALL_APP_PORT;
2072
- const storagePath = String(params.appResults.storagePath ?? '').trim()
2073
- || defaultInstallStoragePath(params.envName);
2090
+ const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
2091
+ const storagePath = String(params.appResults.storagePath ?? '').trim() || defaultInstallStoragePath(params.envName);
2074
2092
  const envFile = String(params.appResults.envFile ?? '').trim() || undefined;
2075
2093
  const apiBaseUrl = Install.resolveApiBaseUrl({
2076
2094
  appResults: params.appResults,
2077
2095
  envAddResults: params.envAddResults,
2078
2096
  });
2079
- const authType = String(params.envAddResults.authType ?? 'oauth').trim()
2080
- || 'oauth';
2097
+ const authType = String(params.envAddResults.authType ?? 'oauth').trim() || 'oauth';
2098
+ const authUsername = authType === 'basic' ? String(params.envAddResults.username ?? params.rootResults.rootUsername ?? '').trim() : '';
2081
2099
  return buildStoredEnvConfig({
2082
2100
  apiBaseUrl,
2083
2101
  authType,
2102
+ ...(authUsername ? { authUsername } : {}),
2084
2103
  accessToken: params.envAddResults.accessToken,
2085
2104
  source: downloadResultsValue(params.downloadResults, 'source'),
2086
2105
  downloadVersion: downloadResultsValue(params.downloadResults, 'version'),
@@ -2115,6 +2134,7 @@ export default class Install extends Command {
2115
2134
  });
2116
2135
  }
2117
2136
  async collectPromptResults(parsed, yes) {
2137
+ const commandArgv = this.argv ?? process.argv.slice(2);
2118
2138
  const resumePreset = await this.resolveResumePresetValues(parsed, yes);
2119
2139
  const envPreset = {
2120
2140
  ...(resumePreset?.envPreset ?? {}),
@@ -2130,7 +2150,7 @@ export default class Install extends Command {
2130
2150
  const envName = String(envResults.env ?? '').trim() || DEFAULT_INSTALL_ENV_NAME;
2131
2151
  const appPreset = {
2132
2152
  ...(resumePreset?.appPreset ?? {}),
2133
- ...Install.buildAppPresetValuesFromFlags(parsed),
2153
+ ...Install.buildAppPresetValuesFromFlags(parsed, commandArgv),
2134
2154
  };
2135
2155
  const appCatalog = Install.buildAppPromptsCatalog(envName, {
2136
2156
  resume: parsed.resume,
@@ -2140,56 +2160,58 @@ export default class Install extends Command {
2140
2160
  envName,
2141
2161
  flags: {
2142
2162
  ...parsed,
2143
- 'app-root-path': parsed['app-root-path']
2144
- ?? Install.toOptionalPromptString(appPreset.appRootPath),
2145
- 'app-port': parsed['app-port']
2146
- ?? Install.toOptionalPromptString(appPreset.appPort),
2147
- 'storage-path': parsed['storage-path']
2148
- ?? Install.toOptionalPromptString(appPreset.storagePath),
2163
+ 'app-root-path': parsed['app-root-path'] ?? Install.toOptionalPromptString(appPreset.appRootPath),
2164
+ 'app-port': parsed['app-port'] ?? Install.toOptionalPromptString(appPreset.appPort),
2165
+ 'storage-path': parsed['storage-path'] ?? Install.toOptionalPromptString(appPreset.storagePath),
2149
2166
  },
2150
2167
  }),
2151
2168
  values: appPreset,
2152
2169
  yesInitialValues: { resume: parsed.resume },
2153
2170
  yes,
2154
2171
  });
2155
- let downloadResults = {};
2156
- if (Boolean(appResults.fetchSource)) {
2157
- const downloadOpts = Install.buildDownloadPromptOptionsForInstall(appResults, envName);
2158
- downloadOpts.values = {
2159
- ...(resumePreset?.downloadPreset ?? {}),
2160
- ...downloadOpts.values,
2161
- ...Install.buildDownloadPresetValuesForInstall(parsed, appResults, envName, yes),
2162
- };
2163
- downloadOpts.yes = yes;
2164
- downloadResults = await runPromptCatalog(Download.prompts, downloadOpts);
2172
+ const downloadOpts = Install.buildDownloadPromptOptionsForInstall(appResults, envName);
2173
+ downloadOpts.values = {
2174
+ ...(resumePreset?.downloadPreset ?? {}),
2175
+ ...downloadOpts.values,
2176
+ ...Install.buildDownloadPresetValuesForInstall(parsed, appResults, envName, yes, commandArgv),
2177
+ ...(parsed['skip-download'] ? Install.buildSkipDownloadValues(envName, appResults) : {}),
2178
+ };
2179
+ downloadOpts.yes = yes;
2180
+ const downloadResults = await runPromptCatalog(Download.prompts, downloadOpts);
2181
+ if (parsed['skip-download']) {
2182
+ delete downloadResults.outputDir;
2183
+ delete downloadResults.replace;
2184
+ delete downloadResults.dockerSave;
2185
+ delete downloadResults.devDependencies;
2186
+ delete downloadResults.build;
2187
+ delete downloadResults.buildDts;
2165
2188
  }
2166
2189
  const dbPreset = {
2167
2190
  ...(resumePreset?.dbPreset ?? {}),
2168
- ...Install.buildDbPresetValuesFromFlags(parsed),
2191
+ ...Install.buildDbPresetValuesFromFlags(parsed, commandArgv),
2169
2192
  };
2170
2193
  const promptedDbResults = await runPromptCatalog(Install.buildDbPromptsCatalog(envName, downloadResults, {
2171
2194
  resume: parsed.resume,
2172
2195
  }), {
2173
2196
  initialValues: {
2174
2197
  ...downloadResults,
2175
- ...await Install.buildDbPromptInitialValues({
2198
+ ...(await Install.buildDbPromptInitialValues({
2176
2199
  flags: {
2177
2200
  ...parsed,
2178
- 'db-port': parsed['db-port']
2179
- ?? Install.toOptionalPromptString(dbPreset.dbPort),
2201
+ 'db-port': parsed['db-port'] ?? Install.toOptionalPromptString(dbPreset.dbPort),
2180
2202
  },
2181
2203
  downloadResults,
2182
2204
  dbPreset,
2183
- }),
2205
+ })),
2184
2206
  },
2185
2207
  values: dbPreset,
2186
2208
  yes,
2187
2209
  });
2188
2210
  const dbResults = {
2189
- ...promptedDbResults,
2190
2211
  ...pickPresetKeys(dbPreset, ['dbSchema', 'dbTablePrefix', 'dbUnderscored']),
2212
+ ...promptedDbResults,
2191
2213
  };
2192
- const rootPreset = Install.buildRootPresetValuesFromFlags(parsed);
2214
+ const rootPreset = Install.buildRootPresetValuesFromFlags(parsed, commandArgv);
2193
2215
  const rootResults = await runPromptCatalog(Install.rootUserPrompts, {
2194
2216
  initialValues: {},
2195
2217
  values: {
@@ -2199,18 +2221,44 @@ export default class Install extends Command {
2199
2221
  yes,
2200
2222
  });
2201
2223
  const envAddPromptsForInstall = this.buildEnvAddPromptsForInstall(parsed);
2202
- const envAddResults = await runPromptCatalog(envAddPromptsForInstall, {
2203
- initialValues: {
2204
- apiBaseUrl: `http://127.0.0.1:${appResults.appPort ?? DEFAULT_INSTALL_APP_PORT}/api`,
2205
- },
2224
+ const envAddResumePreset = resumePreset?.envAddPreset ?? {};
2225
+ const envAddFlagValues = Install.buildEnvAddPresetValuesFromFlags(parsed, commandArgv);
2226
+ const envAddPreset = {
2227
+ ...envAddResumePreset,
2228
+ ...envAddFlagValues,
2229
+ };
2230
+ const resolvedEnvAddAuthType = String(envAddPreset.authType ?? '').trim();
2231
+ const envAddInitialValues = {
2232
+ apiBaseUrl: `http://127.0.0.1:${appResults.appPort ?? DEFAULT_INSTALL_APP_PORT}/api`,
2233
+ ...envAddResumePreset,
2234
+ ...(!parsed['skip-auth'] && resolvedEnvAddAuthType === 'basic'
2235
+ ? {
2236
+ ...(!Object.prototype.hasOwnProperty.call(envAddResumePreset, 'username') &&
2237
+ !Object.prototype.hasOwnProperty.call(envAddFlagValues, 'username') &&
2238
+ rootResults.rootUsername !== undefined
2239
+ ? { username: String(rootResults.rootUsername).trim() }
2240
+ : {}),
2241
+ ...(!Object.prototype.hasOwnProperty.call(envAddFlagValues, 'password') &&
2242
+ rootResults.rootPassword !== undefined
2243
+ ? { password: String(rootResults.rootPassword ?? '') }
2244
+ : {}),
2245
+ }
2246
+ : {}),
2247
+ };
2248
+ const promptedEnvAddResults = await runPromptCatalog(envAddPromptsForInstall, {
2249
+ initialValues: envAddInitialValues,
2206
2250
  values: {
2207
2251
  name: envName,
2208
2252
  ...(parsed['skip-auth'] ? { skipAuth: true } : {}),
2209
- ...(resumePreset?.envAddPreset ?? {}),
2210
- ...Install.buildEnvAddPresetValuesFromFlags(parsed),
2253
+ ...envAddFlagValues,
2211
2254
  },
2212
2255
  yes,
2213
2256
  });
2257
+ const envAddResults = {
2258
+ ...pickPresetKeys(envAddInitialValues, ['username', 'password']),
2259
+ ...promptedEnvAddResults,
2260
+ ...pickPresetKeys(envAddFlagValues, ['username', 'password']),
2261
+ };
2214
2262
  return {
2215
2263
  envName,
2216
2264
  envResults,
@@ -2243,16 +2291,27 @@ export default class Install extends Command {
2243
2291
  : 'Resuming setup from the saved workspace config');
2244
2292
  }
2245
2293
  const promptResults = await this.collectPromptResults(parsed, flags.yes);
2246
- const { envName, appResults, downloadResults, dbResults, rootResults, envAddResults, } = promptResults;
2294
+ const { envName, appResults, downloadResults, dbResults, rootResults, envAddResults } = promptResults;
2295
+ await this.ensureManagedAppRuntimeConfig({
2296
+ envName,
2297
+ appResults,
2298
+ });
2247
2299
  const source = String(downloadResultsValue(downloadResults, 'source') ?? '').trim();
2248
- const usesDockerResources = Boolean(dbResults.builtinDb)
2249
- || (Boolean(appResults.fetchSource) && source === 'docker');
2300
+ const usesDockerResources = Boolean(dbResults.builtinDb) || source === 'docker';
2250
2301
  const dockerNetworkName = usesDockerResources
2251
2302
  ? await resolveDockerNetworkName({ scope: resolveDefaultConfigScope() })
2252
2303
  : undefined;
2253
2304
  const dockerContainerPrefix = usesDockerResources
2254
2305
  ? await resolveDockerContainerPrefix({ scope: resolveDefaultConfigScope() })
2255
2306
  : undefined;
2307
+ if (parsed['skip-download']) {
2308
+ await this.ensureSkippedDownloadInputsReady({
2309
+ source,
2310
+ envName,
2311
+ appResults,
2312
+ downloadResults,
2313
+ });
2314
+ }
2256
2315
  await Install.ensureExternalDbReadyForInstall(dbResults);
2257
2316
  if (!parsed.resume) {
2258
2317
  if (!parsed['skip-save-env-log']) {
@@ -2271,7 +2330,7 @@ export default class Install extends Command {
2271
2330
  }
2272
2331
  }
2273
2332
  let builtinDbPlan;
2274
- if (Boolean(dbResults.builtinDb)) {
2333
+ if (dbResults.builtinDb) {
2275
2334
  builtinDbPlan = await this.startBuiltinDb({
2276
2335
  envName,
2277
2336
  dockerNetworkName,
@@ -2291,14 +2350,16 @@ export default class Install extends Command {
2291
2350
  }
2292
2351
  let dockerAppPlan;
2293
2352
  let localAppPlan;
2294
- if (Boolean(appResults.fetchSource)) {
2353
+ if (source === 'docker' || source === 'npm' || source === 'git') {
2295
2354
  this.logStage('Preparing application');
2296
2355
  if (source === 'docker') {
2297
- await this.downloadManagedSource({
2298
- downloadResults,
2299
- verbose: parsed.verbose,
2300
- });
2301
- printInfo('Application image ready.');
2356
+ if (!parsed['skip-download']) {
2357
+ await this.downloadManagedSource({
2358
+ downloadResults,
2359
+ verbose: parsed.verbose,
2360
+ });
2361
+ printInfo('Application image ready.');
2362
+ }
2302
2363
  dockerAppPlan = await this.installDockerApp({
2303
2364
  envName,
2304
2365
  dockerNetworkName,
@@ -2316,13 +2377,21 @@ export default class Install extends Command {
2316
2377
  }
2317
2378
  else if (source === 'npm' || source === 'git') {
2318
2379
  const localSource = source === 'npm' ? 'npm' : 'git';
2319
- const projectRoot = await this.downloadLocalApp({
2320
- envName,
2321
- appResults,
2322
- downloadResults,
2323
- verbose: parsed.verbose,
2324
- });
2325
- printInfo('Application files ready.');
2380
+ const projectRoot = parsed['skip-download']
2381
+ ? Install.resolveLocalProjectRoot({
2382
+ envName,
2383
+ appResults,
2384
+ downloadResults,
2385
+ })
2386
+ : await this.downloadLocalApp({
2387
+ envName,
2388
+ appResults,
2389
+ downloadResults,
2390
+ verbose: parsed.verbose,
2391
+ });
2392
+ if (!parsed['skip-download']) {
2393
+ printInfo('Application files ready.');
2394
+ }
2326
2395
  localAppPlan = await this.startLocalApp({
2327
2396
  envName,
2328
2397
  source: localSource,
@@ -2346,6 +2415,7 @@ export default class Install extends Command {
2346
2415
  envAddResults,
2347
2416
  }), {
2348
2417
  containerName: dockerAppPlan?.containerName,
2418
+ verbose: parsed.verbose,
2349
2419
  });
2350
2420
  printInfo(`NocoBase is ready at http://127.0.0.1:${dockerAppPlan?.appPort ?? localAppPlan?.appPort}`);
2351
2421
  }