@aifabrix/builder 2.44.5 → 2.45.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 (249) hide show
  1. package/.cursor/rules/cli-layout.mdc +8 -4
  2. package/.cursor/rules/project-rules.mdc +1 -1
  3. package/README.md +15 -23
  4. package/integration/hubspot-test/README.md +2 -0
  5. package/integration/hubspot-test/test.js +5 -3
  6. package/jest.projects.js +104 -2
  7. package/lib/api/controller-health.api.js +49 -0
  8. package/lib/api/dimension-values.api.js +82 -0
  9. package/lib/api/dimensions.api.js +114 -0
  10. package/lib/api/external-systems.api.js +1 -0
  11. package/lib/api/integration-clients.api.js +168 -0
  12. package/lib/api/types/dimension-values.types.js +28 -0
  13. package/lib/api/types/dimensions.types.js +31 -0
  14. package/lib/api/types/integration-clients.types.js +45 -0
  15. package/lib/api/validation-runner.js +46 -25
  16. package/lib/app/deploy-config.js +11 -1
  17. package/lib/app/deploy-status-display.js +3 -3
  18. package/lib/app/deploy.js +36 -14
  19. package/lib/app/display.js +15 -11
  20. package/lib/app/helpers.js +3 -3
  21. package/lib/app/index.js +3 -3
  22. package/lib/app/push.js +46 -23
  23. package/lib/app/register.js +7 -6
  24. package/lib/app/restart-display.js +126 -0
  25. package/lib/app/rotate-secret.js +7 -6
  26. package/lib/app/run-container-start.js +12 -6
  27. package/lib/app/run-env-compose.js +30 -1
  28. package/lib/app/run-helpers.js +58 -19
  29. package/lib/app/run-reload-sync.js +148 -0
  30. package/lib/app/run-resolve-image.js +51 -1
  31. package/lib/app/run.js +148 -74
  32. package/lib/app/show-display.js +7 -0
  33. package/lib/app/show.js +87 -5
  34. package/lib/build/index.js +83 -49
  35. package/lib/cli/doctor-check.js +117 -0
  36. package/lib/cli/index.js +8 -2
  37. package/lib/cli/infra-guided.js +460 -0
  38. package/lib/cli/installation-log-command.js +73 -0
  39. package/lib/cli/setup-app.js +31 -3
  40. package/lib/cli/setup-auth.js +98 -27
  41. package/lib/cli/setup-dev-path-commands.js +50 -3
  42. package/lib/cli/setup-infra-up-dataplane-action.js +111 -0
  43. package/lib/cli/setup-infra-up-platform-action.js +131 -0
  44. package/lib/cli/setup-infra.js +132 -118
  45. package/lib/cli/setup-integration-client.js +182 -0
  46. package/lib/cli/setup-parameters.js +21 -2
  47. package/lib/cli/setup-platform.js +102 -0
  48. package/lib/cli/setup-secrets.js +18 -6
  49. package/lib/cli/setup-utility-resolve.js +132 -0
  50. package/lib/cli/setup-utility.js +143 -84
  51. package/lib/commands/app-logs.js +81 -33
  52. package/lib/commands/auth-config.js +116 -18
  53. package/lib/commands/datasource-capability-dimension-cli.js +128 -0
  54. package/lib/commands/datasource-capability-output.js +29 -0
  55. package/lib/commands/datasource-capability-relate-cli.js +140 -0
  56. package/lib/commands/datasource-capability.js +411 -0
  57. package/lib/commands/datasource-unified-test-cli.options.js +1 -1
  58. package/lib/commands/datasource.js +53 -13
  59. package/lib/commands/dev-down.js +3 -3
  60. package/lib/commands/dev-infra-gate.js +32 -0
  61. package/lib/commands/dev-init.js +13 -7
  62. package/lib/commands/dimension-value.js +179 -0
  63. package/lib/commands/dimension.js +330 -0
  64. package/lib/commands/integration-client.js +430 -0
  65. package/lib/commands/login-device.js +65 -30
  66. package/lib/commands/login.js +21 -10
  67. package/lib/commands/parameters-validate.js +78 -13
  68. package/lib/commands/repair-datasource-auto-rbac.js +166 -0
  69. package/lib/commands/repair-datasource-keys.js +10 -5
  70. package/lib/commands/repair-datasource.js +19 -7
  71. package/lib/commands/repair-env-template.js +4 -1
  72. package/lib/commands/repair-openapi-sync.js +172 -0
  73. package/lib/commands/repair-persist.js +102 -0
  74. package/lib/commands/repair-rbac-extract.js +27 -0
  75. package/lib/commands/repair-rbac-migrate.js +186 -0
  76. package/lib/commands/repair-rbac.js +214 -31
  77. package/lib/commands/repair-system-alignment.js +246 -0
  78. package/lib/commands/repair-system-permissions.js +168 -0
  79. package/lib/commands/repair.js +120 -338
  80. package/lib/commands/secure.js +1 -1
  81. package/lib/commands/setup-modes.js +468 -0
  82. package/lib/commands/setup-prompts.js +421 -0
  83. package/lib/commands/setup.js +254 -0
  84. package/lib/commands/teardown.js +277 -0
  85. package/lib/commands/up-common.js +113 -19
  86. package/lib/commands/up-dataplane.js +44 -19
  87. package/lib/commands/up-miso.js +18 -18
  88. package/lib/commands/upload.js +111 -23
  89. package/lib/commands/wizard-core-helpers.js +14 -11
  90. package/lib/commands/wizard-core.js +6 -5
  91. package/lib/commands/wizard-dataplane.js +2 -2
  92. package/lib/commands/wizard-entity-selection.js +4 -3
  93. package/lib/commands/wizard-headless.js +2 -1
  94. package/lib/commands/wizard.js +2 -1
  95. package/lib/constants/infra-compose-service-names.js +40 -0
  96. package/lib/core/audit-logger.js +1 -34
  97. package/lib/core/config-admin-email.js +56 -0
  98. package/lib/core/config-normalize.js +60 -0
  99. package/lib/core/config-registered-controller-urls.js +54 -0
  100. package/lib/core/config.js +33 -50
  101. package/lib/core/env-reader.js +16 -3
  102. package/lib/core/secrets-admin-env.js +101 -0
  103. package/lib/core/secrets-ensure-infra.js +34 -1
  104. package/lib/core/secrets-ensure.js +88 -66
  105. package/lib/core/secrets-env-content.js +428 -0
  106. package/lib/core/secrets-env-declarative-expand.js +170 -0
  107. package/lib/core/secrets-env-write.js +29 -1
  108. package/lib/core/secrets-load.js +252 -0
  109. package/lib/core/secrets-names.js +32 -0
  110. package/lib/core/secrets.js +17 -757
  111. package/lib/datasource/capability/basic-exposure.js +76 -0
  112. package/lib/datasource/capability/capability-diff-slice.js +41 -0
  113. package/lib/datasource/capability/capability-key.js +34 -0
  114. package/lib/datasource/capability/capability-resolve.js +172 -0
  115. package/lib/datasource/capability/capability-storage-keys.js +22 -0
  116. package/lib/datasource/capability/copy-operations.js +348 -0
  117. package/lib/datasource/capability/copy-test-payload.js +139 -0
  118. package/lib/datasource/capability/create-operations.js +235 -0
  119. package/lib/datasource/capability/dimension-operations.js +151 -0
  120. package/lib/datasource/capability/dimension-validate.js +219 -0
  121. package/lib/datasource/capability/json-pointer.js +31 -0
  122. package/lib/datasource/capability/reference-rewrite.js +51 -0
  123. package/lib/datasource/capability/relate-operations.js +325 -0
  124. package/lib/datasource/capability/relate-validate.js +219 -0
  125. package/lib/datasource/capability/remove-operations.js +275 -0
  126. package/lib/datasource/capability/run-capability-copy.js +152 -0
  127. package/lib/datasource/capability/run-capability-diff.js +135 -0
  128. package/lib/datasource/capability/run-capability-dimension.js +291 -0
  129. package/lib/datasource/capability/run-capability-edit.js +377 -0
  130. package/lib/datasource/capability/run-capability-relate.js +193 -0
  131. package/lib/datasource/capability/run-capability-remove.js +105 -0
  132. package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
  133. package/lib/datasource/capability/validate-capability-slice.js +35 -0
  134. package/lib/datasource/list.js +136 -23
  135. package/lib/datasource/log-viewer.js +2 -4
  136. package/lib/datasource/unified-validation-run.js +51 -16
  137. package/lib/datasource/validate.js +53 -1
  138. package/lib/deployment/deploy-poll-ui.js +60 -0
  139. package/lib/deployment/deployer-status.js +29 -3
  140. package/lib/deployment/deployer.js +48 -30
  141. package/lib/deployment/environment.js +7 -2
  142. package/lib/deployment/poll-interval.js +72 -0
  143. package/lib/deployment/push.js +11 -9
  144. package/lib/external-system/deploy.js +9 -2
  145. package/lib/external-system/download.js +61 -32
  146. package/lib/external-system/sync-deploy-manifest.js +33 -0
  147. package/lib/infrastructure/index.js +49 -19
  148. package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
  149. package/lib/internal/node-fs.js +2 -0
  150. package/lib/parameters/infra-kv-discovery.js +29 -4
  151. package/lib/parameters/infra-parameter-catalog.js +6 -3
  152. package/lib/parameters/infra-parameter-validate.js +67 -19
  153. package/lib/resolvers/datasource-resolver.js +53 -0
  154. package/lib/resolvers/dimension-file.js +52 -0
  155. package/lib/resolvers/manifest-resolver.js +133 -0
  156. package/lib/schema/application-schema.json +4 -0
  157. package/lib/schema/external-datasource.schema.json +183 -53
  158. package/lib/schema/external-system.schema.json +23 -10
  159. package/lib/schema/infra.parameter.yaml +26 -1
  160. package/lib/schema/wizard-config.schema.json +1 -1
  161. package/lib/utils/aifabrix-config-dir-walk.js +40 -0
  162. package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
  163. package/lib/utils/app-config-resolver.js +24 -1
  164. package/lib/utils/app-run-containers.js +2 -2
  165. package/lib/utils/applications-config-defaults.js +206 -0
  166. package/lib/utils/auth-config-validator.js +2 -12
  167. package/lib/utils/bash-secret-env.js +59 -0
  168. package/lib/utils/cli-secrets-error-format.js +78 -0
  169. package/lib/utils/cli-test-layout-chalk.js +31 -9
  170. package/lib/utils/cli-utils.js +4 -36
  171. package/lib/utils/compose-generate-docker-compose.js +111 -6
  172. package/lib/utils/compose-generator.js +17 -8
  173. package/lib/utils/controller-url.js +50 -7
  174. package/lib/utils/datasource-test-run-display.js +8 -0
  175. package/lib/utils/dev-hosts-helper.js +3 -2
  176. package/lib/utils/dev-init-ssh-merge.js +2 -1
  177. package/lib/utils/docker-build.js +17 -9
  178. package/lib/utils/docker-reload-mount.js +127 -0
  179. package/lib/utils/env-copy.js +99 -14
  180. package/lib/utils/env-template.js +5 -1
  181. package/lib/utils/external-readme.js +71 -2
  182. package/lib/utils/external-system-local-test-tty.js +3 -2
  183. package/lib/utils/external-system-readiness-core.js +45 -12
  184. package/lib/utils/external-system-readiness-deploy-display.js +3 -3
  185. package/lib/utils/external-system-readiness-display-internals.js +33 -3
  186. package/lib/utils/external-system-readiness-display.js +10 -1
  187. package/lib/utils/file-upload.js +40 -3
  188. package/lib/utils/health-check-db-init.js +107 -0
  189. package/lib/utils/health-check-public-warn.js +69 -0
  190. package/lib/utils/health-check-url.js +28 -10
  191. package/lib/utils/health-check.js +139 -107
  192. package/lib/utils/help-builder.js +5 -1
  193. package/lib/utils/image-name.js +34 -7
  194. package/lib/utils/infra-optional-service-flags.js +69 -0
  195. package/lib/utils/installation-log-core.js +282 -0
  196. package/lib/utils/installation-log-record.js +237 -0
  197. package/lib/utils/installation-log.js +123 -0
  198. package/lib/utils/integration-file-backup.js +74 -0
  199. package/lib/utils/log-redaction.js +105 -0
  200. package/lib/utils/manifest-location.js +164 -0
  201. package/lib/utils/manifest-source-emit.js +162 -0
  202. package/lib/utils/mutagen-install.js +30 -3
  203. package/lib/utils/paths.js +308 -76
  204. package/lib/utils/postgres-wipe.js +212 -0
  205. package/lib/utils/register-aifabrix-shell-env.js +15 -0
  206. package/lib/utils/remote-dev-auth.js +21 -5
  207. package/lib/utils/remote-docker-env.js +9 -1
  208. package/lib/utils/remote-secrets-loader.js +49 -4
  209. package/lib/utils/resolve-docker-image-ref.js +9 -3
  210. package/lib/utils/run-cli-flags.js +29 -0
  211. package/lib/utils/secrets-ancestor-paths.js +47 -0
  212. package/lib/utils/secrets-canonical.js +10 -3
  213. package/lib/utils/secrets-helpers.js +17 -10
  214. package/lib/utils/secrets-kv-refs.js +42 -0
  215. package/lib/utils/secrets-kv-scope.js +19 -2
  216. package/lib/utils/secrets-materialize-local.js +134 -0
  217. package/lib/utils/secrets-path.js +26 -13
  218. package/lib/utils/secrets-utils.js +20 -10
  219. package/lib/utils/system-builder-root.js +42 -0
  220. package/lib/utils/url-declarative-public-base.js +80 -12
  221. package/lib/utils/url-declarative-resolve-build-urls.js +238 -0
  222. package/lib/utils/url-declarative-resolve-build.js +24 -388
  223. package/lib/utils/url-declarative-resolve-expand-token.js +189 -0
  224. package/lib/utils/url-declarative-resolve-load-doc.js +12 -3
  225. package/lib/utils/url-declarative-resolve-surface-state.js +102 -0
  226. package/lib/utils/url-declarative-resolve.js +47 -7
  227. package/lib/utils/url-declarative-runtime-base-path.js +52 -0
  228. package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
  229. package/lib/utils/urls-local-registry-scan.js +103 -0
  230. package/lib/utils/urls-local-registry.js +158 -76
  231. package/lib/utils/validation-poll-ui.js +81 -0
  232. package/lib/utils/validation-run-poll.js +29 -5
  233. package/lib/utils/with-muted-logger.js +53 -0
  234. package/package.json +3 -1
  235. package/templates/applications/dataplane/application.yaml +5 -1
  236. package/templates/applications/dataplane/rbac.yaml +10 -10
  237. package/templates/applications/keycloak/env.template +8 -6
  238. package/templates/applications/miso-controller/application.yaml +9 -0
  239. package/templates/applications/miso-controller/env.template +27 -29
  240. package/templates/applications/miso-controller/rbac.yaml +9 -9
  241. package/templates/external-system/README.md.hbs +83 -123
  242. package/.npmrc.token +0 -1
  243. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  244. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  245. package/.nyc_output/processinfo/index.json +0 -1
  246. package/lib/api/service-users.api.js +0 -150
  247. package/lib/api/types/service-users.types.js +0 -65
  248. package/lib/cli/setup-service-user.js +0 -187
  249. package/lib/commands/service-user.js +0 -429
@@ -15,6 +15,99 @@ const paths = require('./paths');
15
15
  const { getInfraDirName } = require('../infrastructure/helpers');
16
16
  const { resolveMisoEnvironment } = require('./compose-miso-env');
17
17
 
18
+ /**
19
+ * When running a pulled Python image (no bind-mounted source), the image CMD may invoke the
20
+ * `uvicorn` console script, which is not always on PATH. Derive a `python -m uvicorn` command from
21
+ * `build.reloadStart` so generated compose matches the same app entrypoint as hot-reload runs.
22
+ *
23
+ * @param {string|undefined} reloadStart - Value from application.yaml `build.reloadStart`
24
+ * @returns {string|null} Shell command fragment (no `cd`, no `exec`; image compose prepends `exec ` in JS)
25
+ */
26
+ function derivePythonImageStartFromReload(reloadStart) {
27
+ if (typeof reloadStart !== 'string' || !reloadStart.trim()) {
28
+ return null;
29
+ }
30
+ const trimmed = reloadStart.trim().replace(/\s+--reload\b/g, '').trim();
31
+ if (/^python3?\s+-m\s+uvicorn\s+/i.test(trimmed)) {
32
+ return normalizeComposeShellPortRef(trimmed);
33
+ }
34
+ if (trimmed.startsWith('uvicorn ')) {
35
+ const rest = trimmed.slice('uvicorn '.length);
36
+ return normalizeComposeShellPortRef(`python -m uvicorn ${rest}`);
37
+ }
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * Compose sets `PORT` in the service environment; replace `${PORT:-...}` with `$$PORT` in the
43
+ * fragment so the compose file avoids ambiguous `:` parsing in flow scalars and Compose expands
44
+ * `$$` to `$` for the container shell (which then expands `PORT` from the service environment).
45
+ *
46
+ * @param {string} cmd - Shell command fragment
47
+ * @returns {string} Same fragment with `${PORT:-...}` replaced by `$$PORT` (Compose escapes `$$` → `$` for the shell)
48
+ */
49
+ function normalizeComposeShellPortRef(cmd) {
50
+ return cmd.replace(/\$\{PORT:-[^}]+\}/g, () => '$$PORT');
51
+ }
52
+
53
+ /**
54
+ * Normalizes `build.reloadStart` for Python when using a bind-mounted source in compose.
55
+ *
56
+ * @param {string} raw - Trimmed reload command
57
+ * @returns {string} Command suitable for `cd /app && …` in compose
58
+ */
59
+ function normalizePythonReloadForComposeMounted(raw) {
60
+ const s = normalizeComposeShellPortRef(raw);
61
+ if (/^python3?\s+-m\s+uvicorn\s+/i.test(s)) {
62
+ return s;
63
+ }
64
+ if (s.startsWith('uvicorn ')) {
65
+ return `python -m uvicorn ${s.slice('uvicorn '.length)}`;
66
+ }
67
+ return s;
68
+ }
69
+
70
+ /**
71
+ * Builds the single `reloadStartCommand` passed to compose templates (`command: …` when set).
72
+ *
73
+ * `applications.<app>.reload` in config.yaml (and `aifabrix run --reload`) only controls **bind-mount +
74
+ * `--reload`** via `devMountPath`. For pulled images without a mount, use optional `build.imageRun` in
75
+ * application.yaml, or (Python only) derive from `build.reloadStart` when `imageRun` is unset.
76
+ *
77
+ * @param {string} language - `application.yaml` build.language
78
+ * @param {string|null} devMountPath - Bind mount path when reload sync is active
79
+ * @param {string|undefined} reloadRaw - `build.reloadStart`
80
+ * @param {Object} [build] - `application.yaml` `build` object (`imageRun`, etc.)
81
+ * @returns {string|null}
82
+ */
83
+ function buildReloadStartCommandForCompose(language, devMountPath, reloadRaw, build = {}) {
84
+ const buildObj = build && typeof build === 'object' ? build : {};
85
+ const imageRunRaw = typeof buildObj.imageRun === 'string' ? buildObj.imageRun.trim() : '';
86
+ const reloadTrimmed = typeof reloadRaw === 'string' ? reloadRaw.trim() : '';
87
+
88
+ if (devMountPath) {
89
+ if (!reloadTrimmed) {
90
+ return null;
91
+ }
92
+ return language === 'python' ? normalizePythonReloadForComposeMounted(reloadTrimmed) : reloadTrimmed;
93
+ }
94
+
95
+ if (imageRunRaw) {
96
+ return normalizeComposeShellPortRef(imageRunRaw);
97
+ }
98
+
99
+ if (!reloadTrimmed) {
100
+ return null;
101
+ }
102
+
103
+ if (language === 'python') {
104
+ const imageCmd = derivePythonImageStartFromReload(reloadTrimmed);
105
+ return imageCmd ? `exec ${imageCmd}` : null;
106
+ }
107
+
108
+ return null;
109
+ }
110
+
18
111
  /**
19
112
  * Resolve infra `pgpass` path: prefer system config dir; when home differs, allow legacy layout.
20
113
  * @param {string|number} devId - Developer id (infra vs infra-dev{n})
@@ -119,14 +212,15 @@ async function loadComposeHead(deps, appName, appConfig, options) {
119
212
  * @param {object} head - From loadComposeHead
120
213
  * @returns {object}
121
214
  */
122
- function buildComposeLayouts(deps, appName, appConfig, head) {
215
+ function buildComposeLayouts(deps, appName, appConfig, head, options) {
123
216
  const { buildNetworkAndContainerNames, buildServiceConfig, buildVolumesConfig, buildNetworksConfig } = deps;
124
217
  const { port, imageOverride, devId, idNum, scoped, remoteServer } = head;
125
218
  const { networkName, containerName } = buildNetworkAndContainerNames(appName, devId, idNum, scoped);
126
219
  const serviceConfig = buildServiceConfig(appName, appConfig, port, devId, {
127
220
  imageOverride,
128
221
  scopeOpts: scoped,
129
- remoteServer
222
+ remoteServer,
223
+ omitAppTraefikLabels: options && options.omitAppTraefikLabels === true
130
224
  });
131
225
  const volumesConfig = buildVolumesConfig(appName);
132
226
  const networksConfig = buildNetworksConfig(appConfig);
@@ -161,8 +255,13 @@ async function resolveComposePathsAndSecrets(ctx) {
161
255
  );
162
256
  const devMountPath = resolveDevMountPath(options);
163
257
  const reloadRaw = appConfig.build?.reloadStart;
164
- const reloadStartCommand =
165
- devMountPath && typeof reloadRaw === 'string' && reloadRaw.trim().length > 0 ? reloadRaw.trim() : null;
258
+ const language = appConfig.build?.language || appConfig.language || 'typescript';
259
+ const reloadStartCommand = buildReloadStartCommandForCompose(
260
+ language,
261
+ devMountPath,
262
+ reloadRaw,
263
+ appConfig.build
264
+ );
166
265
  const infraPgpassPath = resolveInfraPgpassPath(devId, paths, fsSync.existsSync);
167
266
  const useInfraPgpass = serviceCf.requiresDatabase && fsSync.existsSync(infraPgpassPath);
168
267
  return {
@@ -185,7 +284,7 @@ async function resolveComposePathsAndSecrets(ctx) {
185
284
  */
186
285
  async function generateDockerComposeImpl(deps, appName, appConfig, options) {
187
286
  const head = await loadComposeHead(deps, appName, appConfig, options);
188
- const layouts = buildComposeLayouts(deps, appName, appConfig, head);
287
+ const layouts = buildComposeLayouts(deps, appName, appConfig, head, options);
189
288
  const side = await resolveComposePathsAndSecrets({
190
289
  options,
191
290
  appName,
@@ -213,4 +312,10 @@ function createGenerateDockerCompose(deps) {
213
312
  return (appName, appConfig, options) => generateDockerComposeImpl(deps, appName, appConfig, options);
214
313
  }
215
314
 
216
- module.exports = { createGenerateDockerCompose, resolveInfraPgpassPath };
315
+ module.exports = {
316
+ createGenerateDockerCompose,
317
+ resolveInfraPgpassPath,
318
+ derivePythonImageStartFromReload,
319
+ buildReloadStartCommandForCompose,
320
+ normalizePythonReloadForComposeMounted
321
+ };
@@ -282,10 +282,16 @@ function buildRequiresConfig(config) {
282
282
  * @param {string} [runExtras.imageOverride] - Full image reference for run (e.g. from --image)
283
283
  * @param {Object|null} [runExtras.scopeOpts] - Traefik / scoped compose options
284
284
  * @param {string|null|undefined} [runExtras.remoteServer] - For ${REMOTE_HOST} in frontDoorRouting.host
285
+ * @param {boolean} [runExtras.omitAppTraefikLabels] - When true (user config `traefik: false`), omit Traefik labels and BASE_PATH env from compose even if application.yaml enables frontDoorRouting
285
286
  * @returns {Object} Service configuration
286
287
  */
287
288
  function buildServiceConfig(appName, config, port, devId, runExtras = {}) {
288
- const { imageOverride = null, scopeOpts = null, remoteServer = null } = runExtras;
289
+ const {
290
+ imageOverride = null,
291
+ scopeOpts = null,
292
+ remoteServer = null,
293
+ omitAppTraefikLabels = false
294
+ } = runExtras;
289
295
  const containerPortValue = getContainerPort(config, 3000);
290
296
  const hostPort = port;
291
297
  const useTraefikScope =
@@ -305,13 +311,16 @@ function buildServiceConfig(appName, config, port, devId, runExtras = {}) {
305
311
  containerPort: containerPortValue, // Container port (always set, equals containerPort if exists, else port)
306
312
  hostPort: hostPort, // Host port (options.port if provided, else config.port)
307
313
  healthCheck,
308
- traefik: buildTraefikConfig(
309
- config,
310
- devId,
311
- scopeForHealthAndTraefik,
312
- remoteServer,
313
- healthCheck.path
314
- ),
314
+ traefik:
315
+ omitAppTraefikLabels === true
316
+ ? { enabled: false }
317
+ : buildTraefikConfig(
318
+ config,
319
+ devId,
320
+ scopeForHealthAndTraefik,
321
+ remoteServer,
322
+ healthCheck.path
323
+ ),
315
324
  ...buildRequiresConfig(config)
316
325
  };
317
326
  }
@@ -42,8 +42,38 @@ function normalizeUrl(url) {
42
42
  }
43
43
 
44
44
  /**
45
- * Get controller URL from logged-in user's device tokens
46
- * Returns the first available controller URL from device tokens stored in config
45
+ * True when `config.yaml` has a non-expired-looking device entry for this controller URL
46
+ * (normalized host/path comparison).
47
+ *
48
+ * @async
49
+ * @param {string} controllerUrl
50
+ * @returns {Promise<boolean>}
51
+ */
52
+ async function hasStoredDeviceTokenForController(controllerUrl) {
53
+ if (!controllerUrl || typeof controllerUrl !== 'string') {
54
+ return false;
55
+ }
56
+ const want = normalizeUrl(controllerUrl);
57
+ if (!want) {
58
+ return false;
59
+ }
60
+ try {
61
+ const userConfig = await config.getConfig();
62
+ if (!userConfig.device || typeof userConfig.device !== 'object') {
63
+ return false;
64
+ }
65
+ return Object.keys(userConfig.device).some((key) => normalizeUrl(key) === want);
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Get controller URL from logged-in user's device tokens.
73
+ * Prefers the entry under {@link config.controller} when it matches a `device` key; otherwise a
74
+ * single `device` entry; with multiple unrelated entries and no matching default, returns **null**
75
+ * (callers must not treat arbitrary key order as the active controller).
76
+ *
47
77
  * @async
48
78
  * @function getControllerUrlFromLoggedInUser
49
79
  * @returns {Promise<string|null>} Controller URL from logged-in user, or null if not found
@@ -60,11 +90,23 @@ async function getControllerUrlFromLoggedInUser() {
60
90
  return null;
61
91
  }
62
92
 
63
- // Return the first available controller URL (normalized)
64
- const firstControllerUrl = deviceUrls[0];
65
- return normalizeUrl(firstControllerUrl);
66
- } catch (error) {
67
- // If config doesn't exist or can't be read, return null
93
+ const cfgController =
94
+ userConfig.controller && typeof userConfig.controller === 'string'
95
+ ? normalizeUrl(userConfig.controller)
96
+ : '';
97
+ if (cfgController) {
98
+ const match = deviceUrls.find((u) => normalizeUrl(u) === cfgController);
99
+ if (match) {
100
+ return normalizeUrl(match);
101
+ }
102
+ }
103
+
104
+ if (deviceUrls.length === 1) {
105
+ return normalizeUrl(deviceUrls[0]);
106
+ }
107
+
108
+ return null;
109
+ } catch {
68
110
  return null;
69
111
  }
70
112
  }
@@ -109,6 +151,7 @@ async function resolveControllerUrl() {
109
151
 
110
152
  module.exports = {
111
153
  getDefaultControllerUrl,
154
+ hasStoredDeviceTokenForController,
112
155
  getControllerUrlFromLoggedInUser,
113
156
  getControllerFromConfig,
114
157
  resolveControllerUrl
@@ -300,12 +300,20 @@ function appendValidationIssueLines(lines, envelope, maxIssues = 5) {
300
300
  const code = iss && iss.code ? chalk.red(`[${iss.code}] `) : '';
301
301
  const msg = iss && iss.message ? String(iss.message) : JSON.stringify(iss);
302
302
  lines.push(` ${code}${chalk.yellow(msg)}`);
303
+ appendDpSec013Details(lines, iss);
303
304
  }
304
305
  if (issues.length > cap) {
305
306
  lines.push(chalk.gray(` … and ${issues.length - cap} more (see --json or debug full/raw)`));
306
307
  }
307
308
  }
308
309
 
310
+ function appendDpSec013Details(lines, iss) {
311
+ if (!iss || iss.code !== 'DP-SEC-013') return;
312
+ const perm = iss.details && iss.details.resolvedPermission ? String(iss.details.resolvedPermission) : '';
313
+ if (!perm) return;
314
+ lines.push(` ${chalk.gray('Missing permission:')} ${chalk.white(perm)}`);
315
+ }
316
+
309
317
  /**
310
318
  * @param {string[]} lines
311
319
  * @param {Object} envelope
@@ -12,6 +12,7 @@ const path = require('path');
12
12
  const readline = require('readline');
13
13
  const chalk = require('chalk');
14
14
  const { nodeFs } = require('../internal/node-fs');
15
+ const { formatSuccessLine } = require('./cli-layout-chalk');
15
16
 
16
17
  const IPV4_RE = /^(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)){3}$/;
17
18
 
@@ -238,7 +239,7 @@ async function resolveHostsIpForInit(hostname, hostsIp, logger) {
238
239
  async function appendHostsBlockOrPrintManual(hostsPath, block, line, logger) {
239
240
  try {
240
241
  await nodeFs().promises.appendFile(hostsPath, block, { encoding: 'utf8' });
241
- logger.log(chalk.green(` Updated ${hostsPath}\n`));
242
+ logger.log(chalk.green(' ') + formatSuccessLine(`Updated ${hostsPath}`) + '\n');
242
243
  } catch (e) {
243
244
  if (e.code === 'EACCES' || e.code === 'EPERM') {
244
245
  logger.log(chalk.yellow(` ✖ Could not write ${hostsPath} (permission denied).`));
@@ -261,7 +262,7 @@ async function appendHostsBlockOrPrintManual(hostsPath, block, line, logger) {
261
262
  async function tryWriteHostsEntry(hostsPath, hostnames, ip, skipConfirm, logger) {
262
263
  const missing = hostnames.filter((h) => !hostsFileHasHostname(hostsPath, h));
263
264
  if (missing.length === 0) {
264
- logger.log(chalk.green(` Required hostnames are already listed in ${hostsPath}. Nothing to do.\n`));
265
+ logger.log(chalk.green(' ') + formatSuccessLine(`Required hostnames are already listed in ${hostsPath}. Nothing to do.`) + '\n');
265
266
  return;
266
267
  }
267
268
  const line = `${ip} ${missing.join(' ')}`;
@@ -6,6 +6,7 @@ const chalk = require('chalk');
6
6
  const config = require('../core/config');
7
7
  const logger = require('./logger');
8
8
  const { ensureDevSshConfigBlock } = require('./dev-ssh-config-helper');
9
+ const { successGlyph } = require('./cli-layout-chalk');
9
10
 
10
11
  /**
11
12
  * Hostname from Builder Server URL (for sync-ssh-host fallback).
@@ -46,7 +47,7 @@ async function mergeDevSshConfigAfterInit(baseUrl, devId) {
46
47
  );
47
48
  } else {
48
49
  logger.log(
49
- chalk.green(' SSH config updated: ') +
50
+ `${chalk.green(' ')}${successGlyph()}${chalk.green(' SSH config updated: ')}` +
50
51
  chalk.cyan(`Host ${res.hostAlias}`) +
51
52
  chalk.gray(` → ${res.configPath}`)
52
53
  );
@@ -1,4 +1,4 @@
1
- const { formatSuccessLine } = require('./cli-test-layout-chalk');
1
+ const { formatSuccessLine, headerKeyValue, formatWarningLine } = require('./cli-test-layout-chalk');
2
2
  /**
3
3
  * Docker Build Utilities
4
4
  *
@@ -177,7 +177,6 @@ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag, b
177
177
  const spinner = ora({ text: 'Starting Docker build...', spinner: 'dots' }).start();
178
178
  const fsSync = require('fs');
179
179
  const path = require('path');
180
- const { getRemoteDockerEnv } = require('./remote-docker-env');
181
180
  dockerfilePath = path.resolve(dockerfilePath);
182
181
  contextPath = path.resolve(contextPath);
183
182
 
@@ -196,7 +195,8 @@ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag, b
196
195
  }
197
196
 
198
197
  const isTest = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
199
- const remoteEnv = isTest ? {} : await getRemoteDockerEnv();
198
+ const { getDockerExecEnv } = require('./remote-docker-env');
199
+ const dockerCliEnv = isTest ? { ...process.env } : await getDockerExecEnv();
200
200
  const resolvedBuildArgs = buildArgs && typeof buildArgs === 'object' ? buildArgs : {};
201
201
  return new Promise((resolve, reject) => {
202
202
  runDockerBuildProcess({
@@ -207,7 +207,7 @@ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag, b
207
207
  spinner,
208
208
  resolve,
209
209
  reject,
210
- env: remoteEnv,
210
+ env: dockerCliEnv,
211
211
  buildArgs: resolvedBuildArgs,
212
212
  noCache
213
213
  });
@@ -258,14 +258,20 @@ async function executeBuild(imageName, dockerfilePath, contextPath, tag, options
258
258
  */
259
259
  async function executeDockerBuildWithTag(effectiveImageName, imageName, dockerfilePath, contextPath, tag, options) {
260
260
  const logger = require('../utils/logger');
261
- const chalk = require('chalk');
262
261
 
263
- logger.log(chalk.blue(`Using Dockerfile: ${dockerfilePath}`));
264
- logger.log(chalk.blue(`Using build context: ${contextPath}`));
262
+ logger.log(headerKeyValue('Dockerfile:', dockerfilePath));
263
+ logger.log(headerKeyValue('Build context:', contextPath));
265
264
 
266
265
  await executeBuild(effectiveImageName, dockerfilePath, contextPath, tag, options);
267
266
 
268
- // Back-compat: also tag the built dev image as the base image name
267
+ const wantCompatTag =
268
+ effectiveImageName !== imageName &&
269
+ options &&
270
+ options.base === true;
271
+ if (!wantCompatTag) {
272
+ return;
273
+ }
274
+
269
275
  try {
270
276
  const { promisify } = require('util');
271
277
  const { exec } = require('child_process');
@@ -275,7 +281,9 @@ async function executeDockerBuildWithTag(effectiveImageName, imageName, dockerfi
275
281
  await run(`docker tag ${effectiveImageName}:${tag} ${imageName}:${tag}`, { env });
276
282
  logger.log(formatSuccessLine(`Tagged image: ${imageName}:${tag}`));
277
283
  } catch (err) {
278
- logger.log(chalk.yellow(`⚠ Warning: Could not create compatibility tag ${imageName}:${tag} - ${err.message}`));
284
+ logger.log(
285
+ formatWarningLine(`Could not create compatibility tag ${imageName}:${tag} - ${err.message}`)
286
+ );
279
287
  }
280
288
  }
281
289
 
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Decide whether `aifabrix run --reload` can bind-mount workspace without Mutagen.
3
+ * Mutagen is only needed when the CLI filesystem and the Docker engine host differ.
4
+ *
5
+ * @fileoverview Co-located Docker detection for reload mounts
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const os = require('os');
13
+
14
+ /**
15
+ * @param {string} host
16
+ * @returns {string}
17
+ */
18
+ function normalizeHost(host) {
19
+ return String(host || '')
20
+ .trim()
21
+ .toLowerCase()
22
+ .replace(/^\[|\]$/g, '');
23
+ }
24
+
25
+ /**
26
+ * @param {string} host
27
+ * @returns {boolean}
28
+ */
29
+ function isLocalLoopbackHost(host) {
30
+ const h = normalizeHost(host);
31
+ return h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '0:0:0:0:0:0:0:1';
32
+ }
33
+
34
+ /**
35
+ * @param {string} rest - after "tcp://"
36
+ * @returns {string|null}
37
+ */
38
+ function tcpHostFromRest(rest) {
39
+ if (rest.startsWith('[')) {
40
+ const end = rest.indexOf(']');
41
+ if (end === -1) return null;
42
+ return normalizeHost(rest.slice(1, end));
43
+ }
44
+ const hostPort = rest.split('/')[0];
45
+ const colonIdx = hostPort.lastIndexOf(':');
46
+ if (colonIdx === -1) {
47
+ return normalizeHost(hostPort);
48
+ }
49
+ const maybePort = hostPort.slice(colonIdx + 1);
50
+ if (/^\d+$/.test(maybePort)) {
51
+ return normalizeHost(hostPort.slice(0, colonIdx));
52
+ }
53
+ return normalizeHost(hostPort);
54
+ }
55
+
56
+ /**
57
+ * Extract host from docker-endpoint (tcp, optional URL form).
58
+ * @param {string} endpoint
59
+ * @returns {string|null} normalized host, or null for unix socket paths / empty
60
+ */
61
+ function extractHostFromDockerEndpoint(endpoint) {
62
+ const s = String(endpoint || '').trim();
63
+ if (!s) return null;
64
+ const lower = s.toLowerCase();
65
+ if (lower.startsWith('unix:')) {
66
+ return null;
67
+ }
68
+ if (lower.startsWith('tcp://')) {
69
+ return tcpHostFromRest(s.slice(6));
70
+ }
71
+ try {
72
+ const u = new URL(s.includes('://') ? s : `tcp://${s}`);
73
+ return normalizeHost(u.hostname);
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * sync-ssh-host localhost check (same rules as run.js isLocalhostHost for reload gate).
81
+ * @param {string} host
82
+ * @returns {boolean}
83
+ */
84
+ function isLocalhostSyncSshHost(host) {
85
+ if (!host || typeof host !== 'string') return false;
86
+ const h = host.trim().toLowerCase();
87
+ return h === 'localhost' || h === '127.0.0.1';
88
+ }
89
+
90
+ /**
91
+ * True when docker API host is this machine (first label or FQDN match).
92
+ * @param {string} dockerHost - from extractHostFromDockerEndpoint
93
+ * @returns {boolean}
94
+ */
95
+ function hostnameMatchesDockerHost(dockerHost) {
96
+ const h = normalizeHost(dockerHost);
97
+ if (!h) return true;
98
+ if (isLocalLoopbackHost(h)) return true;
99
+ const hn = normalizeHost(os.hostname());
100
+ if (h === hn) return true;
101
+ const hnShort = hn.split('.')[0];
102
+ const hShort = h.split('.')[0];
103
+ if (h === hnShort || hn === hShort) return true;
104
+ return false;
105
+ }
106
+
107
+ /**
108
+ * When true, use a direct bind mount for --reload; Mutagen is not required.
109
+ * @param {string|null|undefined} dockerEndpoint - config `docker-endpoint`
110
+ * @returns {boolean}
111
+ */
112
+ function isReloadBindMountOnEngineHost(dockerEndpoint) {
113
+ const e = String(dockerEndpoint || '').trim();
114
+ if (!e) return true;
115
+ if (e.toLowerCase().startsWith('unix:')) return true;
116
+ const host = extractHostFromDockerEndpoint(e);
117
+ if (host === null) {
118
+ return false;
119
+ }
120
+ return hostnameMatchesDockerHost(host);
121
+ }
122
+
123
+ module.exports = {
124
+ extractHostFromDockerEndpoint,
125
+ isReloadBindMountOnEngineHost,
126
+ isLocalhostSyncSshHost
127
+ };