@aifabrix/builder 2.47.4 → 2.48.0

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 (42) hide show
  1. package/jest.isolated-projects.js +4 -0
  2. package/jest.projects.js +4 -0
  3. package/lib/api/types/dev.types.js +1 -1
  4. package/lib/app/register.js +20 -18
  5. package/lib/app/rotate-secret.js +39 -16
  6. package/lib/build/index.js +12 -49
  7. package/lib/build/integration-docker-staging.js +35 -0
  8. package/lib/build/standard-docker-build.js +113 -0
  9. package/lib/cli/setup-app.js +4 -0
  10. package/lib/commands/dev-init.js +18 -0
  11. package/lib/commands/setup-prompts-platform-mode.js +11 -1
  12. package/lib/commands/setup.js +1 -0
  13. package/lib/commands/up-dataplane-credentials.js +88 -5
  14. package/lib/core/setup-platform-mode.js +16 -9
  15. package/lib/parameters/infra-kv-discovery.js +4 -3
  16. package/lib/utils/aifabrix-runtime-config-dir.js +2 -1
  17. package/lib/utils/compose-generate-docker-compose.js +2 -1
  18. package/lib/utils/config-paths.js +2 -2
  19. package/lib/utils/dev-ca-docker-augment.js +54 -0
  20. package/lib/utils/docker-daemon-tls-ca.js +326 -0
  21. package/lib/utils/ensure-dev-certs-for-remote-docker.js +5 -0
  22. package/lib/utils/promote-dev-docker-image.js +170 -0
  23. package/lib/utils/remote-docker-env.js +22 -7
  24. package/lib/utils/secrets-helpers.js +9 -14
  25. package/lib/utils/secrets-kv-refs.js +47 -3
  26. package/lib/utils/secrets-missing-error.js +1 -9
  27. package/lib/utils/token-manager-client-credentials.js +11 -1
  28. package/lib/utils/worker-sidecars.js +28 -1
  29. package/package.json +1 -1
  30. package/.temp/jest-protection-convert/run-IFN2nv/.protection/hubspot-companies.json +0 -1
  31. package/.temp/jest-protection-convert/run-IvmKLO/.protection/hubspot-companies.yaml +0 -17
  32. package/.temp/jest-protection-convert/run-SdvblF/.protection/hubspot-companies.json +0 -1
  33. package/.temp/jest-protection-convert/run-b51TsW/.protection/hubspot-companies.json +0 -1
  34. package/.temp/jest-protection-convert/run-fiLmb1/.protection/hubspot-companies.json +0 -1
  35. package/.temp/jest-protection-convert/run-qYGHMU/.protection/hubspot-companies.yaml +0 -17
  36. package/.temp/jest-protection-fixtures/hubspot-Mq5re8/hubspot-companies.yaml +0 -17
  37. package/.temp/jest-protection-fixtures/hubspot-f3u4YL/hubspot-companies.yaml +0 -17
  38. package/.temp/jest-protection-fixtures/hubspot-iQtd1O/hubspot-companies.yaml +0 -17
  39. package/.temp/jest-protection-fixtures/hubspot-mdmz5M/hubspot-companies.yaml +0 -17
  40. package/.temp/jest-protection-fixtures/hubspot-qbKYxQ/hubspot-companies.yaml +0 -17
  41. package/.temp/jest-protection-fixtures/hubspot-yzlJy6/hubspot-companies.yaml +0 -17
  42. package/.temp/validation/summary.txt +0 -5
@@ -38,6 +38,10 @@ const ISOLATED_SUITE_ENTRIES = [
38
38
  'application-frontdoor-paths-contract',
39
39
  ['**/tests/lib/templates/application-frontdoor-paths.contract.test.js']
40
40
  ],
41
+ [
42
+ 'dataplane-env-template-contract',
43
+ ['**/tests/lib/templates/dataplane-env-template.contract.test.js']
44
+ ],
41
45
  ['database-secret-values', ['**/tests/lib/parameters/database-secret-values.test.js']],
42
46
  ['infra-parameter-validate', ['**/tests/lib/parameters/infra-parameter-validate.test.js']],
43
47
  [
package/jest.projects.js CHANGED
@@ -275,6 +275,10 @@ const defaultProject = {
275
275
  '\\\\tests\\\\lib\\\\templates\\\\application-frontdoor-paths.contract.test.js',
276
276
  'lib/templates/application-frontdoor-paths.contract.test.js',
277
277
  'application-frontdoor-paths\\.contract\\.test\\.js',
278
+ '/tests/lib/templates/dataplane-env-template.contract.test.js',
279
+ '\\\\tests\\\\lib\\\\templates\\\\dataplane-env-template.contract.test.js',
280
+ 'lib/templates/dataplane-env-template.contract.test.js',
281
+ 'dataplane-env-template\\.contract\\.test\\.js',
278
282
  '/tests/lib/core/admin-secrets.test.js',
279
283
  '\\\\tests\\\\lib\\\\core\\\\admin-secrets.test.js',
280
284
  'lib/core/admin-secrets.test.js',
@@ -31,7 +31,7 @@
31
31
  * @property {string} aifabrix-env-config - Env config path
32
32
  * @property {string} remote-server - Builder-server base URL
33
33
  * @property {string} docker-endpoint - Docker API endpoint
34
- * @property {boolean} [docker-tls-skip-verify] - When true and ca.pem is absent, Docker uses DOCKER_TLS_VERIFY=0; if ca.pem exists, daemon is always verified
34
+ * @property {boolean} [docker-tls-skip-verify] - When true, Docker uses DOCKER_TLS_VERIFY=0 even if ca.pem exists (client mTLS unchanged)
35
35
  * @property {string} sync-ssh-user - SSH user for Mutagen
36
36
  * @property {string} sync-ssh-host - SSH host for Mutagen
37
37
  */
@@ -78,38 +78,40 @@ function buildRegistrationData(appConfig, options) {
78
78
  }
79
79
 
80
80
  /**
81
- * Save credentials to local secrets if localhost
81
+ * Save pipeline client credentials to ~/.aifabrix/secrets.local.yaml (always).
82
+ * env.template / in-memory resolve validation runs only for localhost controllers.
82
83
  * @async
83
84
  * @param {Object} responseData - Registration response data
84
85
  * @param {string} apiUrl - API URL
85
86
  */
86
87
  async function saveLocalCredentials(responseData, apiUrl) {
87
- if (!isLocalhost(apiUrl)) {
88
- return;
89
- }
90
-
91
88
  const registeredAppKey = responseData.application.key;
92
89
  const clientIdKey = `${registeredAppKey}-client-idKeyVault`;
93
90
  const clientSecretKey = `${registeredAppKey}-client-secretKeyVault`;
91
+ const localhostController = isLocalhost(apiUrl);
94
92
 
95
93
  try {
96
94
  await saveLocalSecret(clientIdKey, responseData.credentials.clientId);
97
95
  await saveLocalSecret(clientSecretKey, responseData.credentials.clientSecret);
98
96
 
99
- // Update env.template (kv:// refs only; no resolved values touch disk)
100
- await updateEnvTemplate(registeredAppKey, clientIdKey, clientSecretKey, apiUrl);
101
-
102
- // Resolve in-memory so any missing-secret / kv:// error surfaces here, but never
103
- // materialize <appPath>/.env or envOutputPath — that is only done by `aifabrix resolve`.
104
- try {
105
- await generateEnvFile(registeredAppKey, null, 'local', true, { noWrite: true });
106
- } catch (error) {
107
- logger.warn(chalk.yellow(`⚠ Could not validate env resolution: ${error.message}`));
97
+ if (localhostController) {
98
+ // Update env.template (kv:// refs only; no resolved values touch disk)
99
+ await updateEnvTemplate(registeredAppKey, clientIdKey, clientSecretKey, apiUrl);
100
+
101
+ // Resolve in-memory so any missing-secret / kv:// error surfaces here, but never
102
+ // materialize <appPath>/.env or envOutputPath — that is only done by `aifabrix resolve`.
103
+ try {
104
+ await generateEnvFile(registeredAppKey, null, 'local', true, { noWrite: true });
105
+ } catch (error) {
106
+ logger.warn(chalk.yellow(`⚠ Could not validate env resolution: ${error.message}`));
107
+ }
108
+
109
+ logger.log(formatSuccessParagraph('Credentials saved to ~/.aifabrix/secrets.local.yaml'));
110
+ logger.log(formatSuccessLine('env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL'));
111
+ logger.log(formatSuccessLine('Run "aifabrix resolve ' + registeredAppKey + '" to materialize an on-disk .env\n'));
112
+ } else {
113
+ logger.log(formatSuccessParagraph('Credentials saved to ~/.aifabrix/secrets.local.yaml\n'));
108
114
  }
109
-
110
- logger.log(formatSuccessParagraph('Credentials saved to ~/.aifabrix/secrets.local.yaml'));
111
- logger.log(formatSuccessLine('env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL'));
112
- logger.log(formatSuccessLine('Run "aifabrix resolve ' + registeredAppKey + '" to materialize an on-disk .env\n'));
113
115
  } catch (error) {
114
116
  logger.warn(chalk.yellow(`⚠ Could not save credentials locally: ${error.message}`));
115
117
  }
@@ -63,16 +63,34 @@ function validateEnvironment(environment) {
63
63
  }
64
64
  }
65
65
 
66
+ /**
67
+ * @param {number} code
68
+ * @param {string} message
69
+ * @param {{ exitOnFailure?: boolean }} [options]
70
+ * @throws {Error}
71
+ */
72
+ function failRotation(code, message, options = {}) {
73
+ if (options.exitOnFailure === false) {
74
+ throw new Error(message);
75
+ }
76
+ logger.error(formatBlockingError(message));
77
+ process.exit(code);
78
+ }
79
+
66
80
  /**
67
81
  * Resolve controller URL and environment from config; exit if controller is missing.
68
82
  * @async
83
+ * @param {{ exitOnFailure?: boolean }} [options]
69
84
  * @returns {Promise<{controllerUrl: string, environment: string}>}
70
85
  */
71
- async function resolveControllerAndEnvironment() {
86
+ async function resolveControllerAndEnvironment(options = {}) {
72
87
  const controllerUrl = await resolveControllerUrl();
73
88
  if (!controllerUrl) {
74
- logger.error(formatBlockingError('Controller URL is required. Run "aifabrix login" to set the controller URL in config.yaml'));
75
- process.exit(1);
89
+ failRotation(
90
+ 1,
91
+ 'Controller URL is required. Run "aifabrix login" to set the controller URL in config.yaml',
92
+ options
93
+ );
76
94
  }
77
95
  const environment = await resolveEnvironment();
78
96
  validateEnvironment(environment);
@@ -197,9 +215,10 @@ function displayRotationResults(appKey, environment, credentials, apiUrl, messag
197
215
  * Get device token from provided controller URL
198
216
  * @async
199
217
  * @param {string} controllerUrl - Controller URL
218
+ * @param {{ exitOnFailure?: boolean }} [options]
200
219
  * @returns {Promise<Object|null>} Object with token and controllerUrl, or null if failed
201
220
  */
202
- async function getTokenFromUrl(controllerUrl) {
221
+ async function getTokenFromUrl(controllerUrl, options = {}) {
203
222
  try {
204
223
  const normalizedUrl = normalizeControllerUrl(controllerUrl);
205
224
  const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
@@ -210,9 +229,7 @@ async function getTokenFromUrl(controllerUrl) {
210
229
  };
211
230
  }
212
231
  } catch (error) {
213
- logger.error(formatBlockingError(`Failed to authenticate with controller: ${controllerUrl}`));
214
- logger.error(chalk.gray(`Error: ${error.message}`));
215
- process.exit(1);
232
+ failRotation(1, `Failed to authenticate with controller: ${controllerUrl} (${error.message})`, options);
216
233
  }
217
234
  return null;
218
235
  }
@@ -222,9 +239,13 @@ async function getTokenFromUrl(controllerUrl) {
222
239
  * @param {string|null} token - Authentication token
223
240
  * @param {string|null} controllerUrl - Controller URL
224
241
  * @param {string} [providedUrl] - Original provided URL for error context
242
+ * @param {{ exitOnFailure?: boolean }} [options]
225
243
  */
226
- function validateAuthToken(token, controllerUrl, providedUrl) {
244
+ function validateAuthToken(token, controllerUrl, providedUrl, options = {}) {
227
245
  if (!token || !controllerUrl) {
246
+ if (options.exitOnFailure === false) {
247
+ throw new Error('No valid authentication found');
248
+ }
228
249
  const formattedError = formatAuthenticationError({
229
250
  controllerUrl: providedUrl || undefined,
230
251
  message: 'No valid authentication found'
@@ -239,16 +260,17 @@ function validateAuthToken(token, controllerUrl, providedUrl) {
239
260
  * @async
240
261
  * @param {string} [controllerUrl] - Optional controller URL
241
262
  * @param {Object} config - Configuration object
263
+ * @param {{ exitOnFailure?: boolean }} [options]
242
264
  * @returns {Promise<Object>} Object with token and actualControllerUrl
243
265
  * @throws {Error} If authentication fails
244
266
  */
245
- async function getRotationAuthToken(controllerUrl, config) {
267
+ async function getRotationAuthToken(controllerUrl, config, options = {}) {
246
268
  let token = null;
247
269
  let actualControllerUrl = null;
248
270
 
249
271
  // If controller URL provided, try to get device token
250
272
  if (controllerUrl) {
251
- const tokenResult = await getTokenFromUrl(controllerUrl);
273
+ const tokenResult = await getTokenFromUrl(controllerUrl, options);
252
274
  if (tokenResult) {
253
275
  token = tokenResult.token;
254
276
  actualControllerUrl = tokenResult.controllerUrl;
@@ -264,7 +286,7 @@ async function getRotationAuthToken(controllerUrl, config) {
264
286
  }
265
287
  }
266
288
 
267
- validateAuthToken(token, actualControllerUrl, controllerUrl);
289
+ validateAuthToken(token, actualControllerUrl, controllerUrl, options);
268
290
  return { token, actualControllerUrl };
269
291
  }
270
292
 
@@ -323,6 +345,9 @@ async function executeRotation(appKey, actualControllerUrl, environment, token,
323
345
 
324
346
  if (!response.success) {
325
347
  const formattedError = response.formattedError || formatApiError(response, actualControllerUrl);
348
+ if (options.exitOnFailure === false) {
349
+ throw new Error(formattedError || `Rotate secret failed for ${appKey}`);
350
+ }
326
351
  logger.error(formattedError);
327
352
  logger.error(chalk.gray(`\nController URL: ${actualControllerUrl}`));
328
353
  process.exit(1);
@@ -351,16 +376,14 @@ async function rotateSecret(appKey, options = {}) {
351
376
  const { ensureSecretsEncryptionKey } = require('../core/config');
352
377
  await ensureSecretsEncryptionKey();
353
378
 
354
- const { controllerUrl, environment } = await resolveControllerAndEnvironment();
379
+ const { controllerUrl, environment } = await resolveControllerAndEnvironment(options);
355
380
  const config = await getConfig();
356
- const { token, actualControllerUrl } = await getRotationAuthToken(controllerUrl, config);
381
+ const { token, actualControllerUrl } = await getRotationAuthToken(controllerUrl, config, options);
357
382
 
358
383
  try {
359
384
  await executeRotation(appKey, actualControllerUrl, environment, token, options);
360
385
  } catch (error) {
361
- logger.error(formatBlockingError(`Failed to rotate secret via controller: ${actualControllerUrl}`));
362
- logger.error(chalk.gray(`Error: ${error.message}`));
363
- process.exit(1);
386
+ failRotation(1, `Failed to rotate secret via controller: ${actualControllerUrl} (${error.message})`, options);
364
387
  }
365
388
  }
366
389
 
@@ -1,6 +1,5 @@
1
1
  const {
2
2
  formatSuccessLine,
3
- formatSuccessParagraph,
4
3
  formatProgress,
5
4
  formatWarningLine,
6
5
  sectionTitle,
@@ -31,12 +30,10 @@ const secrets = require('../core/secrets');
31
30
  const config = require('../core/config');
32
31
  const logger = require('../utils/logger');
33
32
  const dockerfileUtils = require('../utils/dockerfile-utils');
34
- const dockerBuild = require('../utils/docker-build');
35
33
  const buildCopy = require('../utils/build-copy');
36
34
  const { resolveBuildDockerTagNames } = require('../utils/build-resolve-image');
37
35
  const buildHelpers = require('../utils/build-helpers');
38
- const { ensureRunSecretsForApp } = require('../app/run-env-compose');
39
- const { resolveDockerBuildArgsForApp } = require('./docker-build-args');
36
+ const { runStandardDockerBuild } = require('./standard-docker-build');
40
37
 
41
38
  /**
42
39
  * Loads application config for an application
@@ -353,18 +350,6 @@ function prepareBuildContext(buildConfig, devDir) {
353
350
  return contextPath;
354
351
  }
355
352
 
356
- /**
357
- * Handle Dockerfile generation
358
- * @async
359
- * @param {string} appName - Application name
360
- * @param {Object} params - Parameters
361
- * @param {string} params.devDir - Developer directory
362
- * @param {Object} params.buildConfig - Build configuration
363
- * @param {string} params.contextPath - Build context path
364
- * @param {Object} params.appConfig - Application configuration
365
- * @param {Object} options - Build options
366
- * @returns {Promise<string>} Dockerfile path
367
- */
368
353
  async function handleDockerfileGeneration(appName, params, options, buildHelpers) {
369
354
  const { devDir, buildConfig, contextPath, appConfig } = params;
370
355
  const appDockerfilePath = path.join(devDir, 'Dockerfile');
@@ -393,38 +378,6 @@ async function handleDockerfileGeneration(appName, params, options, buildHelpers
393
378
  }, generateDockerfile);
394
379
  }
395
380
 
396
- /**
397
- * Standard Docker build path after CLI header is printed (non-external apps).
398
- * @param {string} appName
399
- * @param {Object} options
400
- * @returns {Promise<string>} Built image ref e.g. name:tag
401
- */
402
- async function runStandardDockerBuild(appName, options) {
403
- const { buildConfig } = await buildHelpers.loadAndValidateConfig(appName);
404
- const { emitManifestMetadataLineIfTTY } = require('../utils/manifest-source-emit');
405
- const { appPath } = await detectAppType(appName);
406
- emitManifestMetadataLineIfTTY(logger, { appKey: appName, appPath, envOnly: false, json: false });
407
- const { devDir, effectiveImageName, imageName, appConfig } = await prepareDevDirectory(appName, buildConfig, options);
408
- const contextPath = prepareBuildContext(buildConfig, devDir);
409
- const dockerfilePath = await handleDockerfileGeneration(appName, {
410
- devDir,
411
- buildConfig,
412
- contextPath,
413
- appConfig
414
- }, options, buildHelpers);
415
- await ensureRunSecretsForApp(appName);
416
- const buildArgs = await resolveDockerBuildArgsForApp(appName, contextPath);
417
- const buildOptions = { ...options, buildArgs };
418
- const tag = options.tag || 'latest';
419
- if (typeof tag === 'string' && tag.includes(',')) {
420
- throw new Error('Use a single image tag per build (comma-separated multiple tags are not supported).');
421
- }
422
- await dockerBuild.executeDockerBuildWithTag(effectiveImageName, imageName, dockerfilePath, contextPath, tag, buildOptions);
423
- await postBuildTasks(appName, buildConfig);
424
- logger.log(formatSuccessParagraph('Build completed successfully!'));
425
- return `${effectiveImageName}:${tag}`;
426
- }
427
-
428
381
  /**
429
382
  * Builds a container image for the specified application
430
383
  * Auto-detects runtime and generates Dockerfile if needed
@@ -458,4 +411,14 @@ async function buildApp(appName, options = {}) {
458
411
  throw new Error(`Build failed: ${error.message}`);
459
412
  }
460
413
  }
461
- module.exports = { loadVariablesYaml, resolveContextPath, detectLanguage, generateDockerfile, buildApp, postBuildTasks };
414
+ module.exports = {
415
+ loadVariablesYaml,
416
+ resolveContextPath,
417
+ detectLanguage,
418
+ generateDockerfile,
419
+ buildApp,
420
+ postBuildTasks,
421
+ prepareDevDirectory,
422
+ prepareBuildContext,
423
+ handleDockerfileGeneration
424
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Pre-build integration manifest staging for Docker context.
3
+ * @fileoverview Runs scripts/stage_integration_for_docker.py when present in build context
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const fsSync = require('fs');
9
+ const path = require('path');
10
+ const { promisify } = require('util');
11
+ const { exec } = require('child_process');
12
+ const logger = require('../utils/logger');
13
+ const {
14
+ formatProgress,
15
+ formatSuccessLine
16
+ } = require('../utils/cli-test-layout-chalk');
17
+
18
+ const run = promisify(exec);
19
+
20
+ /**
21
+ * When repo ships scripts/stage_integration_for_docker.py, run it before docker build.
22
+ * @param {string} contextPath
23
+ * @returns {Promise<void>}
24
+ */
25
+ async function maybeStageIntegrationForDockerBuild(contextPath) {
26
+ const scriptPath = path.join(contextPath, 'scripts/stage_integration_for_docker.py');
27
+ if (!fsSync.existsSync(scriptPath)) {
28
+ return;
29
+ }
30
+ logger.log(formatProgress('Staging integration runtime manifests for Docker…'));
31
+ await run('python3 scripts/stage_integration_for_docker.py', { cwd: contextPath });
32
+ logger.log(formatSuccessLine('Integration staging ready'));
33
+ }
34
+
35
+ module.exports = { maybeStageIntegrationForDockerBuild };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Standard Docker build orchestration (dev-promote shortcut + full build).
3
+ * @fileoverview Extracted from lib/build/index.js for file-size limits
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const chalk = require('chalk');
9
+ const { detectAppType } = require('../utils/paths');
10
+ const logger = require('../utils/logger');
11
+ const dockerBuild = require('../utils/docker-build');
12
+ const buildHelpers = require('../utils/build-helpers');
13
+ const { ensureRunSecretsForApp } = require('../app/run-env-compose');
14
+ const { formatSuccessParagraph } = require('../utils/cli-test-layout-chalk');
15
+ const { resolveDockerBuildArgsForApp } = require('./docker-build-args');
16
+ const { maybeStageIntegrationForDockerBuild } = require('./integration-docker-staging');
17
+
18
+ /**
19
+ * Lazy require breaks circular dependency with lib/build/index.js.
20
+ * @returns {Object}
21
+ */
22
+ function getBuildIndexDeps() {
23
+ return require('./index');
24
+ }
25
+
26
+ /**
27
+ * When --from-dev --base: tag dev image as base and skip docker build when possible.
28
+ * @param {string} appName
29
+ * @param {Object} options
30
+ * @returns {Promise<string|null>} image ref when promote completed; null to continue full build
31
+ */
32
+ async function tryCompleteDevPromoteBuild(appName, options) {
33
+ const { shouldAttemptDevPromote, tryPromoteDevImageToBase } = require('../utils/promote-dev-docker-image');
34
+ if (!shouldAttemptDevPromote(options)) {
35
+ return null;
36
+ }
37
+ const promotedRef = await tryPromoteDevImageToBase(appName, options);
38
+ if (!promotedRef) {
39
+ logger.log(chalk.blue('No local dev-scoped image found; building base image...'));
40
+ return null;
41
+ }
42
+ const { buildConfig } = await buildHelpers.loadAndValidateConfig(appName);
43
+ const { postBuildTasks } = getBuildIndexDeps();
44
+ await postBuildTasks(appName, buildConfig);
45
+ logger.log(formatSuccessParagraph('Build completed successfully! (promoted from dev image)'));
46
+ return promotedRef;
47
+ }
48
+
49
+ /**
50
+ * Run docker build after workspace prep (shared by standard build path).
51
+ * @param {string} appName
52
+ * @param {Object} options
53
+ * @param {Object} buildConfig
54
+ * @returns {Promise<string>}
55
+ */
56
+ async function executeDockerImageBuild(appName, options, buildConfig) {
57
+ const { emitManifestMetadataLineIfTTY } = require('../utils/manifest-source-emit');
58
+ const {
59
+ prepareDevDirectory,
60
+ prepareBuildContext,
61
+ handleDockerfileGeneration,
62
+ postBuildTasks
63
+ } = getBuildIndexDeps();
64
+ const { appPath } = await detectAppType(appName);
65
+ emitManifestMetadataLineIfTTY(logger, { appKey: appName, appPath, envOnly: false, json: false });
66
+ const { devDir, effectiveImageName, imageName, appConfig } = await prepareDevDirectory(
67
+ appName,
68
+ buildConfig,
69
+ options
70
+ );
71
+ const contextPath = prepareBuildContext(buildConfig, devDir);
72
+ await maybeStageIntegrationForDockerBuild(contextPath);
73
+ const dockerfilePath = await handleDockerfileGeneration(
74
+ appName,
75
+ { devDir, buildConfig, contextPath, appConfig },
76
+ options,
77
+ buildHelpers
78
+ );
79
+ await ensureRunSecretsForApp(appName);
80
+ const buildArgs = await resolveDockerBuildArgsForApp(appName, contextPath);
81
+ const tag = options.tag || 'latest';
82
+ if (typeof tag === 'string' && tag.includes(',')) {
83
+ throw new Error('Use a single image tag per build (comma-separated multiple tags are not supported).');
84
+ }
85
+ await dockerBuild.executeDockerBuildWithTag(
86
+ effectiveImageName,
87
+ imageName,
88
+ dockerfilePath,
89
+ contextPath,
90
+ tag,
91
+ { ...options, buildArgs }
92
+ );
93
+ await postBuildTasks(appName, buildConfig);
94
+ logger.log(formatSuccessParagraph('Build completed successfully!'));
95
+ return `${effectiveImageName}:${tag}`;
96
+ }
97
+
98
+ /**
99
+ * Standard Docker build path after CLI header is printed (non-external apps).
100
+ * @param {string} appName
101
+ * @param {Object} options
102
+ * @returns {Promise<string>} Built image ref e.g. name:tag
103
+ */
104
+ async function runStandardDockerBuild(appName, options) {
105
+ const promotedRef = await tryCompleteDevPromoteBuild(appName, options);
106
+ if (promotedRef) {
107
+ return promotedRef;
108
+ }
109
+ const { buildConfig } = await buildHelpers.loadAndValidateConfig(appName);
110
+ return executeDockerImageBuild(appName, options, buildConfig);
111
+ }
112
+
113
+ module.exports = { runStandardDockerBuild };
@@ -326,6 +326,10 @@ function setupBuildRunLogsDownCommands(program) {
326
326
  '--base',
327
327
  'Build/run the manifest base image (no -devN suffix). Default local build uses a developer-scoped repository when developer-id > 0.'
328
328
  )
329
+ .option(
330
+ '--from-dev',
331
+ 'With --base: tag the existing developer-scoped image as the base image instead of rebuilding when possible'
332
+ )
329
333
  .action(async(appName, options) => {
330
334
  try {
331
335
  const imageTag = await app.buildApp(appName, options);
@@ -16,6 +16,7 @@ const {
16
16
  getCertDir,
17
17
  readClientCertPem,
18
18
  readClientKeyPem,
19
+ readServerCaPem,
19
20
  getCertValidNotAfter,
20
21
  normalizePemNewlines,
21
22
  mergeCaPemBlocks
@@ -40,6 +41,7 @@ const {
40
41
  formatEnsureServerTrustedFailure
41
42
  } = require('../utils/dev-init-health-messages');
42
43
  const { getBadRequestHint, logCertTroubleshootingHint } = require('../utils/dev-init-cert-hints');
44
+ const { augmentAndSaveCaPemForRemoteDocker } = require('../utils/dev-ca-docker-augment');
43
45
 
44
46
  /**
45
47
  * Install dev CA into the OS trust store. On Linux without sudo, log and return false so the caller can use in-process PEM.
@@ -371,6 +373,11 @@ async function runDevInit(options) {
371
373
  await saveCertAndConfig(configDir, devId, issueResponse.certificate, keyPem, caPem);
372
374
 
373
375
  await applySettingsFromServer(baseUrl, devId, issueResponse, keyPem, serverCaPemForTls || undefined);
376
+ await augmentAndSaveCaPemForRemoteDocker(configDir, devId, caPem, {
377
+ settings: issueResponse.settings,
378
+ remoteServerUrl: baseUrl,
379
+ warn: (msg) => logger.log(chalk.yellow(` ⚠ ${msg}`))
380
+ });
374
381
  await _runSshKeyRegistrationStep(baseUrl, issueResponse, keyPem, configDir, devId, serverCaPemForTls || undefined);
375
382
  const sshInfo = await mergeDevSshConfigAfterInit(baseUrl, devId);
376
383
  logOnboardingFinished(sshInfo);
@@ -413,6 +420,11 @@ async function runCertificateRefresh(auth) {
413
420
  const caPem = mergeCaPemBlocks(serverCaPem, issueResponse.caCertificate, issueResponse.ca);
414
421
  await saveCertAndConfig(configDir, devId, issueResponse.certificate, keyPem, caPem);
415
422
  await applySettingsFromServer(auth.serverUrl, devId, issueResponse, keyPem, serverCaPem);
423
+ await augmentAndSaveCaPemForRemoteDocker(configDir, devId, caPem, {
424
+ settings: issueResponse.settings,
425
+ remoteServerUrl: auth.serverUrl,
426
+ warn: (msg) => logger.log(chalk.yellow(` ⚠ ${msg}`))
427
+ });
416
428
  logger.log(formatSuccessLine('Certificate refreshed and config updated from server.\n'));
417
429
  }
418
430
 
@@ -433,6 +445,12 @@ async function fetchAndMergeRemoteSettings(auth, clientCertPem, clientKeyPem, de
433
445
  auth.serverCaPem || undefined
434
446
  );
435
447
  await config.mergeRemoteSettings(settings);
448
+ const existingCa = readServerCaPem(getCertDir(getConfigDirForPaths(), devId));
449
+ await augmentAndSaveCaPemForRemoteDocker(getConfigDirForPaths(), devId, existingCa, {
450
+ settings,
451
+ remoteServerUrl: auth.serverUrl,
452
+ warn: (msg) => logger.log(chalk.yellow(` ⚠ ${msg}`))
453
+ });
436
454
  logger.log(formatSuccessLine('Config updated from server.\n'));
437
455
  await displayDevConfig(devId);
438
456
  }
@@ -9,12 +9,14 @@
9
9
  'use strict';
10
10
 
11
11
  const inquirer = require('inquirer');
12
+ const config = require('../core/config');
12
13
  const {
13
14
  normalizeSetupPlatformMode,
14
15
  SETUP_PLATFORM_MODE,
15
16
  SETUP_PLATFORM_MODE_CLI_VALUES,
16
17
  getSetupPlatformModePromptChoices
17
18
  } = require('../core/setup-platform-mode');
19
+ const { isRemoteServerConfigured } = require('../utils/applications-config-defaults');
18
20
 
19
21
  /**
20
22
  * Resolve platform mode from CLI options, if provided.
@@ -51,12 +53,20 @@ async function promptSetupPlatformMode(options = {}) {
51
53
  const fromCli = resolvePlatformModeFromOptions(options);
52
54
  if (fromCli) return fromCli;
53
55
 
56
+ const remoteServer = await config.getRemoteServer();
57
+ const choices = getSetupPlatformModePromptChoices({
58
+ remoteServerConfigured: isRemoteServerConfigured(remoteServer)
59
+ });
60
+ if (choices.length === 1) {
61
+ return SETUP_PLATFORM_MODE.SINGLE;
62
+ }
63
+
54
64
  const { mode } = await inquirer.prompt([
55
65
  {
56
66
  type: 'list',
57
67
  name: 'mode',
58
68
  message: 'Platform mode:',
59
- choices: getSetupPlatformModePromptChoices(),
69
+ choices,
60
70
  default: SETUP_PLATFORM_MODE.SINGLE
61
71
  }
62
72
  ]);
@@ -65,6 +65,7 @@ async function applySetupPlatformMode(options) {
65
65
  // These are the user gates that control full-mode behavior.
66
66
  const cfg = await config.getConfig();
67
67
  cfg.traefik = derived.traefik;
68
+ cfg.tlsEnabled = derived.tlsEnabled;
68
69
  cfg.useEnvironmentScopedResources = derived.useEnvironmentScopedResources;
69
70
  await config.saveConfig(cfg);
70
71
  await syncPlatformControllerUrlsInConfig();