@aifabrix/builder 2.48.0 → 2.49.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 (90) hide show
  1. package/.temp/jest-protection-convert/run-IFN2nv/.protection/hubspot-companies.json +1 -0
  2. package/.temp/jest-protection-convert/run-IvmKLO/.protection/hubspot-companies.yaml +17 -0
  3. package/.temp/jest-protection-convert/run-SdvblF/.protection/hubspot-companies.json +1 -0
  4. package/.temp/jest-protection-convert/run-b51TsW/.protection/hubspot-companies.json +1 -0
  5. package/.temp/jest-protection-convert/run-fiLmb1/.protection/hubspot-companies.json +1 -0
  6. package/.temp/jest-protection-convert/run-qYGHMU/.protection/hubspot-companies.yaml +17 -0
  7. package/.temp/jest-protection-fixtures/hubspot-Mq5re8/hubspot-companies.yaml +17 -0
  8. package/.temp/jest-protection-fixtures/hubspot-f3u4YL/hubspot-companies.yaml +17 -0
  9. package/.temp/jest-protection-fixtures/hubspot-iQtd1O/hubspot-companies.yaml +17 -0
  10. package/.temp/jest-protection-fixtures/hubspot-mdmz5M/hubspot-companies.yaml +17 -0
  11. package/.temp/jest-protection-fixtures/hubspot-qbKYxQ/hubspot-companies.yaml +17 -0
  12. package/.temp/jest-protection-fixtures/hubspot-yzlJy6/hubspot-companies.yaml +17 -0
  13. package/.temp/validation/summary.txt +5 -0
  14. package/jest.isolated-projects.js +3 -0
  15. package/jest.projects.js +12 -0
  16. package/lib/api/applications.api.js +23 -0
  17. package/lib/api/dev.api.js +1 -1
  18. package/lib/api/index.js +5 -2
  19. package/lib/api/types/dev.types.js +3 -2
  20. package/lib/app/deploy-config.js +6 -4
  21. package/lib/app/register.js +1 -1
  22. package/lib/app/run-container-start.js +0 -9
  23. package/lib/app/run-helpers.js +8 -4
  24. package/lib/app/run-reload-sync.js +16 -18
  25. package/lib/app/run-stop-containers.js +48 -5
  26. package/lib/app/run.js +6 -3
  27. package/lib/app/show.js +2 -2
  28. package/lib/cli/infra-guided-auth.js +42 -6
  29. package/lib/cli/infra-guided-footers.js +164 -0
  30. package/lib/cli/infra-guided-spinners.js +57 -0
  31. package/lib/cli/infra-guided.js +41 -179
  32. package/lib/cli/setup-app.js +1 -1
  33. package/lib/cli/setup-dev.js +3 -2
  34. package/lib/cli/setup-infra.js +3 -2
  35. package/lib/cli/setup-platform.js +3 -0
  36. package/lib/commands/login-device.js +6 -2
  37. package/lib/commands/login.js +4 -1
  38. package/lib/commands/setup-modes.js +19 -3
  39. package/lib/commands/setup-platform-auth.js +25 -0
  40. package/lib/commands/setup-prompts-admin.js +9 -0
  41. package/lib/commands/setup.js +60 -8
  42. package/lib/commands/up-dataplane-bootstrap.js +149 -0
  43. package/lib/commands/up-dataplane-credentials.js +19 -4
  44. package/lib/commands/up-dataplane.js +74 -11
  45. package/lib/core/secrets-env-declarative-expand.js +35 -23
  46. package/lib/core/setup-platform-mode.js +43 -0
  47. package/lib/deployment/deployer-status.js +26 -3
  48. package/lib/deployment/deployer.js +7 -14
  49. package/lib/external-system/download-helpers.js +10 -1
  50. package/lib/infrastructure/compose.js +40 -12
  51. package/lib/infrastructure/services.js +15 -3
  52. package/lib/infrastructure/traefik-tls-dynamic.js +88 -0
  53. package/lib/utils/app-bootstrap-api.js +53 -0
  54. package/lib/utils/app-run-containers.js +14 -3
  55. package/lib/utils/applications-config-defaults.js +19 -3
  56. package/lib/utils/compose-generate-docker-compose.js +16 -5
  57. package/lib/utils/compose-generator.js +34 -15
  58. package/lib/utils/compose-traefik-ingress-base.js +13 -14
  59. package/lib/utils/config-paths.js +6 -1
  60. package/lib/utils/dev-user-groups.js +1 -0
  61. package/lib/utils/docker-daemon-tls-ca.js +94 -57
  62. package/lib/utils/docker-reload-mount.js +12 -1
  63. package/lib/utils/external-env-template.js +53 -18
  64. package/lib/utils/health-check-poll-ui.js +84 -0
  65. package/lib/utils/health-check-url.js +54 -34
  66. package/lib/utils/health-check.js +43 -16
  67. package/lib/utils/infra-optional-service-flags.js +31 -2
  68. package/lib/utils/mutagen.js +7 -4
  69. package/lib/utils/platform-controller-url.js +23 -1
  70. package/lib/utils/refresh-remote-settings.js +67 -0
  71. package/lib/utils/remote-host-for-front-door.js +43 -0
  72. package/lib/utils/run-cli-flags.js +37 -1
  73. package/lib/utils/url-declarative-public-base.js +11 -4
  74. package/lib/utils/url-declarative-resolve-build-urls.js +11 -2
  75. package/lib/utils/url-declarative-resolve-expand-token.js +12 -1
  76. package/lib/utils/url-declarative-resolve-load-doc.js +30 -11
  77. package/lib/utils/url-declarative-resolve.js +2 -0
  78. package/lib/utils/urls-local-registry-scan.js +95 -10
  79. package/lib/utils/urls-local-registry.js +24 -1
  80. package/lib/utils/worker-sidecars.js +2 -2
  81. package/lib/validation/external-system-auth-rules.js +45 -7
  82. package/package.json +2 -1
  83. package/scripts/test-dataplane-bootstrap.js +331 -0
  84. package/templates/applications/dataplane/env.template +22 -5
  85. package/templates/applications/dataplane/rbac.yaml +11 -0
  86. package/templates/applications/miso-controller/application.yaml +1 -1
  87. package/templates/applications/miso-controller/env.template +3 -0
  88. package/templates/infra/compose.yaml.hbs +13 -9
  89. package/templates/python/docker-compose.hbs +3 -0
  90. package/templates/typescript/docker-compose.hbs +3 -0
@@ -0,0 +1 @@
1
+ {"apiVersion":"dataplane.aifabrix.ai/v1","kind":"Protection","metadata":{"key":"hubspot-country-sales","displayName":"HubSpot Country Sales Access"},"spec":{"enabled":true,"datasourceKey":"hubspot-companies","mode":"replaceForSource","rules":[{"key":"sales-country-users","principal":{"type":"group","expression":"Sales {{fk.country.metadata.iso3}} Users"},"grants":[{"dimensionKey":"country","valueExpression":"{{fk.country.metadata.iso2}}"}]}]}}
@@ -0,0 +1,17 @@
1
+ apiVersion: dataplane.aifabrix.ai/v1
2
+ kind: Protection
3
+ metadata:
4
+ key: hubspot-country-sales
5
+ displayName: HubSpot Country Sales Access
6
+ spec:
7
+ enabled: true
8
+ datasourceKey: hubspot-companies
9
+ mode: replaceForSource
10
+ rules:
11
+ - key: sales-country-users
12
+ principal:
13
+ type: group
14
+ expression: "Sales {{fk.country.metadata.iso3}} Users"
15
+ grants:
16
+ - dimensionKey: country
17
+ valueExpression: "{{fk.country.metadata.iso2}}"
@@ -0,0 +1 @@
1
+ {"apiVersion":"dataplane.aifabrix.ai/v1","kind":"Protection","metadata":{"key":"hubspot-country-sales","displayName":"HubSpot Country Sales Access"},"spec":{"enabled":true,"datasourceKey":"hubspot-companies","mode":"replaceForSource","rules":[{"key":"sales-country-users","principal":{"type":"group","expression":"Sales {{fk.country.metadata.iso3}} Users"},"grants":[{"dimensionKey":"country","valueExpression":"{{fk.country.metadata.iso2}}"}]}]}}
@@ -0,0 +1,17 @@
1
+ apiVersion: dataplane.aifabrix.ai/v1
2
+ kind: Protection
3
+ metadata:
4
+ key: hubspot-country-sales
5
+ displayName: HubSpot Country Sales Access
6
+ spec:
7
+ enabled: true
8
+ datasourceKey: hubspot-companies
9
+ mode: replaceForSource
10
+ rules:
11
+ - key: sales-country-users
12
+ principal:
13
+ type: group
14
+ expression: "Sales {{fk.country.metadata.iso3}} Users"
15
+ grants:
16
+ - dimensionKey: country
17
+ valueExpression: "{{fk.country.metadata.iso2}}"
@@ -0,0 +1,17 @@
1
+ apiVersion: dataplane.aifabrix.ai/v1
2
+ kind: Protection
3
+ metadata:
4
+ key: hubspot-country-sales
5
+ displayName: HubSpot Country Sales Access
6
+ spec:
7
+ enabled: true
8
+ datasourceKey: hubspot-companies
9
+ mode: replaceForSource
10
+ rules:
11
+ - key: sales-country-users
12
+ principal:
13
+ type: group
14
+ expression: "Sales {{fk.country.metadata.iso3}} Users"
15
+ grants:
16
+ - dimensionKey: country
17
+ valueExpression: "{{fk.country.metadata.iso2}}"
@@ -0,0 +1,17 @@
1
+ apiVersion: dataplane.aifabrix.ai/v1
2
+ kind: Protection
3
+ metadata:
4
+ key: hubspot-country-sales
5
+ displayName: HubSpot Country Sales Access
6
+ spec:
7
+ enabled: true
8
+ datasourceKey: hubspot-companies
9
+ mode: replaceForSource
10
+ rules:
11
+ - key: sales-country-users
12
+ principal:
13
+ type: group
14
+ expression: "Sales {{fk.country.metadata.iso3}} Users"
15
+ grants:
16
+ - dimensionKey: country
17
+ valueExpression: "{{fk.country.metadata.iso2}}"
@@ -0,0 +1,17 @@
1
+ apiVersion: dataplane.aifabrix.ai/v1
2
+ kind: Protection
3
+ metadata:
4
+ key: hubspot-country-sales
5
+ displayName: HubSpot Country Sales Access
6
+ spec:
7
+ enabled: true
8
+ datasourceKey: hubspot-companies
9
+ mode: replaceForSource
10
+ rules:
11
+ - key: sales-country-users
12
+ principal:
13
+ type: group
14
+ expression: "Sales {{fk.country.metadata.iso3}} Users"
15
+ grants:
16
+ - dimensionKey: country
17
+ valueExpression: "{{fk.country.metadata.iso2}}"
@@ -0,0 +1,17 @@
1
+ apiVersion: dataplane.aifabrix.ai/v1
2
+ kind: Protection
3
+ metadata:
4
+ key: hubspot-country-sales
5
+ displayName: HubSpot Country Sales Access
6
+ spec:
7
+ enabled: true
8
+ datasourceKey: hubspot-companies
9
+ mode: replaceForSource
10
+ rules:
11
+ - key: sales-country-users
12
+ principal:
13
+ type: group
14
+ expression: "Sales {{fk.country.metadata.iso3}} Users"
15
+ grants:
16
+ - dimensionKey: country
17
+ valueExpression: "{{fk.country.metadata.iso2}}"
@@ -0,0 +1,17 @@
1
+ apiVersion: dataplane.aifabrix.ai/v1
2
+ kind: Protection
3
+ metadata:
4
+ key: hubspot-country-sales
5
+ displayName: HubSpot Country Sales Access
6
+ spec:
7
+ enabled: true
8
+ datasourceKey: hubspot-companies
9
+ mode: replaceForSource
10
+ rules:
11
+ - key: sales-country-users
12
+ principal:
13
+ type: group
14
+ expression: "Sales {{fk.country.metadata.iso3}} Users"
15
+ grants:
16
+ - dimensionKey: country
17
+ valueExpression: "{{fk.country.metadata.iso2}}"
@@ -0,0 +1,17 @@
1
+ apiVersion: dataplane.aifabrix.ai/v1
2
+ kind: Protection
3
+ metadata:
4
+ key: hubspot-country-sales
5
+ displayName: HubSpot Country Sales Access
6
+ spec:
7
+ enabled: true
8
+ datasourceKey: hubspot-companies
9
+ mode: replaceForSource
10
+ rules:
11
+ - key: sales-country-users
12
+ principal:
13
+ type: group
14
+ expression: "Sales {{fk.country.metadata.iso3}} Users"
15
+ grants:
16
+ - dimensionKey: country
17
+ valueExpression: "{{fk.country.metadata.iso2}}"
@@ -0,0 +1,5 @@
1
+ lint:fix exit=0
2
+ lint exit=0
3
+ test exit=0
4
+ miso typecheck exit=0
5
+ miso build exit=0
@@ -60,6 +60,8 @@ const ISOLATED_SUITE_ENTRIES = [
60
60
  ['secrets-ensure-catalog-fallback', ['**/tests/lib/core/secrets-ensure-catalog-fallback.test.js']],
61
61
  ['secrets-ensure', ['**/tests/lib/core/secrets-ensure.test.js']],
62
62
  ['url-declarative-vdir-inactive-env', ['**/tests/lib/utils/url-declarative-vdir-inactive-env.test.js']],
63
+ ['url-declarative-resolve-load-doc', ['**/tests/lib/utils/url-declarative-resolve-load-doc.test.js']],
64
+ ['compose-celery-worker', ['**/tests/lib/utils/compose-celery-worker.test.js']],
63
65
  [
64
66
  'url-declarative-user-cfg-per-app-proxy',
65
67
  ['**/tests/lib/utils/url-declarative-user-cfg-per-app-proxy.test.js']
@@ -79,6 +81,7 @@ const ISOLATED_SUITE_ENTRIES = [
79
81
  ['infra-kv-discovery', ['**/tests/lib/parameters/infra-kv-discovery.test.js']],
80
82
  ['infra-env-defaults', ['**/tests/lib/utils/infra-env-defaults.test.js']],
81
83
  ['compose-traefik-template', ['**/tests/lib/infrastructure/compose-traefik-template.test.js']],
84
+ ['traefik-tls-dynamic', ['**/tests/lib/infrastructure/traefik-tls-dynamic.test.js']],
82
85
  ['generator-external-rbac', ['**/tests/lib/generator/generator-external-rbac.test.js']],
83
86
  ['helpers-ensure-admin-secrets', ['**/tests/lib/infrastructure/helpers-ensure-admin-secrets.test.js']],
84
87
  ['secrets-generator', ['**/tests/lib/utils/secrets-generator.test.js']],
package/jest.projects.js CHANGED
@@ -156,6 +156,14 @@ const defaultProject = {
156
156
  '\\\\tests\\\\lib\\\\utils\\\\url-declarative-vdir-inactive-env.test.js',
157
157
  'lib/utils/url-declarative-vdir-inactive-env.test.js',
158
158
  'url-declarative-vdir-inactive-env\\.test\\.js',
159
+ '/tests/lib/utils/url-declarative-resolve-load-doc.test.js',
160
+ '\\\\tests\\\\lib\\\\utils\\\\url-declarative-resolve-load-doc.test.js',
161
+ 'lib/utils/url-declarative-resolve-load-doc.test.js',
162
+ 'url-declarative-resolve-load-doc\\.test\\.js',
163
+ '/tests/lib/utils/compose-celery-worker.test.js',
164
+ '\\\\tests\\\\lib\\\\utils\\\\compose-celery-worker.test.js',
165
+ 'lib/utils/compose-celery-worker.test.js',
166
+ 'compose-celery-worker\\.test\\.js',
159
167
  '/tests/lib/utils/url-declarative-user-cfg-per-app-proxy.test.js',
160
168
  '\\\\tests\\\\lib\\\\utils\\\\url-declarative-user-cfg-per-app-proxy.test.js',
161
169
  'lib/utils/url-declarative-user-cfg-per-app-proxy.test.js',
@@ -200,6 +208,10 @@ const defaultProject = {
200
208
  '\\\\tests\\\\lib\\\\infrastructure\\\\compose-traefik-template.test.js',
201
209
  'lib/infrastructure/compose-traefik-template.test.js',
202
210
  'compose-traefik-template\\.test\\.js',
211
+ '/tests/lib/infrastructure/traefik-tls-dynamic.test.js',
212
+ '\\\\tests\\\\lib\\\\infrastructure\\\\traefik-tls-dynamic.test.js',
213
+ 'lib/infrastructure/traefik-tls-dynamic.test.js',
214
+ 'traefik-tls-dynamic\\.test\\.js',
203
215
  '/tests/lib/utils/paths-app-listing.test.js',
204
216
  '\\\\tests\\\\lib\\\\utils\\\\paths-app-listing.test.js',
205
217
  'lib/utils/paths-app-listing.test.js',
@@ -142,6 +142,28 @@ async function registerApplication(controllerUrl, envKey, authConfig, registrati
142
142
  });
143
143
  }
144
144
 
145
+ /**
146
+ * Bootstrap application in an environment (install-time, ENCRYPTION_KEY header).
147
+ * POST /api/v1/environments/{envKey}/applications/bootstrap
148
+ * @async
149
+ * @function bootstrapApplication
150
+ * @param {string} controllerUrl - Controller base URL
151
+ * @param {string} envKey - Environment key
152
+ * @param {string} encryptionKey - Shared ENCRYPTION_KEY (sent as x-encryption-key; never log)
153
+ * @param {Object} registrationData - Same body as register
154
+ * @returns {Promise<Object>} Bootstrap response
155
+ * @throws {Error} If bootstrap fails
156
+ */
157
+ async function bootstrapApplication(controllerUrl, envKey, encryptionKey, registrationData) {
158
+ const client = new ApiClient(controllerUrl, {
159
+ type: 'bootstrap',
160
+ encryptionKey
161
+ });
162
+ return await client.post(`/api/v1/environments/${envKey}/applications/bootstrap`, {
163
+ body: registrationData
164
+ });
165
+ }
166
+
145
167
  /**
146
168
  * Rotate application secret
147
169
  * POST /api/v1/environments/{envKey}/applications/{appKey}/rotate-secret
@@ -187,6 +209,7 @@ module.exports = {
187
209
  updateApplication,
188
210
  deleteApplication,
189
211
  registerApplication,
212
+ bootstrapApplication,
190
213
  rotateApplicationSecret,
191
214
  getApplicationStatus
192
215
  };
@@ -198,7 +198,7 @@ async function listUsers(serverUrl, clientCertPem, serverCaPem) {
198
198
  * @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
199
199
  * @param {string} serverUrl - Builder Server base URL
200
200
  * @param {string} clientCertPem - PEM client certificate
201
- * @param {Object} body - CreateUserDto: developerId, name, email, optional groups (admin | secret-manager | developer | docker)
201
+ * @param {Object} body - CreateUserDto: developerId, name, email, optional groups (admin | secret-manager | secret-reader | developer | docker)
202
202
  * @param {string} [serverCaPem] - Dev root CA PEM
203
203
  * @returns {Promise<Object>} UserResponseDto
204
204
  */
package/lib/api/index.js CHANGED
@@ -17,8 +17,9 @@ class ApiClient {
17
17
  * App endpoints receive token-only auth (Bearer). x-client-id/x-client-secret are used only for the controller token endpoint.
18
18
  * @param {string} baseUrl - Base URL for the API (controller URL)
19
19
  * @param {Object} [authConfig] - Authentication configuration
20
- * @param {string} [authConfig.type] - Auth type ('bearer' | 'client-token' | 'client-credentials')
20
+ * @param {string} [authConfig.type] - Auth type ('bearer' | 'client-token' | 'client-credentials' | 'bootstrap')
21
21
  * @param {string} [authConfig.token] - Bearer token
22
+ * @param {string} [authConfig.encryptionKey] - Shared ENCRYPTION_KEY (bootstrap only; never log)
22
23
  * @param {string} [authConfig.clientId] - Client ID (client-credentials)
23
24
  * @param {string} [authConfig.clientSecret] - Client secret (client-credentials)
24
25
  */
@@ -79,7 +80,9 @@ class ApiClient {
79
80
  }
80
81
 
81
82
  // User token (bearer) → Authorization: Bearer; application token (client-token) → x-client-token
82
- if (this.authConfig.token) {
83
+ if (this.authConfig.type === 'bootstrap' && this.authConfig.encryptionKey) {
84
+ headers['x-encryption-key'] = String(this.authConfig.encryptionKey);
85
+ } else if (this.authConfig.token) {
83
86
  if (this.authConfig.type === 'client-token') {
84
87
  headers['x-client-token'] = this.authConfig.token;
85
88
  } else {
@@ -31,6 +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 {string} [developer-root-domain] - App hostname root (e.g. aifabrix.dev → dev01.aifabrix.dev)
34
35
  * @property {boolean} [docker-tls-skip-verify] - When true, Docker uses DOCKER_TLS_VERIFY=0 even if ca.pem exists (client mTLS unchanged)
35
36
  * @property {string} sync-ssh-user - SSH user for Mutagen
36
37
  * @property {string} sync-ssh-host - SSH host for Mutagen
@@ -45,7 +46,7 @@
45
46
  * @property {string} createdAt - ISO 8601
46
47
  * @property {boolean} certificateIssued - Whether cert was issued
47
48
  * @property {string} [certificateValidNotAfter] - Cert validity end (optional)
48
- * @property {string[]} groups - Access groups (admin, secret-manager, developer, docker)
49
+ * @property {string[]} groups - Access groups (admin, secret-manager, secret-reader, developer, docker)
49
50
  */
50
51
 
51
52
  /**
@@ -54,7 +55,7 @@
54
55
  * @property {string} developerId - Unique developer ID (numeric string)
55
56
  * @property {string} name - Display name
56
57
  * @property {string} email - Email
57
- * @property {string[]} [groups] - Default [developer]; tokens: admin, secret-manager, developer, docker
58
+ * @property {string[]} [groups] - Default [developer]; tokens: admin, secret-manager, secret-reader, developer, docker
58
59
  */
59
60
 
60
61
  /**
@@ -80,7 +80,8 @@ async function extractDeploymentConfig(options, _variables) {
80
80
  repositoryUrl: options.repositoryUrl,
81
81
  // Omit default here so deployer can resolve 2s vs 5s from controller deploymentType (GET /api/v1/health)
82
82
  pollInterval: Number.isFinite(pollIntervalExplicit) ? pollIntervalExplicit : undefined,
83
- pollMaxAttempts: options.pollMaxAttempts || 60
83
+ pollMaxAttempts: options.pollMaxAttempts || 60,
84
+ deploymentAuth: options.deploymentAuth
84
85
  };
85
86
  }
86
87
 
@@ -118,7 +119,7 @@ async function configureDeploymentEnvironment(_options, deploymentConfig) {
118
119
  * @returns {Promise<void>}
119
120
  * @throws {Error} If authentication fails
120
121
  */
121
- async function refreshDeploymentToken(appName, deploymentConfig) {
122
+ async function refreshDeploymentToken(appName, deploymentConfig, options = {}) {
122
123
  if (!deploymentConfig.controllerUrl) {
123
124
  throw new Error('Controller URL is required. Run "aifabrix login" to set the controller URL in config.yaml');
124
125
  }
@@ -127,7 +128,8 @@ async function refreshDeploymentToken(appName, deploymentConfig) {
127
128
  const authConfig = await getDeploymentAuth(
128
129
  deploymentConfig.controllerUrl,
129
130
  deploymentConfig.envKey,
130
- appName
131
+ appName,
132
+ { deploymentAuth: options.deploymentAuth || deploymentConfig.deploymentAuth }
131
133
  );
132
134
  if (!authConfig || !authConfig.controller) {
133
135
  throw new Error('Invalid authentication configuration: missing controller URL');
@@ -159,7 +161,7 @@ async function loadDeploymentConfig(appName, options) {
159
161
  const deploymentConfig = await extractDeploymentConfig(options, variables);
160
162
 
161
163
  await configureDeploymentEnvironment(options, deploymentConfig);
162
- await refreshDeploymentToken(appName, deploymentConfig);
164
+ await refreshDeploymentToken(appName, deploymentConfig, options);
163
165
 
164
166
  validateDeploymentConfig(deploymentConfig);
165
167
 
@@ -199,5 +199,5 @@ async function registerApplication(appKey, options = {}) {
199
199
  displayRegistrationResults(responseData, authConfig.apiUrl, environment, registrationData.displayName);
200
200
  }
201
201
 
202
- module.exports = { registerApplication, getEnvironmentPrefix };
202
+ module.exports = { registerApplication, getEnvironmentPrefix, buildRegistrationData, saveLocalCredentials, applyLocalhostPortAdjustment };
203
203
 
@@ -112,15 +112,6 @@ async function waitForHealthyAndCleanupEnvFiles(appName, port, appConfig, o) {
112
112
  logger.log(formatSuccessLine(`Container ${containerName} started`));
113
113
  await containerHelpers.logContainerStatus(containerName, debug);
114
114
 
115
- const healthCheckPath = appConfig?.healthCheck?.path || '/health';
116
- const healthUrl = (typeof healthCheck.computeHealthCheckUrl === 'function')
117
- ? await healthCheck.computeHealthCheckUrl(appName, port, appConfig, { runOptions: ro })
118
- : null;
119
- // Keep a stable fallback in the message if URL computation fails for any reason.
120
- const displayUrl = (healthUrl && typeof healthUrl === 'string')
121
- ? healthUrl
122
- : `http://localhost:${port}${healthCheckPath}`;
123
- logger.log(formatProgress(`Waiting for healthy: ${displayUrl}…`));
124
115
  await healthCheck.waitForHealthCheck(appName, 90, port, appConfig, debug, ro);
125
116
 
126
117
  for (const p of [runEnvPath, runEnvAdminPath]) {
@@ -290,8 +290,10 @@ function calculateComposePort(options, appConfig, developerId) {
290
290
  * @param {string} devDir - Dev directory path
291
291
  * @returns {Promise<string>} Path to compose file
292
292
  */
293
- async function generateComposeFile(appName, appConfig, composeOptions, devDir) {
294
- logger.log(formatProgress('Generating Docker Compose configuration…'));
293
+ async function generateComposeFile(appName, appConfig, composeOptions, devDir, logOpts = {}) {
294
+ if (!logOpts.suppressProgressLog) {
295
+ logger.log(formatProgress('Generating Docker Compose configuration…'));
296
+ }
295
297
  const composeContent = await composeGenerator.generateDockerCompose(appName, appConfig, composeOptions);
296
298
  runEnvCompose.assertNoPasswordLiteralsInCompose(composeContent);
297
299
  const tempComposePath = path.join(devDir, 'docker-compose.yaml');
@@ -352,7 +354,7 @@ async function prepareEnvironment(appName, appConfig, options) {
352
354
  );
353
355
 
354
356
  runEnvCompose.cleanApplicationsDir(developerId);
355
- logger.log(formatProgress('Building merged .env (admin + app secrets)…'));
357
+ logger.log(formatProgress('Preparing run environment (env + compose)…'));
356
358
  const { runEnvPath, runEnvAdminPath } = await runEnvCompose.buildMergedRunEnvAndWrite(
357
359
  appName,
358
360
  appConfig,
@@ -370,7 +372,9 @@ async function prepareEnvironment(appName, appConfig, options) {
370
372
  omitAppTraefikLabels: userCfg.traefik === false
371
373
  };
372
374
  composeOptions.port = calculateComposePort(composeOptions, appConfig, developerId);
373
- const composePath = await generateComposeFile(appName, appConfig, composeOptions, devDir);
375
+ const composePath = await generateComposeFile(appName, appConfig, composeOptions, devDir, {
376
+ suppressProgressLog: true
377
+ });
374
378
 
375
379
  await writeEnvOutputIfConfigured(appName, appConfig, runEnvPath, options);
376
380
 
@@ -9,16 +9,15 @@
9
9
  'use strict';
10
10
 
11
11
  const chalk = require('chalk');
12
- const { formatProgress } = require('../utils/cli-test-layout-chalk');
12
+ const { formatProgress, metadata } = require('../utils/cli-test-layout-chalk');
13
13
  const config = require('../core/config');
14
14
  const logger = require('../utils/logger');
15
15
  const mutagen = require('../utils/mutagen');
16
16
  const pathsUtil = require('../utils/paths');
17
17
  const {
18
18
  isReloadBindMountOnEngineHost,
19
- isLocalhostSyncSshHost
19
+ isHostOnThisMachine
20
20
  } = require('../utils/docker-reload-mount');
21
- const { sectionTitle, headerKeyValue, metadata } = require('../utils/cli-test-layout-chalk');
22
21
 
23
22
  /**
24
23
  * @typedef {Object} ReloadSyncSummaryBind
@@ -109,9 +108,11 @@ async function ensureReloadSync(appName, developerId, debug, codePath, remoteSyn
109
108
  return { transport: 'bind-mount', hostPath: codePath };
110
109
  }
111
110
  const syncSshHost = await config.getSyncSshHost();
112
- if (isLocalhostSyncSshHost(syncSshHost || '')) {
111
+ if (isHostOnThisMachine(syncSshHost || '')) {
113
112
  if (debug) {
114
- logger.log(chalk.gray('[DEBUG] sync-ssh-host is localhost; skipping Mutagen, using local path'));
113
+ logger.log(
114
+ chalk.gray('[DEBUG] sync-ssh-host is this machine; skipping Mutagen, using local path')
115
+ );
115
116
  }
116
117
  return { transport: 'bind-mount', hostPath: codePath };
117
118
  }
@@ -127,22 +128,19 @@ function logReloadDevSummary(reload, summary) {
127
128
  if (!reload || !summary) {
128
129
  return;
129
130
  }
130
- logger.log('');
131
- logger.log(sectionTitle('Reload (dev)'));
132
131
  if (summary.transport === 'bind-mount') {
133
- logger.log(headerKeyValue('Transport:', 'Direct bind mount on the Docker host (no Mutagen).'));
134
- logger.log(headerKeyValue('Host path → container:', `${summary.hostPath} → /app`));
135
- logger.log(metadata('Edits under the host path are visible inside the container immediately.'));
136
- logger.log('');
132
+ logger.log(
133
+ metadata(
134
+ 'Reload (dev) — Edits under the host path are visible inside the container immediately.'
135
+ )
136
+ );
137
137
  return;
138
138
  }
139
- logger.log(headerKeyValue('Transport:', 'Mutagen two-way sync (Docker engine on another host).'));
140
- logger.log(headerKeyValue('Session:', summary.sessionName));
141
- logger.log(headerKeyValue('Sync host:', summary.syncSshHost));
142
- logger.log(headerKeyValue('Local folder:', summary.localPath));
143
- logger.log(headerKeyValue('Remote mount (-v):', summary.remotePath));
144
- logger.log(metadata(`Mutagen endpoint: ${summary.sshUrl}`));
145
- logger.log('');
139
+ logger.log(
140
+ metadata(
141
+ `Reload (dev) — Mutagen sync (${summary.sessionName}); edits sync to ${summary.remotePath} before the container sees them.`
142
+ )
143
+ );
146
144
  }
147
145
 
148
146
  module.exports = { ensureReloadSync, logReloadDevSummary };
@@ -8,7 +8,10 @@
8
8
 
9
9
  'use strict';
10
10
 
11
- const { formatWarningLine } = require('../utils/cli-test-layout-chalk');
11
+ const {
12
+ formatProgress,
13
+ formatSuccessLine
14
+ } = require('../utils/cli-test-layout-chalk');
12
15
  const config = require('../core/config');
13
16
  const logger = require('../utils/logger');
14
17
  const containerHelpers = require('../utils/app-run-containers');
@@ -16,6 +19,34 @@ const helpers = require('./run-helpers');
16
19
  const { computeEffectiveEnvironmentScopedResources } = require('../utils/environment-scoped-resources');
17
20
  const { getCeleryCompanionAppKeys } = require('../utils/celery-companion-apps');
18
21
 
22
+ function shouldUseReplaceContainerSpinner() {
23
+ return Boolean(process && process.stdout && process.stdout.isTTY);
24
+ }
25
+
26
+ /**
27
+ * @param {string} progressText
28
+ * @param {string} successText
29
+ * @param {() => Promise<void>} work
30
+ * @returns {Promise<void>}
31
+ */
32
+ async function runContainerReplaceLine(progressText, successText, work) {
33
+ if (shouldUseReplaceContainerSpinner()) {
34
+ const ora = require('ora');
35
+ const spinner = ora({ text: progressText, spinner: 'dots' }).start();
36
+ try {
37
+ await work();
38
+ spinner.succeed(successText);
39
+ } catch (error) {
40
+ spinner.fail(progressText.replace(/…$/, '').trim());
41
+ throw error;
42
+ }
43
+ return;
44
+ }
45
+ logger.log(formatProgress(progressText));
46
+ await work();
47
+ logger.log(formatSuccessLine(successText));
48
+ }
49
+
19
50
  /**
20
51
  * @param {Object} appConfig
21
52
  * @param {Object} options
@@ -62,8 +93,14 @@ async function findRunningCompanionKeys(companionKeys, developerId, debug, scope
62
93
  async function stopCompanionContainers(companionKeys, developerId, debug, scopeOpts) {
63
94
  for (const companionKey of companionKeys) {
64
95
  const companionName = containerHelpers.getContainerName(companionKey, developerId, scopeOpts);
65
- logger.log(formatWarningLine(`Stopping companion container ${companionName}`));
66
- await helpers.stopAndRemoveContainer(companionKey, developerId, debug, scopeOpts);
96
+ await runContainerReplaceLine(
97
+ `Stopping companion container ${companionName}…`,
98
+ `Companion container ${companionName} stopped and removed`,
99
+ () =>
100
+ helpers.stopAndRemoveContainer(companionKey, developerId, debug, scopeOpts, {
101
+ suppressLog: true
102
+ })
103
+ );
67
104
  }
68
105
  }
69
106
 
@@ -101,8 +138,14 @@ async function checkAndStopContainer(appName, appConfig, options, debug) {
101
138
  return;
102
139
  }
103
140
  const containerName = containerHelpers.getContainerName(appName, developerId, scopeOpts);
104
- logger.log(formatWarningLine(`Container ${containerName} is already running`));
105
- await helpers.stopAndRemoveContainer(appName, developerId, debug, scopeOpts);
141
+ await runContainerReplaceLine(
142
+ `Replacing running container ${containerName}…`,
143
+ `Container ${containerName} stopped and removed`,
144
+ () =>
145
+ helpers.stopAndRemoveContainer(appName, developerId, debug, scopeOpts, {
146
+ suppressLog: true
147
+ })
148
+ );
106
149
  }
107
150
 
108
151
  module.exports = {
package/lib/app/run.js CHANGED
@@ -29,7 +29,7 @@ const { ensureReloadSync, logReloadDevSummary } = require('./run-reload-sync');
29
29
  const { logRestartDevMountSummary } = require('./restart-display');
30
30
  const { checkAndStopContainer } = require('./run-stop-containers');
31
31
  const { execWithDockerEnv } = require('../utils/docker-exec');
32
- const { isRunCliNoProxy } = require('../utils/run-cli-flags');
32
+ const { resolveRunProxyPersistEnabled, resolveRunEffectiveNoProxy } = require('../utils/run-cli-flags');
33
33
 
34
34
  /**
35
35
  * True if host is localhost or 127.0.0.1 (case-insensitive).
@@ -347,7 +347,10 @@ async function prepareAppRun(appName, options, debug) {
347
347
  }
348
348
  let userCfg = await config.getConfig();
349
349
  const appsDefaults = require('../utils/applications-config-defaults');
350
- await appsDefaults.persistApplicationRunProxyFlag(appName, !isRunCliNoProxy(options));
350
+ await appsDefaults.persistApplicationRunProxyFlag(
351
+ appName,
352
+ resolveRunProxyPersistEnabled(options, userCfg)
353
+ );
351
354
  if (envKey === 'dev') {
352
355
  await appsDefaults.persistApplicationReloadFlag(appName, options.reload === true);
353
356
  }
@@ -356,7 +359,7 @@ async function prepareAppRun(appName, options, debug) {
356
359
  const mergedOpts = {
357
360
  ...options,
358
361
  reload: options.reload === true,
359
- noProxy: isRunCliNoProxy(options) || !savedProxyHint
362
+ noProxy: resolveRunEffectiveNoProxy(options, userCfg, savedProxyHint)
360
363
  };
361
364
  logger.log('');
362
365
  logger.log(sectionTitle('Run'));
package/lib/app/show.js CHANGED
@@ -45,11 +45,11 @@ const {
45
45
  * @param {Object} userCfg
46
46
  */
47
47
  function attachRunDefaultsFromUserConfig(summary, appKey, userCfg) {
48
- const { isApplicationsReloadDefaultOn, getApplicationsRunProxyHint } = require('../utils/applications-config-defaults');
48
+ const { isApplicationsReloadDefaultOn, getEffectiveApplicationsRunProxyHint } = require('../utils/applications-config-defaults');
49
49
  if (isApplicationsReloadDefaultOn(userCfg, appKey)) {
50
50
  summary.runReloadDefault = true;
51
51
  }
52
- summary.runProxyDefault = getApplicationsRunProxyHint(userCfg, appKey);
52
+ summary.runProxyDefault = getEffectiveApplicationsRunProxyHint(userCfg, appKey);
53
53
  }
54
54
 
55
55
  /**