@nocobase/cli 2.1.0-alpha.26 → 2.1.0-alpha.27

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 (43) hide show
  1. package/README.md +24 -0
  2. package/README.zh-CN.md +4 -0
  3. package/dist/commands/app/down.js +2 -3
  4. package/dist/commands/app/upgrade.js +112 -128
  5. package/dist/commands/config/delete.js +30 -0
  6. package/dist/commands/config/get.js +29 -0
  7. package/dist/commands/config/index.js +20 -0
  8. package/dist/commands/config/list.js +29 -0
  9. package/dist/commands/config/set.js +35 -0
  10. package/dist/commands/db/check.js +230 -0
  11. package/dist/commands/db/shared.js +1 -1
  12. package/dist/commands/env/shared.js +1 -1
  13. package/dist/commands/init.js +0 -1
  14. package/dist/commands/install.js +87 -35
  15. package/dist/commands/license/activate.js +357 -0
  16. package/dist/commands/license/env.js +94 -0
  17. package/dist/commands/license/generate-id.js +107 -0
  18. package/dist/commands/license/id.js +52 -0
  19. package/dist/commands/license/index.js +20 -0
  20. package/dist/commands/license/plugins/clean.js +98 -0
  21. package/dist/commands/license/plugins/index.js +20 -0
  22. package/dist/commands/license/plugins/list.js +50 -0
  23. package/dist/commands/license/plugins/shared.js +325 -0
  24. package/dist/commands/license/plugins/sync.js +267 -0
  25. package/dist/commands/license/shared.js +411 -0
  26. package/dist/commands/license/status.js +50 -0
  27. package/dist/lib/api-client.js +74 -3
  28. package/dist/lib/app-runtime.js +26 -10
  29. package/dist/lib/auth-store.js +29 -66
  30. package/dist/lib/build-config.js +8 -0
  31. package/dist/lib/cli-config.js +176 -0
  32. package/dist/lib/cli-home.js +6 -21
  33. package/dist/lib/db-connection-check.js +178 -0
  34. package/dist/lib/generated-command.js +23 -3
  35. package/dist/lib/plugin-storage.js +127 -0
  36. package/dist/lib/prompt-validators.js +4 -4
  37. package/dist/lib/runtime-generator.js +89 -10
  38. package/dist/lib/self-manager.js +57 -2
  39. package/dist/lib/startup-update.js +85 -7
  40. package/dist/locale/en-US.json +16 -13
  41. package/dist/locale/zh-CN.json +16 -13
  42. package/nocobase-ctl.config.json +82 -0
  43. package/package.json +16 -4
@@ -0,0 +1,230 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { Command, Flags } from '@oclif/core';
10
+ import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime } from '../../lib/app-runtime.js';
11
+ import { checkExternalDbConnection, formatDbCheckAddress, readExternalDbConnectionConfig, } from "../../lib/db-connection-check.js";
12
+ import { commandOutput } from '../../lib/run-npm.js';
13
+ import { validateTcpPort } from "../../lib/prompt-validators.js";
14
+ const DEFAULT_DOCKER_REGISTRY = 'nocobase/nocobase';
15
+ const DEFAULT_DOCKER_VERSION = 'alpha';
16
+ function trimValue(value) {
17
+ const text = String(value ?? '').trim();
18
+ return text || undefined;
19
+ }
20
+ function resolveRequiredDbField(flagValue, envValue) {
21
+ return trimValue(flagValue) ?? trimValue(envValue);
22
+ }
23
+ function normalizeDockerPlatform(value) {
24
+ const text = trimValue(value);
25
+ if (!text || text === 'auto') {
26
+ return undefined;
27
+ }
28
+ if (text === 'linux/amd64' || text === 'linux/arm64') {
29
+ return text;
30
+ }
31
+ return undefined;
32
+ }
33
+ function formatMissingFieldsMessage(missing, hasEnv) {
34
+ return [
35
+ 'Missing database settings for connectivity check.',
36
+ `Required: ${missing.join(', ')}.`,
37
+ hasEnv
38
+ ? 'Pass `--env <name>` to reuse a saved env, or provide the missing `--db-*` flags explicitly.'
39
+ : 'Provide all required `--db-*` flags explicitly, or pass `--env <name>` to reuse a saved env.',
40
+ ].join('\n');
41
+ }
42
+ function resolveDbConfigFromFlags(flags, envConfig) {
43
+ return {
44
+ builtinDb: false,
45
+ dbDialect: resolveRequiredDbField(flags['db-dialect'], envConfig?.dbDialect),
46
+ dbHost: resolveRequiredDbField(flags['db-host'], envConfig?.dbHost),
47
+ dbPort: resolveRequiredDbField(flags['db-port'], envConfig?.dbPort),
48
+ dbDatabase: resolveRequiredDbField(flags['db-database'], envConfig?.dbDatabase),
49
+ dbUser: resolveRequiredDbField(flags['db-user'], envConfig?.dbUser),
50
+ dbPassword: flags['db-password'] !== undefined
51
+ ? String(flags['db-password'] ?? '')
52
+ : envConfig?.dbPassword !== undefined
53
+ ? String(envConfig.dbPassword ?? '')
54
+ : undefined,
55
+ };
56
+ }
57
+ function validateDbConfigOrThrow(command, dbConfig, hasEnv) {
58
+ const missing = [];
59
+ if (!dbConfig.dbDialect) {
60
+ missing.push('--db-dialect');
61
+ }
62
+ if (!dbConfig.dbHost) {
63
+ missing.push('--db-host');
64
+ }
65
+ if (!dbConfig.dbPort) {
66
+ missing.push('--db-port');
67
+ }
68
+ if (!dbConfig.dbDatabase) {
69
+ missing.push('--db-database');
70
+ }
71
+ if (!dbConfig.dbUser) {
72
+ missing.push('--db-user');
73
+ }
74
+ if (!dbConfig.dbPassword) {
75
+ missing.push('--db-password');
76
+ }
77
+ if (missing.length > 0) {
78
+ command.error(formatMissingFieldsMessage(missing, hasEnv));
79
+ }
80
+ const portError = validateTcpPort(dbConfig.dbPort);
81
+ if (portError) {
82
+ command.error(portError);
83
+ }
84
+ }
85
+ async function resolveDbCheckInput(command, flags) {
86
+ const envName = flags.env?.trim() || undefined;
87
+ if (!envName) {
88
+ const dbConfig = resolveDbConfigFromFlags(flags);
89
+ validateDbConfigOrThrow(command, dbConfig, false);
90
+ return {
91
+ dbConfig,
92
+ };
93
+ }
94
+ const runtime = await resolveManagedAppRuntime(envName);
95
+ if (!runtime) {
96
+ command.error(formatMissingManagedAppEnvMessage(envName));
97
+ }
98
+ const dbConfig = resolveDbConfigFromFlags(flags, runtime.env.config);
99
+ validateDbConfigOrThrow(command, dbConfig, true);
100
+ return {
101
+ envName: runtime.envName,
102
+ kind: runtime.kind,
103
+ runtime: runtime,
104
+ dbConfig,
105
+ };
106
+ }
107
+ function buildConnectionConfigOrThrow(command, dbConfig) {
108
+ const connectionConfig = readExternalDbConnectionConfig(dbConfig);
109
+ if (!connectionConfig) {
110
+ command.error('Unsupported or incomplete database settings for connectivity check.');
111
+ }
112
+ return connectionConfig;
113
+ }
114
+ async function runExplicitDbCheck(command, dbConfig) {
115
+ const connectionConfig = buildConnectionConfigOrThrow(command, dbConfig);
116
+ const address = formatDbCheckAddress(connectionConfig);
117
+ const validationError = await checkExternalDbConnection(connectionConfig);
118
+ return {
119
+ ok: !validationError,
120
+ dialect: connectionConfig.dialect,
121
+ address,
122
+ error: validationError ?? null,
123
+ };
124
+ }
125
+ async function runDockerDbCheck(command, runtime, dbConfig) {
126
+ const connectionConfig = buildConnectionConfigOrThrow(command, dbConfig);
127
+ const config = runtime.env.config ?? {};
128
+ const imageRef = `${trimValue(config.dockerRegistry) || DEFAULT_DOCKER_REGISTRY}:${trimValue(config.downloadVersion) || DEFAULT_DOCKER_VERSION}`;
129
+ const args = [
130
+ 'run',
131
+ '--rm',
132
+ '--network',
133
+ runtime.dockerNetworkName || runtime.workspaceName,
134
+ ];
135
+ const dockerPlatform = normalizeDockerPlatform(config.dockerPlatform);
136
+ if (dockerPlatform) {
137
+ args.push('--platform', dockerPlatform);
138
+ }
139
+ args.push('--entrypoint', 'nb', imageRef, 'db', 'check', '--db-dialect', connectionConfig.dialect, '--db-host', connectionConfig.host, '--db-port', String(connectionConfig.port), '--db-database', connectionConfig.database, '--db-user', connectionConfig.user, '--db-password', connectionConfig.password, '--json');
140
+ const output = await commandOutput('docker', args, {
141
+ errorName: 'docker run',
142
+ });
143
+ let payload;
144
+ try {
145
+ payload = JSON.parse(output);
146
+ }
147
+ catch {
148
+ command.error(`Failed to parse database check response from Docker: ${output}`);
149
+ }
150
+ const ok = Boolean(payload.ok);
151
+ const dialect = trimValue(payload.dialect) || connectionConfig.dialect;
152
+ const address = trimValue(payload.address) || formatDbCheckAddress(connectionConfig);
153
+ const error = trimValue(payload.error) || null;
154
+ return {
155
+ ok,
156
+ dialect,
157
+ address,
158
+ error,
159
+ };
160
+ }
161
+ async function runDbCheckForRuntime(command, runtime, dbConfig) {
162
+ if (runtime.kind === 'docker') {
163
+ return await runDockerDbCheck(command, runtime, dbConfig);
164
+ }
165
+ if (runtime.kind === 'local') {
166
+ return await runExplicitDbCheck(command, dbConfig);
167
+ }
168
+ command.error(`Env "${runtime.envName}" does not support automatic database connectivity checks.`);
169
+ }
170
+ export default class DbCheck extends Command {
171
+ static description = 'Check whether a database is reachable using the selected env settings or explicit `--db-*` flags.';
172
+ static examples = [
173
+ '<%= config.bin %> <%= command.id %> --env app1',
174
+ '<%= config.bin %> <%= command.id %> --env app1 --db-password new-secret --json',
175
+ '<%= config.bin %> <%= command.id %> --db-dialect postgres --db-host 127.0.0.1 --db-port 5432 --db-database nocobase --db-user nocobase --db-password secret',
176
+ ];
177
+ static flags = {
178
+ env: Flags.string({
179
+ char: 'e',
180
+ description: 'CLI env name to read saved database settings from. Defaults to the current env when omitted.',
181
+ }),
182
+ 'db-dialect': Flags.string({
183
+ description: 'Database dialect: postgres, kingbase, mysql, or mariadb.',
184
+ options: ['postgres', 'kingbase', 'mysql', 'mariadb'],
185
+ }),
186
+ 'db-host': Flags.string({
187
+ description: 'Database host name or IP address.',
188
+ }),
189
+ 'db-port': Flags.string({
190
+ description: 'Database TCP port.',
191
+ }),
192
+ 'db-database': Flags.string({
193
+ description: 'Database name.',
194
+ }),
195
+ 'db-user': Flags.string({
196
+ description: 'Database username.',
197
+ }),
198
+ 'db-password': Flags.string({
199
+ description: 'Database password.',
200
+ }),
201
+ json: Flags.boolean({
202
+ description: 'Output the check result as JSON.',
203
+ default: false,
204
+ }),
205
+ };
206
+ async run() {
207
+ const { flags } = await this.parse(DbCheck);
208
+ const input = await resolveDbCheckInput(this, flags);
209
+ const result = input.runtime
210
+ ? await runDbCheckForRuntime(this, input.runtime, input.dbConfig)
211
+ : await runExplicitDbCheck(this, input.dbConfig);
212
+ if (flags.json) {
213
+ this.log(JSON.stringify({
214
+ ok: result.ok,
215
+ env: input.envName,
216
+ kind: input.kind,
217
+ dialect: result.dialect,
218
+ address: result.address,
219
+ error: result.error,
220
+ }, null, 2));
221
+ return;
222
+ }
223
+ if (!result.ok) {
224
+ this.error(result.error ?? 'Database check failed.');
225
+ }
226
+ this.log(input.envName
227
+ ? `Database check passed for env "${input.envName}" (${result.dialect} ${result.address}).`
228
+ : `Database check passed (${result.dialect} ${result.address}).`);
229
+ }
230
+ }
@@ -23,7 +23,7 @@ export async function resolveDbRuntime(envName) {
23
23
  const source = runtime.kind === 'http' || runtime.kind === 'ssh' ? runtime.kind : runtime.source;
24
24
  const dbDialect = String(runtime.env.config.dbDialect ?? 'postgres').trim() || 'postgres';
25
25
  if ((runtime.kind === 'local' || runtime.kind === 'docker') && runtime.env.config.builtinDb) {
26
- const containerName = buildDockerDbContainerName(runtime.envName, dbDialect, runtime.workspaceName);
26
+ const containerName = buildDockerDbContainerName(runtime.envName, dbDialect, runtime.dockerContainerPrefix || runtime.workspaceName);
27
27
  return {
28
28
  kind: 'builtin',
29
29
  envName: runtime.envName,
@@ -141,7 +141,7 @@ export async function dbStatus(runtime) {
141
141
  return '-';
142
142
  }
143
143
  const dbDialect = String(runtime.env.config.dbDialect ?? 'postgres').trim() || 'postgres';
144
- const containerName = buildDockerDbContainerName(runtime.envName, dbDialect, runtime.workspaceName);
144
+ const containerName = buildDockerDbContainerName(runtime.envName, dbDialect, runtime.dockerContainerPrefix || runtime.workspaceName);
145
145
  return await dockerStatus(containerName);
146
146
  }
147
147
  export async function runtimeStatus(runtime) {
@@ -303,7 +303,6 @@ Prompt modes:
303
303
  }),
304
304
  'skip-skills': Flags.boolean({
305
305
  description: 'Skip installing or updating NocoBase AI coding skills during init',
306
- hidden: true,
307
306
  default: false,
308
307
  }),
309
308
  'ui-host': Flags.string({
@@ -17,11 +17,14 @@ import { exit } from 'node:process';
17
17
  import { runPromptCatalog, } from "../lib/prompt-catalog.js";
18
18
  import { applyCliLocale, localeText, resolveCliLocale, translateCli, } from "../lib/cli-locale.js";
19
19
  import { resolveConfiguredEnvPath, resolveDefaultConfigScope, resolveEnvRoot, resolveEnvRelativePath, } from '../lib/cli-home.js';
20
+ import { defaultDockerContainerPrefix, defaultDockerNetworkName, } from '../lib/app-runtime.js';
21
+ import { resolveDockerContainerPrefix, resolveDockerNetworkName, } from '../lib/cli-config.js';
20
22
  import { findAvailableTcpPort, validateAvailableTcpPort, validateTcpPort, validateEnvKey, } from "../lib/prompt-validators.js";
23
+ import { validateExternalDbConfig } from "../lib/db-connection-check.js";
21
24
  import { formatMissingManagedAppEnvMessage } from '../lib/app-runtime.js';
22
25
  import { run, runNocoBaseCommand } from '../lib/run-npm.js';
23
26
  import { startTask, stopTask, updateTask } from '../lib/ui.js';
24
- import { ensureWorkspaceName, getEnv, loadAuthConfig, upsertEnv } from '../lib/auth-store.js';
27
+ import { getEnv, upsertEnv } from '../lib/auth-store.js';
25
28
  import { buildStoredEnvConfig } from '../lib/env-config.js';
26
29
  import Download, { defaultDockerRegistryForLang, } from './download.js';
27
30
  import EnvAdd from "./env/add.js";
@@ -180,6 +183,16 @@ function validateBuiltinDbEnabled(value, values) {
180
183
  }
181
184
  return translateCli('commands.install.validation.builtinDbUnsupported', { dialect });
182
185
  }
186
+ async function validateExternalDbPromptField(value, values) {
187
+ const builtinDb = values.builtinDb === undefined ? true : Boolean(values.builtinDb);
188
+ if (builtinDb) {
189
+ return undefined;
190
+ }
191
+ if (typeof value === 'string' && value.trim() === '') {
192
+ return undefined;
193
+ }
194
+ return await validateExternalDbConfig(values);
195
+ }
183
196
  function defaultInstallAppRootPath(envName) {
184
197
  const name = String(envName ?? DEFAULT_INSTALL_ENV_NAME).trim() || DEFAULT_INSTALL_ENV_NAME;
185
198
  return `./${name}/source/`;
@@ -447,6 +460,7 @@ export default class Install extends Command {
447
460
  initialValue: (values) => defaultDbHostForBuiltinDb(values),
448
461
  yesInitialValue: DEFAULT_INSTALL_BUILTIN_DB_HOST,
449
462
  required: true,
463
+ validate: validateExternalDbPromptField,
450
464
  hidden: (values) => Boolean(values.builtinDb),
451
465
  },
452
466
  dbPort: {
@@ -464,6 +478,7 @@ export default class Install extends Command {
464
478
  message: installText('prompts.dbDatabase.message'),
465
479
  initialValue: (values) => defaultDbDatabaseForDialect(values.dbDialect),
466
480
  required: true,
481
+ validate: validateExternalDbPromptField,
467
482
  },
468
483
  dbUser: {
469
484
  type: 'text',
@@ -471,6 +486,7 @@ export default class Install extends Command {
471
486
  initialValue: DEFAULT_INSTALL_DB_USER,
472
487
  yesInitialValue: DEFAULT_INSTALL_DB_USER,
473
488
  required: true,
489
+ validate: validateExternalDbPromptField,
474
490
  },
475
491
  dbPassword: {
476
492
  type: 'password',
@@ -478,6 +494,7 @@ export default class Install extends Command {
478
494
  initialValue: DEFAULT_INSTALL_DB_PASSWORD,
479
495
  yesInitialValue: DEFAULT_INSTALL_DB_PASSWORD,
480
496
  required: true,
497
+ validate: validateExternalDbPromptField,
481
498
  },
482
499
  };
483
500
  static rootUserPrompts = {
@@ -741,6 +758,9 @@ export default class Install extends Command {
741
758
  const builtinDb = values.builtinDb === undefined ? true : Boolean(values.builtinDb);
742
759
  const source = String(values.source ?? '').trim();
743
760
  if (!builtinDb || source === 'docker') {
761
+ if (!builtinDb) {
762
+ return await validateExternalDbConfig({ ...values, dbPort: value });
763
+ }
744
764
  return undefined;
745
765
  }
746
766
  return await Install.validateResumeAwareTcpPort(value, values, 'db');
@@ -765,6 +785,23 @@ export default class Install extends Command {
765
785
  });
766
786
  return reusesManagedPort ? undefined : portError;
767
787
  }
788
+ static async ensureExternalDbReadyForInstall(dbResults) {
789
+ const builtinDb = dbResults.builtinDb === undefined ? true : Boolean(dbResults.builtinDb);
790
+ if (builtinDb) {
791
+ return;
792
+ }
793
+ const dialect = String(dbResults.dbDialect ?? 'postgres').trim() || 'postgres';
794
+ const host = String(dbResults.dbHost ?? '').trim();
795
+ const port = String(dbResults.dbPort ?? '').trim();
796
+ const database = String(dbResults.dbDatabase ?? '').trim();
797
+ const address = host && port ? `${host}:${port}` : host || port || '(unknown address)';
798
+ const target = database ? `${address}/${database}` : address;
799
+ p.log.step(`Checking external ${dialect} database: ${target}`);
800
+ const validationError = await validateExternalDbConfig(dbResults);
801
+ if (validationError) {
802
+ throw new Error(validationError);
803
+ }
804
+ }
768
805
  static async readResumePortValidationContext(values) {
769
806
  if (!Boolean(values.resume)) {
770
807
  return undefined;
@@ -777,24 +814,23 @@ export default class Install extends Command {
777
814
  const builtinDb = values.builtinDb === undefined ? undefined : Boolean(values.builtinDb);
778
815
  const dbDialect = Install.toOptionalPromptString(values.dbDialect);
779
816
  const appRootPath = Install.toOptionalPromptString(values.appRootPath);
780
- const workspaceName = Install.toOptionalPromptString(values.workspaceName)
781
- ?? await Install.resolveResumeWorkspaceName(envName);
817
+ const dockerNetworkName = await Install.resolveResumeDockerNetworkName();
818
+ const dockerContainerPrefix = await Install.resolveResumeDockerContainerPrefix();
782
819
  return {
783
820
  envName,
784
- ...(workspaceName ? { workspaceName } : {}),
821
+ ...(dockerNetworkName ? { dockerNetworkName } : {}),
822
+ ...(dockerContainerPrefix ? { dockerContainerPrefix } : {}),
785
823
  ...(source ? { source } : {}),
786
824
  ...(builtinDb !== undefined ? { builtinDb } : {}),
787
825
  ...(dbDialect ? { dbDialect } : {}),
788
826
  ...(appRootPath ? { appRootPath } : {}),
789
827
  };
790
828
  }
791
- static async resolveResumeWorkspaceName(envName) {
792
- if (!envName) {
793
- return undefined;
794
- }
795
- const config = await loadAuthConfig({ scope: resolveDefaultConfigScope() });
796
- const stored = String(config.name ?? '').trim();
797
- return stored || Install.defaultWorkspaceName();
829
+ static async resolveResumeDockerNetworkName() {
830
+ return await resolveDockerNetworkName({ scope: resolveDefaultConfigScope() });
831
+ }
832
+ static async resolveResumeDockerContainerPrefix() {
833
+ return await resolveDockerContainerPrefix({ scope: resolveDefaultConfigScope() });
798
834
  }
799
835
  static async isResumeManagedPortReuse(params) {
800
836
  if (params.target === 'app') {
@@ -802,13 +838,13 @@ export default class Install extends Command {
802
838
  && params.context.appRootPath) {
803
839
  return await Install.isLocalPm2ProcessUsingPort(params.context.appRootPath, params.port);
804
840
  }
805
- const containerName = Install.buildDockerAppContainerName(params.context.envName, params.context.workspaceName);
841
+ const containerName = Install.buildDockerAppContainerName(params.context.envName, params.context.dockerContainerPrefix);
806
842
  return await Install.isDockerContainerPublishingPort(containerName, params.port);
807
843
  }
808
844
  if (!params.context.builtinDb || params.context.source === 'docker') {
809
845
  return false;
810
846
  }
811
- const containerName = Install.buildBuiltinDbContainerName(params.context.envName, params.context.dbDialect ?? 'postgres', params.context.workspaceName);
847
+ const containerName = Install.buildBuiltinDbContainerName(params.context.envName, params.context.dbDialect ?? 'postgres', params.context.dockerContainerPrefix);
812
848
  return await Install.isDockerContainerPublishingPort(containerName, params.port);
813
849
  }
814
850
  static async isDockerContainerPublishingPort(containerName, port) {
@@ -1148,27 +1184,33 @@ export default class Install extends Command {
1148
1184
  .replace(/^-+|-+$/g, '');
1149
1185
  return normalized || 'nocobase';
1150
1186
  }
1151
- static defaultWorkspaceName() {
1152
- return Install.sanitizeDockerResourceName(`nb-${path.basename(resolveEnvRoot(resolveDefaultConfigScope()))}`);
1187
+ static defaultDockerNetworkName() {
1188
+ return Install.sanitizeDockerResourceName(defaultDockerNetworkName());
1153
1189
  }
1154
- static buildBuiltinDbResourcePrefix(envName, workspaceName) {
1155
- void envName;
1156
- const storedName = String(workspaceName ?? '').trim();
1190
+ static defaultDockerContainerPrefix() {
1191
+ return Install.sanitizeDockerResourceName(defaultDockerContainerPrefix(resolveEnvRoot(resolveDefaultConfigScope())));
1192
+ }
1193
+ static buildBuiltinDbContainerPrefix(containerPrefix) {
1194
+ const storedName = String(containerPrefix ?? '').trim();
1157
1195
  return storedName
1158
1196
  ? Install.sanitizeDockerResourceName(storedName)
1159
- : Install.defaultWorkspaceName();
1197
+ : Install.defaultDockerContainerPrefix();
1160
1198
  }
1161
- static async ensureWorkspaceName() {
1162
- return await ensureWorkspaceName(Install.defaultWorkspaceName(), { scope: resolveDefaultConfigScope() });
1199
+ static buildManagedDockerNetworkName(networkName) {
1200
+ const storedName = String(networkName ?? '').trim();
1201
+ return storedName
1202
+ ? Install.sanitizeDockerResourceName(storedName)
1203
+ : Install.defaultDockerNetworkName();
1163
1204
  }
1164
- static buildBuiltinDbNetworkName(envName, workspaceName) {
1165
- return Install.buildBuiltinDbResourcePrefix(envName, workspaceName);
1205
+ static buildBuiltinDbNetworkName(envName, networkName) {
1206
+ void envName;
1207
+ return Install.buildManagedDockerNetworkName(networkName);
1166
1208
  }
1167
- static buildBuiltinDbContainerName(envName, dbDialect, workspaceName) {
1168
- return Install.sanitizeDockerResourceName(`${Install.buildBuiltinDbResourcePrefix(envName, workspaceName)}-${envName}-${dbDialect}`);
1209
+ static buildBuiltinDbContainerName(envName, dbDialect, containerPrefix) {
1210
+ return Install.sanitizeDockerResourceName(`${Install.buildBuiltinDbContainerPrefix(containerPrefix)}-${envName}-${dbDialect}`);
1169
1211
  }
1170
- static buildDockerAppContainerName(envName, workspaceName) {
1171
- return Install.sanitizeDockerResourceName(`${Install.buildBuiltinDbResourcePrefix(envName, workspaceName)}-${envName}-app`);
1212
+ static buildDockerAppContainerName(envName, containerPrefix) {
1213
+ return Install.sanitizeDockerResourceName(`${Install.buildBuiltinDbContainerPrefix(containerPrefix)}-${envName}-app`);
1172
1214
  }
1173
1215
  static buildInitAppEnvVars(params) {
1174
1216
  const out = {};
@@ -1194,8 +1236,8 @@ export default class Install extends Command {
1194
1236
  const dbPort = String(params.dbPort ?? defaultDbPortForDialect(dbDialect)).trim()
1195
1237
  || defaultDbPortForDialect(dbDialect);
1196
1238
  const defaultDbDatabase = defaultDbDatabaseForDialect(dbDialect);
1197
- const networkName = Install.buildBuiltinDbNetworkName(params.envName, params.workspaceName);
1198
- const containerName = Install.buildBuiltinDbContainerName(params.envName, dbDialect, params.workspaceName);
1239
+ const networkName = Install.buildBuiltinDbNetworkName(params.envName, params.dockerNetworkName ?? params.workspaceName);
1240
+ const containerName = Install.buildBuiltinDbContainerName(params.envName, dbDialect, params.dockerContainerPrefix ?? params.workspaceName);
1199
1241
  const dbHostInput = String(params.dbHost ?? '').trim();
1200
1242
  const dbHost = Install.shouldPublishBuiltinDbPort(params.source)
1201
1243
  ? (dbHostInput
@@ -1485,6 +1527,8 @@ export default class Install extends Command {
1485
1527
  const plan = Install.buildBuiltinDbPlan({
1486
1528
  envName: params.envName,
1487
1529
  workspaceName: params.workspaceName,
1530
+ dockerNetworkName: params.dockerNetworkName,
1531
+ dockerContainerPrefix: params.dockerContainerPrefix,
1488
1532
  storagePath,
1489
1533
  source: params.downloadResults.source,
1490
1534
  dbDialect: params.dbResults.dbDialect,
@@ -1532,7 +1576,7 @@ export default class Install extends Command {
1532
1576
  const dbPassword = String(params.dbResults.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD;
1533
1577
  const appKey = crypto.randomBytes(32).toString('hex');
1534
1578
  const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
1535
- const containerName = Install.buildDockerAppContainerName(params.envName, params.workspaceName);
1579
+ const containerName = Install.buildDockerAppContainerName(params.envName, params.dockerContainerPrefix ?? params.workspaceName);
1536
1580
  const initEnvVars = Install.buildInitAppEnvVars({
1537
1581
  appResults: params.appResults,
1538
1582
  rootResults: params.rootResults,
@@ -1580,11 +1624,12 @@ export default class Install extends Command {
1580
1624
  }
1581
1625
  async installDockerApp(params) {
1582
1626
  const networkName = params.builtinDbPlan?.networkName
1583
- ?? Install.buildBuiltinDbNetworkName(params.envName, params.workspaceName);
1627
+ ?? Install.buildBuiltinDbNetworkName(params.envName, params.dockerNetworkName ?? params.workspaceName);
1584
1628
  await this.ensureDockerNetwork(networkName);
1585
1629
  const plan = Install.buildDockerAppPlan({
1586
1630
  envName: params.envName,
1587
1631
  workspaceName: params.workspaceName,
1632
+ dockerContainerPrefix: params.dockerContainerPrefix,
1588
1633
  appResults: params.appResults,
1589
1634
  downloadResults: params.downloadResults,
1590
1635
  dbResults: params.dbResults,
@@ -2041,9 +2086,13 @@ export default class Install extends Command {
2041
2086
  const source = String(downloadResultsValue(downloadResults, 'source') ?? '').trim();
2042
2087
  const usesDockerResources = Boolean(dbResults.builtinDb)
2043
2088
  || (Boolean(appResults.fetchSource) && source === 'docker');
2044
- const workspaceName = usesDockerResources
2045
- ? await Install.ensureWorkspaceName()
2089
+ const dockerNetworkName = usesDockerResources
2090
+ ? await resolveDockerNetworkName({ scope: resolveDefaultConfigScope() })
2091
+ : undefined;
2092
+ const dockerContainerPrefix = usesDockerResources
2093
+ ? await resolveDockerContainerPrefix({ scope: resolveDefaultConfigScope() })
2046
2094
  : undefined;
2095
+ await Install.ensureExternalDbReadyForInstall(dbResults);
2047
2096
  if (!parsed.resume) {
2048
2097
  await this.saveInstalledEnv({
2049
2098
  envName,
@@ -2053,12 +2102,14 @@ export default class Install extends Command {
2053
2102
  rootResults,
2054
2103
  envAddResults,
2055
2104
  });
2105
+ p.log.info(`Saved install config for env "${envName}"`);
2056
2106
  }
2057
2107
  let builtinDbPlan;
2058
2108
  if (Boolean(dbResults.builtinDb)) {
2059
2109
  builtinDbPlan = await this.startBuiltinDb({
2060
2110
  envName,
2061
- workspaceName,
2111
+ dockerNetworkName,
2112
+ dockerContainerPrefix,
2062
2113
  appResults,
2063
2114
  downloadResults,
2064
2115
  dbResults,
@@ -2082,7 +2133,8 @@ export default class Install extends Command {
2082
2133
  });
2083
2134
  dockerAppPlan = await this.installDockerApp({
2084
2135
  envName,
2085
- workspaceName,
2136
+ dockerNetworkName,
2137
+ dockerContainerPrefix,
2086
2138
  appResults,
2087
2139
  downloadResults,
2088
2140
  dbResults,