@aifabrix/builder 2.44.5 → 2.44.6

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 (207) hide show
  1. package/.cursor/rules/cli-layout.mdc +1 -1
  2. package/.cursor/rules/project-rules.mdc +1 -1
  3. package/.npmrc.token +1 -1
  4. package/README.md +15 -23
  5. package/integration/hubspot-test/README.md +2 -0
  6. package/integration/hubspot-test/test.js +5 -3
  7. package/jest.projects.js +48 -2
  8. package/lib/api/controller-health.api.js +49 -0
  9. package/lib/api/dimension-values.api.js +82 -0
  10. package/lib/api/dimensions.api.js +114 -0
  11. package/lib/api/external-systems.api.js +1 -0
  12. package/lib/api/integration-clients.api.js +168 -0
  13. package/lib/api/types/dimension-values.types.js +28 -0
  14. package/lib/api/types/dimensions.types.js +31 -0
  15. package/lib/api/types/integration-clients.types.js +45 -0
  16. package/lib/api/validation-runner.js +46 -25
  17. package/lib/app/deploy-config.js +11 -1
  18. package/lib/app/deploy-status-display.js +3 -3
  19. package/lib/app/deploy.js +36 -14
  20. package/lib/app/display.js +15 -11
  21. package/lib/app/push.js +46 -23
  22. package/lib/app/register.js +1 -1
  23. package/lib/app/restart-display.js +95 -0
  24. package/lib/app/rotate-secret.js +1 -1
  25. package/lib/app/run-container-start.js +12 -6
  26. package/lib/app/run-env-compose.js +30 -1
  27. package/lib/app/run-helpers.js +44 -12
  28. package/lib/app/run-reload-sync.js +148 -0
  29. package/lib/app/run-resolve-image.js +51 -1
  30. package/lib/app/run.js +99 -73
  31. package/lib/build/index.js +75 -45
  32. package/lib/cli/doctor-check.js +117 -0
  33. package/lib/cli/index.js +8 -2
  34. package/lib/cli/infra-guided.js +445 -0
  35. package/lib/cli/setup-app.js +20 -2
  36. package/lib/cli/setup-auth.js +26 -0
  37. package/lib/cli/setup-dev-path-commands.js +50 -3
  38. package/lib/cli/setup-infra.js +134 -61
  39. package/lib/cli/setup-integration-client.js +182 -0
  40. package/lib/cli/setup-parameters.js +21 -2
  41. package/lib/cli/setup-platform.js +102 -0
  42. package/lib/cli/setup-secrets.js +18 -6
  43. package/lib/cli/setup-utility.js +78 -33
  44. package/lib/commands/datasource-capability-dimension-cli.js +128 -0
  45. package/lib/commands/datasource-capability-output.js +29 -0
  46. package/lib/commands/datasource-capability-relate-cli.js +140 -0
  47. package/lib/commands/datasource-capability.js +411 -0
  48. package/lib/commands/datasource-unified-test-cli.options.js +1 -1
  49. package/lib/commands/datasource.js +53 -13
  50. package/lib/commands/dev-down.js +3 -3
  51. package/lib/commands/dev-infra-gate.js +32 -0
  52. package/lib/commands/dev-init.js +13 -7
  53. package/lib/commands/dimension-value.js +179 -0
  54. package/lib/commands/dimension.js +330 -0
  55. package/lib/commands/integration-client.js +430 -0
  56. package/lib/commands/login-device.js +65 -30
  57. package/lib/commands/login.js +21 -10
  58. package/lib/commands/parameters-validate.js +78 -13
  59. package/lib/commands/repair-datasource-auto-rbac.js +166 -0
  60. package/lib/commands/repair-datasource-keys.js +10 -5
  61. package/lib/commands/repair-datasource.js +19 -7
  62. package/lib/commands/repair-env-template.js +4 -1
  63. package/lib/commands/repair-openapi-sync.js +172 -0
  64. package/lib/commands/repair-persist.js +102 -0
  65. package/lib/commands/repair-rbac-extract.js +27 -0
  66. package/lib/commands/repair-rbac-migrate.js +186 -0
  67. package/lib/commands/repair-rbac.js +214 -31
  68. package/lib/commands/repair-system-alignment.js +246 -0
  69. package/lib/commands/repair-system-permissions.js +168 -0
  70. package/lib/commands/repair.js +120 -338
  71. package/lib/commands/secure.js +1 -1
  72. package/lib/commands/setup-modes.js +455 -0
  73. package/lib/commands/setup-prompts.js +388 -0
  74. package/lib/commands/setup.js +149 -0
  75. package/lib/commands/teardown.js +228 -0
  76. package/lib/commands/up-common.js +79 -19
  77. package/lib/commands/up-dataplane.js +33 -11
  78. package/lib/commands/up-miso.js +7 -11
  79. package/lib/commands/upload.js +109 -23
  80. package/lib/commands/wizard-core-helpers.js +14 -11
  81. package/lib/commands/wizard-core.js +6 -5
  82. package/lib/commands/wizard-dataplane.js +2 -2
  83. package/lib/commands/wizard-entity-selection.js +4 -3
  84. package/lib/commands/wizard-headless.js +2 -1
  85. package/lib/commands/wizard.js +2 -1
  86. package/lib/constants/infra-compose-service-names.js +40 -0
  87. package/lib/core/env-reader.js +16 -3
  88. package/lib/core/secrets-admin-env.js +101 -0
  89. package/lib/core/secrets-ensure-infra.js +34 -1
  90. package/lib/core/secrets-ensure.js +88 -66
  91. package/lib/core/secrets-env-content.js +432 -0
  92. package/lib/core/secrets-env-write.js +27 -1
  93. package/lib/core/secrets-load.js +248 -0
  94. package/lib/core/secrets-names.js +32 -0
  95. package/lib/core/secrets.js +17 -757
  96. package/lib/datasource/capability/basic-exposure.js +76 -0
  97. package/lib/datasource/capability/capability-diff-slice.js +41 -0
  98. package/lib/datasource/capability/capability-key.js +34 -0
  99. package/lib/datasource/capability/capability-resolve.js +172 -0
  100. package/lib/datasource/capability/capability-storage-keys.js +22 -0
  101. package/lib/datasource/capability/copy-operations.js +348 -0
  102. package/lib/datasource/capability/copy-test-payload.js +139 -0
  103. package/lib/datasource/capability/create-operations.js +235 -0
  104. package/lib/datasource/capability/dimension-operations.js +151 -0
  105. package/lib/datasource/capability/dimension-validate.js +219 -0
  106. package/lib/datasource/capability/json-pointer.js +31 -0
  107. package/lib/datasource/capability/reference-rewrite.js +51 -0
  108. package/lib/datasource/capability/relate-operations.js +325 -0
  109. package/lib/datasource/capability/relate-validate.js +219 -0
  110. package/lib/datasource/capability/remove-operations.js +275 -0
  111. package/lib/datasource/capability/run-capability-copy.js +152 -0
  112. package/lib/datasource/capability/run-capability-diff.js +135 -0
  113. package/lib/datasource/capability/run-capability-dimension.js +291 -0
  114. package/lib/datasource/capability/run-capability-edit.js +377 -0
  115. package/lib/datasource/capability/run-capability-relate.js +193 -0
  116. package/lib/datasource/capability/run-capability-remove.js +105 -0
  117. package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
  118. package/lib/datasource/capability/validate-capability-slice.js +35 -0
  119. package/lib/datasource/list.js +136 -23
  120. package/lib/datasource/log-viewer.js +2 -4
  121. package/lib/datasource/unified-validation-run.js +51 -16
  122. package/lib/datasource/validate.js +53 -1
  123. package/lib/deployment/deploy-poll-ui.js +60 -0
  124. package/lib/deployment/deployer-status.js +29 -3
  125. package/lib/deployment/deployer.js +48 -30
  126. package/lib/deployment/environment.js +7 -2
  127. package/lib/deployment/poll-interval.js +72 -0
  128. package/lib/deployment/push.js +11 -9
  129. package/lib/external-system/deploy.js +4 -1
  130. package/lib/external-system/download.js +61 -32
  131. package/lib/external-system/sync-deploy-manifest.js +33 -0
  132. package/lib/infrastructure/index.js +49 -19
  133. package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
  134. package/lib/parameters/infra-kv-discovery.js +29 -4
  135. package/lib/parameters/infra-parameter-catalog.js +6 -3
  136. package/lib/parameters/infra-parameter-validate.js +67 -19
  137. package/lib/resolvers/datasource-resolver.js +53 -0
  138. package/lib/resolvers/dimension-file.js +52 -0
  139. package/lib/resolvers/manifest-resolver.js +133 -0
  140. package/lib/schema/external-datasource.schema.json +183 -53
  141. package/lib/schema/external-system.schema.json +23 -10
  142. package/lib/schema/infra.parameter.yaml +26 -11
  143. package/lib/schema/wizard-config.schema.json +1 -1
  144. package/lib/utils/aifabrix-config-dir-walk.js +40 -0
  145. package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
  146. package/lib/utils/app-run-containers.js +2 -2
  147. package/lib/utils/bash-secret-env.js +59 -0
  148. package/lib/utils/cli-secrets-error-format.js +78 -0
  149. package/lib/utils/cli-test-layout-chalk.js +31 -9
  150. package/lib/utils/cli-utils.js +4 -36
  151. package/lib/utils/datasource-test-run-display.js +8 -0
  152. package/lib/utils/dev-hosts-helper.js +3 -2
  153. package/lib/utils/dev-init-ssh-merge.js +2 -1
  154. package/lib/utils/docker-build.js +17 -9
  155. package/lib/utils/docker-reload-mount.js +127 -0
  156. package/lib/utils/external-readme.js +71 -2
  157. package/lib/utils/external-system-local-test-tty.js +3 -2
  158. package/lib/utils/external-system-readiness-core.js +45 -12
  159. package/lib/utils/external-system-readiness-deploy-display.js +3 -3
  160. package/lib/utils/external-system-readiness-display-internals.js +33 -3
  161. package/lib/utils/external-system-readiness-display.js +10 -1
  162. package/lib/utils/file-upload.js +40 -3
  163. package/lib/utils/health-check-db-init.js +107 -0
  164. package/lib/utils/health-check-public-warn.js +69 -0
  165. package/lib/utils/health-check-url.js +19 -4
  166. package/lib/utils/health-check.js +135 -105
  167. package/lib/utils/help-builder.js +5 -1
  168. package/lib/utils/image-name.js +34 -7
  169. package/lib/utils/integration-file-backup.js +74 -0
  170. package/lib/utils/mutagen-install.js +30 -3
  171. package/lib/utils/paths.js +108 -25
  172. package/lib/utils/postgres-wipe.js +212 -0
  173. package/lib/utils/register-aifabrix-shell-env.js +15 -0
  174. package/lib/utils/remote-dev-auth.js +21 -5
  175. package/lib/utils/remote-docker-env.js +9 -1
  176. package/lib/utils/remote-secrets-loader.js +42 -3
  177. package/lib/utils/resolve-docker-image-ref.js +9 -3
  178. package/lib/utils/secrets-ancestor-paths.js +47 -0
  179. package/lib/utils/secrets-helpers.js +17 -10
  180. package/lib/utils/secrets-kv-refs.js +42 -0
  181. package/lib/utils/secrets-kv-scope.js +19 -2
  182. package/lib/utils/secrets-materialize-local.js +134 -0
  183. package/lib/utils/secrets-path.js +24 -10
  184. package/lib/utils/secrets-utils.js +2 -2
  185. package/lib/utils/system-builder-root.js +34 -0
  186. package/lib/utils/url-declarative-resolve-build.js +6 -1
  187. package/lib/utils/url-declarative-runtime-base-path.js +32 -0
  188. package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
  189. package/lib/utils/urls-local-registry.js +23 -12
  190. package/lib/utils/validation-poll-ui.js +81 -0
  191. package/lib/utils/validation-run-poll.js +29 -5
  192. package/lib/utils/with-muted-logger.js +53 -0
  193. package/package.json +1 -1
  194. package/templates/applications/dataplane/application.yaml +1 -1
  195. package/templates/applications/dataplane/rbac.yaml +10 -10
  196. package/templates/applications/keycloak/env.template +8 -6
  197. package/templates/applications/miso-controller/application.yaml +7 -0
  198. package/templates/applications/miso-controller/env.template +1 -1
  199. package/templates/applications/miso-controller/rbac.yaml +9 -9
  200. package/templates/external-system/README.md.hbs +83 -123
  201. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  202. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  203. package/.nyc_output/processinfo/index.json +0 -1
  204. package/lib/api/service-users.api.js +0 -150
  205. package/lib/api/types/service-users.types.js +0 -65
  206. package/lib/cli/setup-service-user.js +0 -187
  207. package/lib/commands/service-user.js +0 -429
@@ -14,9 +14,36 @@ const fs = require('fs').promises;
14
14
  const fsSync = require('fs');
15
15
  const pathsUtil = require('../utils/paths');
16
16
  const adminSecrets = require('../core/admin-secrets');
17
+ const secretsEnsure = require('../core/secrets-ensure');
17
18
  const secretsEnvWrite = require('../core/secrets-env-write');
18
19
  const { getContainerPort } = require('../utils/port-resolver');
19
20
 
21
+ /**
22
+ * Backfill only secrets needed for `aifabrix run <app>`: kv:// keys from that app's env.template
23
+ * (catalog defaults via placeholderContext). Does not run full `ensureInfraSecrets`.
24
+ *
25
+ * Docker Postgres admin password for compose/db-init comes from **admin-secrets.env** (see
26
+ * `ensureAdminSecrets` / `readAndDecryptAdminSecrets`), not from `postgres-passwordKeyVault` on every run.
27
+ * The kv key is used when **generating** admin-secrets (e.g. first `up-infra`) via `generateAdminSecretsEnv`;
28
+ * we do not duplicate it here when an admin file already exists.
29
+ *
30
+ * @async
31
+ * @param {string} appName - Builder application key
32
+ * @returns {Promise<void>}
33
+ */
34
+ async function ensureRunSecretsForApp(appName) {
35
+ const placeholderContext = secretsEnsure.buildInfraPlaceholderContext({});
36
+ const userSecretsPath = pathsUtil.getPrimaryUserSecretsLocalPath();
37
+ const templatePath = path.join(pathsUtil.getBuilderPath(appName), 'env.template');
38
+ if (fsSync.existsSync(templatePath)) {
39
+ await secretsEnsure.ensureSecretsFromEnvTemplate(templatePath, {
40
+ placeholderContext,
41
+ preferredFilePath: userSecretsPath,
42
+ useMergedSecretsForMissingKeys: true
43
+ });
44
+ }
45
+ }
46
+
20
47
  /**
21
48
  * Clean applications directory: remove generated docker-compose.yaml and .env.* files.
22
49
  * @param {string|number} developerId - Developer ID
@@ -204,6 +231,7 @@ async function buildMergedRunEnvAndWrite(appName, appConfig, devDir, developerId
204
231
  const ensureAdminSecretsFn = typeof infra.ensureAdminSecrets === 'function'
205
232
  ? infra.ensureAdminSecrets
206
233
  : require('../infrastructure/helpers').ensureAdminSecrets;
234
+ await ensureRunSecretsForApp(appName);
207
235
  await ensureAdminSecretsFn();
208
236
  const adminObj = await adminSecrets.readAndDecryptAdminSecrets();
209
237
  const runEnvKey = scopeOpts && scopeOpts.runEnvKey ? String(scopeOpts.runEnvKey).toLowerCase() : 'dev';
@@ -248,5 +276,6 @@ function assertNoPasswordLiteralsInCompose(composeContent) {
248
276
  module.exports = {
249
277
  cleanApplicationsDir,
250
278
  buildMergedRunEnvAndWrite,
251
- assertNoPasswordLiteralsInCompose
279
+ assertNoPasswordLiteralsInCompose,
280
+ ensureRunSecretsForApp
252
281
  };
@@ -1,4 +1,12 @@
1
- const { formatSuccessLine, formatSuccessParagraph } = require('../utils/cli-test-layout-chalk');
1
+ const {
2
+ formatSuccessLine,
3
+ formatSuccessParagraph,
4
+ formatProgress,
5
+ formatNextActions,
6
+ headerKeyValue,
7
+ metadata,
8
+ infoLine
9
+ } = require('../utils/cli-test-layout-chalk');
2
10
  /**
3
11
  * AI Fabrix Builder - App Run Helpers
4
12
  *
@@ -28,6 +36,8 @@ const { resolveRunImage } = require('./run-resolve-image');
28
36
  const { startContainer } = require('./run-container-start');
29
37
  const { resolveEnvOutputPath, writeEnvOutputForReload, writeEnvOutputForLocal } = require('../utils/env-copy');
30
38
  const { resolveVersionForApp } = require('../utils/image-version');
39
+ const healthCheckUtil = require('../utils/health-check');
40
+ const { computeTraefikPublicAppUrl } = require('../utils/health-check-url');
31
41
 
32
42
  /** Template apps (keycloak, miso-controller, dataplane) - never update application config when running */
33
43
  const TEMPLATE_APP_KEYS = ['keycloak', 'miso-controller', 'dataplane'];
@@ -179,7 +189,7 @@ async function checkPrerequisites(appName, appConfig, debug = false, skipInfraCh
179
189
  logger.log(chalk.gray(`[DEBUG] Image name: ${imageName}, tag: ${imageTag}`));
180
190
  }
181
191
 
182
- logger.log(chalk.blue(`Checking if image ${fullImageName} exists...`));
192
+ logger.log(formatProgress(`Checking image ${fullImageName}…`));
183
193
  const imageExists = await checkImageExists(imageName, imageTag, debug);
184
194
  if (!imageExists) {
185
195
  const isTemplateApp = TEMPLATE_APP_KEYS.includes(appName);
@@ -208,12 +218,12 @@ async function checkInfraHealthOrThrow(debug, appConfig) {
208
218
  const requirements = getAppInfraRequirements(appConfig);
209
219
  if (requirements && !requirements.needsPostgres && !requirements.needsRedis) {
210
220
  logger.log(
211
- chalk.blue('Skipping infrastructure check (application does not require database or redis)...')
221
+ infoLine('Skipping Postgres/Redis health check (not required by this application).')
212
222
  );
213
223
  return;
214
224
  }
215
225
 
216
- logger.log(chalk.blue('Checking infrastructure health...'));
226
+ logger.log(formatProgress('Checking infrastructure health'));
217
227
  const infra = require('../infrastructure');
218
228
  const healthOptions =
219
229
  requirements === null
@@ -280,7 +290,7 @@ function calculateComposePort(options, appConfig, developerId) {
280
290
  * @returns {Promise<string>} Path to compose file
281
291
  */
282
292
  async function generateComposeFile(appName, appConfig, composeOptions, devDir) {
283
- logger.log(chalk.blue('Generating Docker Compose configuration...'));
293
+ logger.log(formatProgress('Generating Docker Compose configuration'));
284
294
  const composeContent = await composeGenerator.generateDockerCompose(appName, appConfig, composeOptions);
285
295
  runEnvCompose.assertNoPasswordLiteralsInCompose(composeContent);
286
296
  const tempComposePath = path.join(devDir, 'docker-compose.yaml');
@@ -338,7 +348,7 @@ async function prepareEnvironment(appName, appConfig, options) {
338
348
  );
339
349
 
340
350
  runEnvCompose.cleanApplicationsDir(developerId);
341
- logger.log(chalk.blue('Building merged .env (admin + app secrets)...'));
351
+ logger.log(formatProgress('Building merged .env (admin + app secrets)'));
342
352
  const { runEnvPath, runEnvAdminPath } = await runEnvCompose.buildMergedRunEnvAndWrite(
343
353
  appName,
344
354
  appConfig,
@@ -367,15 +377,37 @@ async function prepareEnvironment(appName, appConfig, options) {
367
377
  * @param {string} appName - Application name
368
378
  * @param {number} port - Application port
369
379
  * @param {Object} appConfig - Application configuration (with developerId property)
380
+ * @param {Object|null} [runScopeOpts] - Optional env-scoped container naming
381
+ * @param {Object} [runOptions] - Run options (traefikEnabled / probeViaTraefik for public URL)
370
382
  */
371
- async function displayRunStatus(appName, port, appConfig, runScopeOpts = null) {
383
+ async function displayRunStatus(appName, port, appConfig, runScopeOpts = null, runOptions = {}) {
372
384
  const containerName = containerHelpers.getContainerName(appName, appConfig.developerId, runScopeOpts);
373
- const healthCheckPath = appConfig?.healthCheck?.path || '/health';
374
- const healthCheckUrl = `http://localhost:${port}${healthCheckPath}`;
385
+ const ro = runOptions && typeof runOptions === 'object' ? runOptions : {};
386
+ const wantsPublic = Boolean(ro.probeViaTraefik === true || ro.traefikEnabled === true);
375
387
 
376
- logger.log(formatSuccessParagraph(`App running at http://localhost:${port}`));
377
- logger.log(chalk.blue(`Health check: ${healthCheckUrl}`));
378
- logger.log(chalk.gray(`Container: ${containerName}`));
388
+ let primaryAppUrl = `http://localhost:${port}`;
389
+ if (wantsPublic) {
390
+ const pub = await computeTraefikPublicAppUrl(appName, port, appConfig);
391
+ if (pub) primaryAppUrl = pub;
392
+ }
393
+
394
+ const healthTipUrl = await healthCheckUtil.computeHealthCheckUrl(appName, port, appConfig, {
395
+ runOptions: ro
396
+ });
397
+
398
+ logger.log(formatSuccessParagraph(`App running at ${primaryAppUrl}`));
399
+ if (wantsPublic && primaryAppUrl.indexOf('localhost') === -1) {
400
+ logger.log(metadata(`Local direct: http://localhost:${port}`));
401
+ }
402
+ logger.log(headerKeyValue('Health check:', healthTipUrl));
403
+ logger.log(headerKeyValue('Container:', containerName));
404
+ logger.log('');
405
+ logger.log(
406
+ formatNextActions([
407
+ `Validate errors: aifabrix logs ${appName} -l error`,
408
+ `Follow output: aifabrix logs ${appName} -f -t 100`
409
+ ])
410
+ );
379
411
  }
380
412
 
381
413
  module.exports = {
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Mutagen sync for `aifabrix run --reload` when Docker runs on another host.
3
+ *
4
+ * @fileoverview ensureReloadSync extracted from run.js (size limits)
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const chalk = require('chalk');
12
+ const { formatProgress } = require('../utils/cli-test-layout-chalk');
13
+ const config = require('../core/config');
14
+ const logger = require('../utils/logger');
15
+ const mutagen = require('../utils/mutagen');
16
+ const pathsUtil = require('../utils/paths');
17
+ const {
18
+ isReloadBindMountOnEngineHost,
19
+ isLocalhostSyncSshHost
20
+ } = require('../utils/docker-reload-mount');
21
+ const { sectionTitle, headerKeyValue, metadata } = require('../utils/cli-test-layout-chalk');
22
+
23
+ /**
24
+ * @typedef {Object} ReloadSyncSummaryBind
25
+ * @property {'bind-mount'} transport
26
+ * @property {string} hostPath - Host directory bind-mounted to /app in the container
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} ReloadSyncSummaryMutagen
31
+ * @property {'mutagen'} transport
32
+ * @property {string} remotePath - Path on sync host used for Docker -v
33
+ * @property {string} sessionName - Mutagen session name
34
+ * @property {string} localPath - Local folder synced (same as build context)
35
+ * @property {string} syncSshHost - sync-ssh-host from config
36
+ * @property {string} sshUrl - Mutagen endpoint (user@host:path)
37
+ */
38
+
39
+ /**
40
+ * @typedef {ReloadSyncSummaryBind|ReloadSyncSummaryMutagen} ReloadSyncSummary
41
+ */
42
+
43
+ /**
44
+ * Install Mutagen if needed, create sync session, return summary for compose and UX.
45
+ * @param {string} appName
46
+ * @param {string} developerId
47
+ * @param {boolean} debug
48
+ * @param {string} codePath
49
+ * @param {string} [remoteSyncPath]
50
+ * @param {string} syncSshHost
51
+ * @returns {Promise<ReloadSyncSummaryMutagen>}
52
+ */
53
+ async function startMutagenReloadSync(
54
+ appName,
55
+ developerId,
56
+ debug,
57
+ codePath,
58
+ remoteSyncPath,
59
+ syncSshHost
60
+ ) {
61
+ const [userMutagenFolder, syncSshUser] = await Promise.all([
62
+ config.getUserMutagenFolder(),
63
+ config.getSyncSshUser()
64
+ ]);
65
+ if (!userMutagenFolder || !syncSshUser || !syncSshHost) {
66
+ throw new Error(
67
+ 'run --reload requires remote server sync settings. Run "aifabrix dev init" or set user-mutagen-folder, sync-ssh-user, sync-ssh-host in config.'
68
+ );
69
+ }
70
+ const mutagenPath = await mutagen.ensureMutagenPath(logger.log);
71
+ const remotePath = mutagen.getRemotePath(userMutagenFolder, appName, remoteSyncPath);
72
+ const sshUrl = mutagen.getSyncSshUrl(syncSshUser, syncSshHost, remotePath);
73
+ const sessionName = mutagen.getSessionName(developerId, appName);
74
+ const localPath = (codePath && typeof codePath === 'string') ? codePath : pathsUtil.getBuilderPath(appName);
75
+ if (debug) logger.log(chalk.gray(`[DEBUG] Mutagen sync: ${sessionName} ${localPath} <-> ${sshUrl}`));
76
+ logger.log(formatProgress('Reload: ensuring Mutagen sync (remote Docker engine)…'));
77
+ await mutagen.ensureSyncSession(mutagenPath, sessionName, localPath, sshUrl);
78
+ return {
79
+ transport: 'mutagen',
80
+ remotePath,
81
+ sessionName,
82
+ localPath,
83
+ syncSshHost,
84
+ sshUrl
85
+ };
86
+ }
87
+
88
+ /**
89
+ * When run --reload in dev with remote: ensure Mutagen sync session; return summary for mounts and CLI copy.
90
+ * Uses codePath (resolved build.context) as Mutagen local path so one config field drives both local and remote.
91
+ * Skips Mutagen when docker-endpoint targets this host (bind mount) or sync-ssh-host is localhost.
92
+ *
93
+ * @param {string} appName - Application name
94
+ * @param {string} developerId - Developer ID
95
+ * @param {boolean} debug - Debug flag
96
+ * @param {string} codePath - Resolved build.context (absolute path to app code)
97
+ * @param {string} [remoteSyncPath] - Optional relative path under user-mutagen-folder (from build.remoteSyncPath); when unset, defaults to dev/<appKey>
98
+ * @returns {Promise<ReloadSyncSummary>}
99
+ * @throws {Error} If --reload but remote not configured, or Mutagen install fails
100
+ */
101
+ async function ensureReloadSync(appName, developerId, debug, codePath, remoteSyncPath) {
102
+ const endpoint = await config.getDockerEndpoint();
103
+ if (isReloadBindMountOnEngineHost(endpoint)) {
104
+ if (debug) {
105
+ logger.log(
106
+ chalk.gray('[DEBUG] Docker engine shares this host filesystem; skipping Mutagen for --reload')
107
+ );
108
+ }
109
+ return { transport: 'bind-mount', hostPath: codePath };
110
+ }
111
+ const syncSshHost = await config.getSyncSshHost();
112
+ if (isLocalhostSyncSshHost(syncSshHost || '')) {
113
+ if (debug) {
114
+ logger.log(chalk.gray('[DEBUG] sync-ssh-host is localhost; skipping Mutagen, using local path'));
115
+ }
116
+ return { transport: 'bind-mount', hostPath: codePath };
117
+ }
118
+ return startMutagenReloadSync(appName, developerId, debug, codePath, remoteSyncPath, syncSshHost);
119
+ }
120
+
121
+ /**
122
+ * Log how --reload is wired (bind vs Mutagen) after compose/env prep, before container start.
123
+ * @param {boolean} reload
124
+ * @param {ReloadSyncSummary|undefined} summary
125
+ */
126
+ function logReloadDevSummary(reload, summary) {
127
+ if (!reload || !summary) {
128
+ return;
129
+ }
130
+ logger.log('');
131
+ logger.log(sectionTitle('Reload (dev)'));
132
+ 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('');
137
+ return;
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('');
146
+ }
147
+
148
+ module.exports = { ensureReloadSync, logReloadDevSummary };
@@ -6,7 +6,10 @@
6
6
 
7
7
  'use strict';
8
8
 
9
+ const config = require('../core/config');
9
10
  const { resolveDockerImageRef } = require('../utils/resolve-docker-image-ref');
11
+ const { checkImageExists } = require('../utils/app-run-containers');
12
+ const { buildDevImageRepositoryPath } = require('../utils/image-name');
10
13
 
11
14
  /**
12
15
  * @param {string} appName
@@ -18,4 +21,51 @@ function resolveRunImage(appName, appConfig, runOptions) {
18
21
  return resolveDockerImageRef(appName, appConfig, runOptions);
19
22
  }
20
23
 
21
- module.exports = { resolveRunImage };
24
+ /**
25
+ * Resolves which local image to run: developer-scoped ref first when present, else manifest base.
26
+ * With {@link runOptions.base} true, uses manifest base only (no dev-first probe).
27
+ *
28
+ * @async
29
+ * @param {string} appName
30
+ * @param {Object} appConfig
31
+ * @param {Object} [runOptions]
32
+ * @param {string} [runOptions.image] - Full override (skips fallback logic)
33
+ * @param {boolean} [runOptions.base] - When true, manifest base ref only
34
+ * @returns {Promise<{ imageName: string, imageTag: string }>}
35
+ */
36
+ async function resolveRunImageWithLocalFallback(appName, appConfig, runOptions = {}) {
37
+ const opts = runOptions || {};
38
+ if (opts.image) {
39
+ return resolveDockerImageRef(appName, appConfig, opts);
40
+ }
41
+
42
+ const baseRef = resolveDockerImageRef(appName, appConfig, opts);
43
+
44
+ if (opts.base === true) {
45
+ return baseRef;
46
+ }
47
+
48
+ const developerId = await config.getDeveloperId();
49
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
50
+ if (!Number.isFinite(idNum) || idNum === 0) {
51
+ return baseRef;
52
+ }
53
+
54
+ const tag = baseRef.imageTag;
55
+ const devRepo = buildDevImageRepositoryPath(baseRef.imageName, developerId);
56
+ const devExists = await checkImageExists(devRepo, tag, false);
57
+ if (devExists) {
58
+ return { imageName: devRepo, imageTag: tag };
59
+ }
60
+ const baseExists = await checkImageExists(baseRef.imageName, tag, false);
61
+ if (baseExists) {
62
+ return baseRef;
63
+ }
64
+
65
+ throw new Error(
66
+ `Docker image not found (tried ${devRepo}:${tag} then ${baseRef.imageName}:${tag})\n` +
67
+ `Run 'aifabrix build ${appName}' or pull the manifest image, or use --base after pulling the base image.`
68
+ );
69
+ }
70
+
71
+ module.exports = { resolveRunImage, resolveRunImageWithLocalFallback };
package/lib/app/run.js CHANGED
@@ -10,15 +10,23 @@
10
10
  */
11
11
 
12
12
  const chalk = require('chalk');
13
+ const {
14
+ formatWarningLine,
15
+ sectionTitle,
16
+ headerKeyValue,
17
+ metadata,
18
+ formatNextActions
19
+ } = require('../utils/cli-test-layout-chalk');
13
20
  const config = require('../core/config');
14
21
  const logger = require('../utils/logger');
15
22
  const pathsUtil = require('../utils/paths');
16
23
  const { checkPortAvailable, waitForHealthCheck } = require('../utils/health-check');
17
24
  const composeGenerator = require('../utils/compose-generator');
18
25
  const containerHelpers = require('../utils/app-run-containers');
19
- const mutagen = require('../utils/mutagen');
20
26
  // Helper functions extracted to reduce file size and complexity
21
27
  const helpers = require('./run-helpers');
28
+ const { ensureReloadSync, logReloadDevSummary } = require('./run-reload-sync');
29
+ const { logRestartDevMountSummary } = require('./restart-display');
22
30
  const { execWithDockerEnv } = require('../utils/docker-exec');
23
31
 
24
32
  /**
@@ -73,8 +81,16 @@ async function validateAppForRun(appName, _debug) {
73
81
  try {
74
82
  const { isExternal, baseDir } = await detectAppType(appName);
75
83
  if (baseDir !== 'builder' || isExternal) {
76
- logger.log(chalk.yellow('⚠ External systems don\'t run as Docker containers.'));
77
- logger.log(chalk.blue('Use "aifabrix build" to deploy to dataplane, then test via OpenAPI endpoints.'));
84
+ logger.log('');
85
+ logger.log(sectionTitle('Run'));
86
+ logger.log(headerKeyValue('Application:', appName));
87
+ logger.log(
88
+ formatWarningLine('External integrations are not started as local Docker containers.')
89
+ );
90
+ logger.log(
91
+ metadata('Build and deploy the integration, then use its APIs from your environment.')
92
+ );
93
+ logger.log(formatNextActions([`aifabrix build ${appName}`]));
78
94
  return false;
79
95
  }
80
96
  } catch (error) {
@@ -114,7 +130,7 @@ async function checkAndStopContainer(appName, appConfig, options, debug) {
114
130
  }
115
131
 
116
132
  const containerName = containerHelpers.getContainerName(appName, developerId, scopeOpts);
117
- logger.log(chalk.yellow(`Container ${containerName} is already running`));
133
+ logger.log(formatWarningLine(`Container ${containerName} is already running`));
118
134
  await helpers.stopAndRemoveContainer(appName, developerId, debug, scopeOpts);
119
135
  }
120
136
 
@@ -149,48 +165,6 @@ async function calculateHostPort(appConfig, options, debug) {
149
165
  return hostPort;
150
166
  }
151
167
 
152
- /**
153
- * When run --reload in dev with remote: ensure Mutagen sync session; return remote path for compose mount.
154
- * Uses codePath (resolved build.context) as Mutagen local path so one config field drives both local and remote.
155
- * When Docker endpoint, remote-server, or sync-ssh-host is localhost/127.0.0.1, returns null (no sync; use local path).
156
- *
157
- * @param {string} appName - Application name
158
- * @param {string} developerId - Developer ID
159
- * @param {boolean} debug - Debug flag
160
- * @param {string} codePath - Resolved build.context (absolute path to app code)
161
- * @param {string} [remoteSyncPath] - Optional relative path under user-mutagen-folder (from build.remoteSyncPath); when unset, defaults to dev/<appKey>
162
- * @returns {Promise<string|null>} Remote path for -v mount or null if not remote/reload or already on server (localhost)
163
- * @throws {Error} If --reload but remote not configured, or Mutagen install fails
164
- */
165
- async function ensureReloadSync(appName, developerId, debug, codePath, remoteSyncPath) {
166
- const endpoint = await config.getDockerEndpoint();
167
- const serverUrl = await config.getRemoteServer();
168
- if (!endpoint && !serverUrl) return null;
169
- const syncSshHost = await config.getSyncSshHost();
170
- if (isLocalhostEndpoint(endpoint) || isLocalhostEndpoint(serverUrl) || isLocalhostHost(syncSshHost || '')) {
171
- if (debug) logger.log(chalk.gray('[DEBUG] Docker/remote/sync host is localhost; skipping Mutagen, using local path'));
172
- return null;
173
- }
174
- const [userMutagenFolder, syncSshUser] = await Promise.all([
175
- config.getUserMutagenFolder(),
176
- config.getSyncSshUser()
177
- ]);
178
- if (!userMutagenFolder || !syncSshUser || !syncSshHost) {
179
- throw new Error(
180
- 'run --reload requires remote server sync settings. Run "aifabrix dev init" or set user-mutagen-folder, sync-ssh-user, sync-ssh-host in config.'
181
- );
182
- }
183
- const mutagenPath = await mutagen.ensureMutagenPath(logger.log);
184
- const remotePath = mutagen.getRemotePath(userMutagenFolder, appName, remoteSyncPath);
185
- const sshUrl = mutagen.getSyncSshUrl(syncSshUser, syncSshHost, remotePath);
186
- const sessionName = mutagen.getSessionName(developerId, appName);
187
- const localPath = (codePath && typeof codePath === 'string') ? codePath : pathsUtil.getBuilderPath(appName);
188
- if (debug) logger.log(chalk.gray(`[DEBUG] Mutagen sync: ${sessionName} ${localPath} <-> ${sshUrl}`));
189
- logger.log(chalk.blue('Ensuring Mutagen sync session...'));
190
- await mutagen.ensureSyncSession(mutagenPath, sessionName, localPath, sshUrl);
191
- return remotePath;
192
- }
193
-
194
168
  /**
195
169
  * Load and configure application
196
170
  * @async
@@ -238,12 +212,21 @@ async function startAppContainer(appName, tempComposePath, hostPort, appConfig,
238
212
  devMountPath,
239
213
  misoEnvironment
240
214
  });
241
- await helpers.displayRunStatus(appName, hostPort, appConfig, opts.runScopeOpts || null);
215
+ await helpers.displayRunStatus(
216
+ appName,
217
+ hostPort,
218
+ appConfig,
219
+ opts.runScopeOpts || null,
220
+ runOptions || {}
221
+ );
242
222
  } catch (error) {
243
- logger.log(chalk.yellow(`\n⚠️ Compose file preserved at: ${tempComposePath}`));
244
- logger.log(chalk.yellow(' Review the file to debug issues'));
223
+ logger.log('');
224
+ logger.log(formatWarningLine(`Compose file preserved at ${tempComposePath}`));
225
+ logger.log(metadata(' Review the compose file, fix the issue, then run again.'));
245
226
  if (runEnvPath || runEnvAdminPath) {
246
- logger.log(chalk.yellow(' Run .env file(s) (contain secrets) were not deleted; remove them manually if desired.'));
227
+ logger.log(
228
+ metadata(' Run .env file(s) contain secrets and were not deleted; remove manually if needed.')
229
+ );
247
230
  }
248
231
  if (debug) {
249
232
  logger.log(chalk.gray(`[DEBUG] Error during container start: ${error.message}`));
@@ -271,30 +254,46 @@ async function resolveRunOptions(appName, appConfig, options, envKey, debug, eff
271
254
  const codePath = pathsUtil.resolveBuildContext(builderPath, appConfig.build?.context || '.');
272
255
  if (options.reload && envKey === 'dev') {
273
256
  const remoteSyncPath = appConfig.build?.remoteSyncPath;
274
- const remotePath = await ensureReloadSync(appName, appConfig.developerId, debug, codePath, remoteSyncPath);
275
- runOptions.devMountPath = remotePath || codePath;
257
+ const reloadSummary = await ensureReloadSync(appName, appConfig.developerId, debug, codePath, remoteSyncPath);
258
+ runOptions.reloadSyncSummary = reloadSummary;
259
+ runOptions.devMountPath =
260
+ reloadSummary.transport === 'mutagen' ? reloadSummary.remotePath : codePath;
276
261
  }
277
262
  return runOptions;
278
263
  }
279
264
 
280
265
  /**
281
- * Prepare run: validate app, load config, check prereqs, stop existing container, resolve port and reload, prepare env.
282
- * @param {string} appName - Application name
283
- * @param {Object} options - Run options
284
- * @param {boolean} debug - Debug flag
285
- * @returns {Promise<{ appConfig: Object, tempComposePath: string, hostPort: number }|null>} Prepared run context or null if should not continue
266
+ * When Traefik is enabled in user config, pass through for health checks (DNS + localhost order).
267
+ * @param {Object} runOptions
268
+ * @param {Object} userCfg
286
269
  */
287
- async function prepareAppRun(appName, options, debug) {
288
- const envKey = (options.env || 'dev').toLowerCase();
270
+ function applyTraefikFlagToRunOptions(runOptions, userCfg) {
271
+ if (userCfg && userCfg.traefik === true) {
272
+ runOptions.traefikEnabled = true;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * @param {string} envKey
278
+ * @throws {Error} If env is not dev, tst, or pro
279
+ */
280
+ function assertValidRunEnvKey(envKey) {
289
281
  if (envKey !== 'dev' && envKey !== 'tst' && envKey !== 'pro') {
290
282
  throw new Error('--env must be dev, tst, or pro');
291
283
  }
292
- const shouldContinue = await validateAppForRun(appName, debug);
293
- if (!shouldContinue) {
294
- return null;
295
- }
296
- const appConfig = await loadAndConfigureApp(appName, debug);
297
- const userCfg = await config.getConfig();
284
+ }
285
+
286
+ /**
287
+ * Image resolution, prereqs, port, compose env — after app config and user config are loaded.
288
+ * @param {string} appName
289
+ * @param {Object} appConfig
290
+ * @param {Object} options - Original run options
291
+ * @param {string} envKey
292
+ * @param {Object} userCfg - getConfig() result
293
+ * @param {boolean} debug
294
+ * @returns {Promise<Object>} Prepared run context (same shape as prepareAppRun return)
295
+ */
296
+ async function computeRunPreparationCore(appName, appConfig, options, envKey, userCfg, debug) {
298
297
  const { computeEffectiveEnvironmentScopedResources } = require('../utils/environment-scoped-resources');
299
298
  const effectiveEnvironmentScopedResources = computeEffectiveEnvironmentScopedResources(
300
299
  Boolean(userCfg.useEnvironmentScopedResources),
@@ -304,17 +303,24 @@ async function prepareAppRun(appName, options, debug) {
304
303
  const runScopeOpts = effectiveEnvironmentScopedResources
305
304
  ? { effectiveEnvironmentScopedResources: true, env: envKey }
306
305
  : null;
307
- await helpers.checkPrerequisites(appName, appConfig, debug, options.skipInfraCheck === true, options);
308
- await checkAndStopContainer(appName, appConfig, options, debug);
309
- const hostPort = await calculateHostPort(appConfig, options, debug);
306
+ const { resolveRunImageWithLocalFallback } = require('./run-resolve-image');
307
+ const resolvedRef = await resolveRunImageWithLocalFallback(appName, appConfig, options);
308
+ const effectiveRunOptions = {
309
+ ...options,
310
+ image: `${resolvedRef.imageName}:${resolvedRef.imageTag}`
311
+ };
312
+ await helpers.checkPrerequisites(appName, appConfig, debug, effectiveRunOptions.skipInfraCheck === true, effectiveRunOptions);
313
+ await checkAndStopContainer(appName, appConfig, effectiveRunOptions, debug);
314
+ const hostPort = await calculateHostPort(appConfig, effectiveRunOptions, debug);
310
315
  const runOptions = await resolveRunOptions(
311
316
  appName,
312
317
  appConfig,
313
- options,
318
+ effectiveRunOptions,
314
319
  envKey,
315
320
  debug,
316
321
  effectiveEnvironmentScopedResources
317
322
  );
323
+ applyTraefikFlagToRunOptions(runOptions, userCfg);
318
324
  const { composePath: tempComposePath, runEnvPath, runEnvAdminPath } = await helpers.prepareEnvironment(appName, appConfig, runOptions);
319
325
  const result = {
320
326
  appConfig,
@@ -329,6 +335,28 @@ async function prepareAppRun(appName, options, debug) {
329
335
  return result;
330
336
  }
331
337
 
338
+ /**
339
+ * Prepare run: validate app, load config, check prereqs, stop existing container, resolve port and reload, prepare env.
340
+ * @param {string} appName - Application name
341
+ * @param {Object} options - Run options
342
+ * @param {boolean} debug - Debug flag
343
+ * @returns {Promise<{ appConfig: Object, tempComposePath: string, hostPort: number }|null>} Prepared run context or null if should not continue
344
+ */
345
+ async function prepareAppRun(appName, options, debug) {
346
+ const envKey = (options.env || 'dev').toLowerCase();
347
+ assertValidRunEnvKey(envKey);
348
+ const shouldContinue = await validateAppForRun(appName, debug);
349
+ if (!shouldContinue) {
350
+ return null;
351
+ }
352
+ logger.log('');
353
+ logger.log(sectionTitle('Run'));
354
+ logger.log(headerKeyValue('Application:', appName));
355
+ const appConfig = await loadAndConfigureApp(appName, debug);
356
+ const userCfg = await config.getConfig();
357
+ return computeRunPreparationCore(appName, appConfig, options, envKey, userCfg, debug);
358
+ }
359
+
332
360
  /**
333
361
  * Runs the application locally using Docker
334
362
  * Starts container with proper port mapping and environment
@@ -360,10 +388,7 @@ async function runApp(appName, options = {}) {
360
388
  if (debug) {
361
389
  logger.log(chalk.gray(`[DEBUG] Compose file generated: ${prepared.tempComposePath}`));
362
390
  }
363
- if (options.reload && prepared.devMountPath) {
364
- logger.log(chalk.gray('With --reload: workspace mounted from host at /app (container runs as your user for write access).'));
365
- logger.log(chalk.gray(` Host path: ${prepared.devMountPath}`));
366
- }
391
+ logReloadDevSummary(Boolean(options.reload), prepared.mergedRunOptions.reloadSyncSummary);
367
392
  await startAppContainer(appName, prepared.tempComposePath, prepared.hostPort, prepared.appConfig, {
368
393
  debug,
369
394
  runEnvPath: prepared.runEnvPath,
@@ -401,6 +426,7 @@ async function restartApp(appName) {
401
426
  const containerName = containerHelpers.getContainerName(appName, developerId);
402
427
  try {
403
428
  await execWithDockerEnv(`docker restart ${containerName}`);
429
+ await logRestartDevMountSummary(containerName);
404
430
  } catch (error) {
405
431
  const msg = (error.stderr || error.stdout || error.message || '').toLowerCase();
406
432
  if (msg.includes('no such container') || msg.includes('is not running')) {