@aifabrix/builder 2.44.4 → 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 (214) 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 +68 -17
  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/types/wizard.types.js +2 -1
  17. package/lib/api/validation-runner.js +46 -25
  18. package/lib/app/deploy-config.js +11 -1
  19. package/lib/app/deploy-status-display.js +3 -3
  20. package/lib/app/deploy.js +36 -14
  21. package/lib/app/display.js +15 -11
  22. package/lib/app/push.js +46 -23
  23. package/lib/app/register.js +1 -1
  24. package/lib/app/restart-display.js +95 -0
  25. package/lib/app/rotate-secret.js +1 -1
  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 +44 -12
  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 +99 -73
  32. package/lib/build/index.js +75 -45
  33. package/lib/cli/doctor-check.js +117 -0
  34. package/lib/cli/index.js +8 -2
  35. package/lib/cli/infra-guided.js +445 -0
  36. package/lib/cli/setup-app.help.js +1 -1
  37. package/lib/cli/setup-app.js +20 -2
  38. package/lib/cli/setup-app.test-commands.js +9 -5
  39. package/lib/cli/setup-auth.js +26 -0
  40. package/lib/cli/setup-dev-path-commands.js +50 -3
  41. package/lib/cli/setup-infra.js +138 -61
  42. package/lib/cli/setup-integration-client.js +182 -0
  43. package/lib/cli/setup-parameters.js +21 -2
  44. package/lib/cli/setup-platform.js +102 -0
  45. package/lib/cli/setup-secrets.js +18 -6
  46. package/lib/cli/setup-utility.js +97 -33
  47. package/lib/commands/datasource-capability-dimension-cli.js +128 -0
  48. package/lib/commands/datasource-capability-output.js +29 -0
  49. package/lib/commands/datasource-capability-relate-cli.js +140 -0
  50. package/lib/commands/datasource-capability.js +411 -0
  51. package/lib/commands/datasource-unified-test-cli.options.js +1 -1
  52. package/lib/commands/datasource.js +53 -13
  53. package/lib/commands/dev-down.js +3 -3
  54. package/lib/commands/dev-infra-gate.js +32 -0
  55. package/lib/commands/dev-init.js +13 -7
  56. package/lib/commands/dimension-value.js +179 -0
  57. package/lib/commands/dimension.js +330 -0
  58. package/lib/commands/integration-client.js +430 -0
  59. package/lib/commands/login-device.js +65 -30
  60. package/lib/commands/login.js +21 -10
  61. package/lib/commands/parameters-validate.js +78 -13
  62. package/lib/commands/repair-datasource-auto-rbac.js +166 -0
  63. package/lib/commands/repair-datasource-keys.js +10 -5
  64. package/lib/commands/repair-datasource.js +19 -7
  65. package/lib/commands/repair-env-template.js +4 -1
  66. package/lib/commands/repair-openapi-sync.js +172 -0
  67. package/lib/commands/repair-persist.js +102 -0
  68. package/lib/commands/repair-rbac-extract.js +27 -0
  69. package/lib/commands/repair-rbac-migrate.js +186 -0
  70. package/lib/commands/repair-rbac.js +225 -19
  71. package/lib/commands/repair-system-alignment.js +246 -0
  72. package/lib/commands/repair-system-permissions.js +168 -0
  73. package/lib/commands/repair.js +120 -354
  74. package/lib/commands/secure.js +1 -1
  75. package/lib/commands/setup-modes.js +455 -0
  76. package/lib/commands/setup-prompts.js +388 -0
  77. package/lib/commands/setup.js +149 -0
  78. package/lib/commands/teardown.js +228 -0
  79. package/lib/commands/test-e2e-external.js +4 -3
  80. package/lib/commands/up-common.js +97 -12
  81. package/lib/commands/up-dataplane.js +33 -11
  82. package/lib/commands/up-miso.js +7 -11
  83. package/lib/commands/upload.js +109 -23
  84. package/lib/commands/wizard-core-helpers.js +14 -11
  85. package/lib/commands/wizard-core.js +58 -15
  86. package/lib/commands/wizard-dataplane.js +2 -2
  87. package/lib/commands/wizard-entity-selection.js +72 -14
  88. package/lib/commands/wizard-headless.js +7 -3
  89. package/lib/commands/wizard-helpers.js +13 -1
  90. package/lib/commands/wizard.js +210 -61
  91. package/lib/constants/infra-compose-service-names.js +40 -0
  92. package/lib/core/env-reader.js +16 -3
  93. package/lib/core/secrets-admin-env.js +101 -0
  94. package/lib/core/secrets-ensure-infra.js +34 -1
  95. package/lib/core/secrets-ensure.js +88 -66
  96. package/lib/core/secrets-env-content.js +432 -0
  97. package/lib/core/secrets-env-write.js +27 -1
  98. package/lib/core/secrets-load.js +248 -0
  99. package/lib/core/secrets-names.js +32 -0
  100. package/lib/core/secrets.js +17 -757
  101. package/lib/datasource/capability/basic-exposure.js +76 -0
  102. package/lib/datasource/capability/capability-diff-slice.js +41 -0
  103. package/lib/datasource/capability/capability-key.js +34 -0
  104. package/lib/datasource/capability/capability-resolve.js +172 -0
  105. package/lib/datasource/capability/capability-storage-keys.js +22 -0
  106. package/lib/datasource/capability/copy-operations.js +348 -0
  107. package/lib/datasource/capability/copy-test-payload.js +139 -0
  108. package/lib/datasource/capability/create-operations.js +235 -0
  109. package/lib/datasource/capability/dimension-operations.js +151 -0
  110. package/lib/datasource/capability/dimension-validate.js +219 -0
  111. package/lib/datasource/capability/json-pointer.js +31 -0
  112. package/lib/datasource/capability/reference-rewrite.js +51 -0
  113. package/lib/datasource/capability/relate-operations.js +325 -0
  114. package/lib/datasource/capability/relate-validate.js +219 -0
  115. package/lib/datasource/capability/remove-operations.js +275 -0
  116. package/lib/datasource/capability/run-capability-copy.js +152 -0
  117. package/lib/datasource/capability/run-capability-diff.js +135 -0
  118. package/lib/datasource/capability/run-capability-dimension.js +291 -0
  119. package/lib/datasource/capability/run-capability-edit.js +377 -0
  120. package/lib/datasource/capability/run-capability-relate.js +193 -0
  121. package/lib/datasource/capability/run-capability-remove.js +105 -0
  122. package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
  123. package/lib/datasource/capability/validate-capability-slice.js +35 -0
  124. package/lib/datasource/list.js +136 -23
  125. package/lib/datasource/log-viewer.js +2 -4
  126. package/lib/datasource/unified-validation-run.js +51 -16
  127. package/lib/datasource/validate.js +53 -1
  128. package/lib/deployment/deploy-poll-ui.js +60 -0
  129. package/lib/deployment/deployer-status.js +29 -3
  130. package/lib/deployment/deployer.js +48 -30
  131. package/lib/deployment/environment.js +7 -2
  132. package/lib/deployment/poll-interval.js +72 -0
  133. package/lib/deployment/push.js +11 -9
  134. package/lib/external-system/deploy.js +4 -1
  135. package/lib/external-system/download.js +61 -32
  136. package/lib/external-system/sync-deploy-manifest.js +33 -0
  137. package/lib/generator/wizard-prompts.js +7 -1
  138. package/lib/generator/wizard.js +34 -0
  139. package/lib/infrastructure/index.js +49 -19
  140. package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
  141. package/lib/parameters/infra-kv-discovery.js +29 -4
  142. package/lib/parameters/infra-parameter-catalog.js +6 -3
  143. package/lib/parameters/infra-parameter-validate.js +67 -19
  144. package/lib/resolvers/datasource-resolver.js +53 -0
  145. package/lib/resolvers/dimension-file.js +52 -0
  146. package/lib/resolvers/manifest-resolver.js +133 -0
  147. package/lib/schema/external-datasource.schema.json +183 -53
  148. package/lib/schema/external-system.schema.json +23 -10
  149. package/lib/schema/infra.parameter.yaml +26 -11
  150. package/lib/schema/wizard-config.schema.json +2 -2
  151. package/lib/utils/aifabrix-config-dir-walk.js +40 -0
  152. package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
  153. package/lib/utils/app-run-containers.js +2 -2
  154. package/lib/utils/bash-secret-env.js +59 -0
  155. package/lib/utils/cli-secrets-error-format.js +78 -0
  156. package/lib/utils/cli-test-layout-chalk.js +31 -9
  157. package/lib/utils/cli-utils.js +4 -36
  158. package/lib/utils/datasource-test-run-display.js +8 -0
  159. package/lib/utils/dev-hosts-helper.js +3 -2
  160. package/lib/utils/dev-init-ssh-merge.js +2 -1
  161. package/lib/utils/docker-build.js +17 -9
  162. package/lib/utils/docker-reload-mount.js +127 -0
  163. package/lib/utils/external-readme.js +117 -4
  164. package/lib/utils/external-system-local-test-tty.js +3 -2
  165. package/lib/utils/external-system-readiness-core.js +45 -12
  166. package/lib/utils/external-system-readiness-deploy-display.js +3 -3
  167. package/lib/utils/external-system-readiness-display-internals.js +33 -3
  168. package/lib/utils/external-system-readiness-display.js +10 -1
  169. package/lib/utils/file-upload.js +40 -3
  170. package/lib/utils/health-check-db-init.js +107 -0
  171. package/lib/utils/health-check-public-warn.js +69 -0
  172. package/lib/utils/health-check-url.js +19 -4
  173. package/lib/utils/health-check.js +135 -105
  174. package/lib/utils/help-builder.js +5 -1
  175. package/lib/utils/image-name.js +34 -7
  176. package/lib/utils/integration-file-backup.js +74 -0
  177. package/lib/utils/mutagen-install.js +30 -3
  178. package/lib/utils/paths.js +108 -25
  179. package/lib/utils/postgres-wipe.js +212 -0
  180. package/lib/utils/register-aifabrix-shell-env.js +15 -0
  181. package/lib/utils/remote-dev-auth.js +21 -5
  182. package/lib/utils/remote-docker-env.js +9 -1
  183. package/lib/utils/remote-secrets-loader.js +42 -3
  184. package/lib/utils/resolve-docker-image-ref.js +9 -3
  185. package/lib/utils/secrets-ancestor-paths.js +47 -0
  186. package/lib/utils/secrets-helpers.js +17 -10
  187. package/lib/utils/secrets-kv-refs.js +42 -0
  188. package/lib/utils/secrets-kv-scope.js +19 -2
  189. package/lib/utils/secrets-materialize-local.js +134 -0
  190. package/lib/utils/secrets-path.js +24 -10
  191. package/lib/utils/secrets-utils.js +2 -2
  192. package/lib/utils/system-builder-root.js +34 -0
  193. package/lib/utils/url-declarative-resolve-build.js +6 -1
  194. package/lib/utils/url-declarative-runtime-base-path.js +32 -0
  195. package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
  196. package/lib/utils/urls-local-registry.js +73 -20
  197. package/lib/utils/validation-poll-ui.js +81 -0
  198. package/lib/utils/validation-run-poll.js +29 -5
  199. package/lib/utils/with-muted-logger.js +53 -0
  200. package/package.json +1 -1
  201. package/templates/applications/dataplane/application.yaml +1 -1
  202. package/templates/applications/dataplane/rbac.yaml +10 -10
  203. package/templates/applications/keycloak/env.template +8 -6
  204. package/templates/applications/miso-controller/application.yaml +7 -0
  205. package/templates/applications/miso-controller/env.template +7 -7
  206. package/templates/applications/miso-controller/rbac.yaml +9 -9
  207. package/templates/external-system/README.md.hbs +89 -102
  208. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  209. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  210. package/.nyc_output/processinfo/index.json +0 -1
  211. package/lib/api/service-users.api.js +0 -150
  212. package/lib/api/types/service-users.types.js +0 -65
  213. package/lib/cli/setup-service-user.js +0 -187
  214. package/lib/commands/service-user.js +0 -429
@@ -26,6 +26,7 @@ const {
26
26
  registerHandlebarsHelper
27
27
  } = require('./helpers');
28
28
  const secretsEnsure = require('../core/secrets-ensure');
29
+ const { getRestartableInfraServiceNames } = require('../constants/infra-compose-service-names');
29
30
  const {
30
31
  buildTraefikConfig,
31
32
  validateTraefikConfig,
@@ -36,6 +37,7 @@ const {
36
37
  startDockerServicesAndConfigure,
37
38
  checkInfraHealth
38
39
  } = require('./services');
40
+ const { tryComposeProjectDown, stopInfraDockerStackOrphaned } = require('./orphan-infra-docker-teardown');
39
41
  const adminSecrets = require('../core/admin-secrets');
40
42
  // Lazy require to avoid circular dependency: infra -> app/down -> run-helpers -> infra
41
43
 
@@ -220,9 +222,18 @@ async function stopInfra() {
220
222
  const devId = await config.getDeveloperId();
221
223
  const { infraDir, adminSecretsPath } = resolveInfraStatePaths(devId);
222
224
  const composePath = path.join(infraDir, 'compose.yaml');
225
+ const hasCompose = fs.existsSync(composePath);
226
+ const hasAdminSecrets = fs.existsSync(adminSecretsPath);
223
227
 
224
- if (!fs.existsSync(composePath) || !fs.existsSync(adminSecretsPath)) {
225
- logger.log('Infrastructure not running or not properly configured');
228
+ if (!hasCompose || !hasAdminSecrets) {
229
+ logger.log('Infra compose or admin-secrets missing; stopping Docker stack (no volume delete)...');
230
+ await stopAllAppContainers(devId);
231
+ try {
232
+ await tryComposeProjectDown(devId, false);
233
+ } catch (err) {
234
+ logger.log(`Compose project down failed (${err.message}); applying orphan cleanup...`);
235
+ await stopInfraDockerStackOrphaned(devId, { removeVolumes: false });
236
+ }
226
237
  return;
227
238
  }
228
239
 
@@ -281,21 +292,34 @@ async function stopInfraWithVolumes() {
281
292
  const devId = await config.getDeveloperId();
282
293
  const { infraDir, adminSecretsPath } = resolveInfraStatePaths(devId);
283
294
  const composePath = path.join(infraDir, 'compose.yaml');
295
+ const hasCompose = fs.existsSync(composePath);
296
+ const hasAdminSecrets = fs.existsSync(adminSecretsPath);
284
297
 
285
- if (!fs.existsSync(composePath) || !fs.existsSync(adminSecretsPath)) {
286
- logger.log('Infrastructure not running or not properly configured');
298
+ await stopAllAppContainersAndVolumes(devId);
299
+
300
+ if (hasCompose && hasAdminSecrets) {
301
+ await withRunEnv(infraDir, adminSecretsPath, async(runEnvPath) => {
302
+ logger.log('Stopping infrastructure services and removing all data...');
303
+ const projectName = getInfraProjectName(devId);
304
+ const composeCmd = await dockerUtils.getComposeCommand();
305
+ try {
306
+ await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${runEnvPath}" down -v`, { cwd: infraDir });
307
+ } catch (err) {
308
+ logger.log(`Compose down failed (${err.message}); applying orphan cleanup...`);
309
+ await stopInfraDockerStackOrphaned(devId, { removeVolumes: true });
310
+ }
311
+ logger.log('Infrastructure services stopped and all data removed');
312
+ });
287
313
  return;
288
314
  }
289
315
 
290
- await withRunEnv(infraDir, adminSecretsPath, async(runEnvPath) => {
291
- logger.log('Stopping application containers on the same network...');
292
- await stopAllAppContainersAndVolumes(devId);
293
- logger.log('Stopping infrastructure services and removing all data...');
294
- const projectName = getInfraProjectName(devId);
295
- const composeCmd = await dockerUtils.getComposeCommand();
296
- await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${runEnvPath}" down -v`, { cwd: infraDir });
297
- logger.log('Infrastructure services stopped and all data removed');
298
- });
316
+ logger.log('Infra compose or admin-secrets missing; tearing down stuck Docker stack (project/volumes/network)...');
317
+ try {
318
+ await tryComposeProjectDown(devId, true);
319
+ } catch (err) {
320
+ logger.log(`Compose project down failed (${err.message}); applying orphan cleanup...`);
321
+ await stopInfraDockerStackOrphaned(devId, { removeVolumes: true });
322
+ }
299
323
  }
300
324
 
301
325
  /**
@@ -305,18 +329,19 @@ async function stopInfraWithVolumes() {
305
329
  * @async
306
330
  * @function restartService
307
331
  * @param {string} serviceName - Name of service to restart
332
+ * @param {{ suppressProgressLog?: boolean }} [options] - When `suppressProgressLog: true`, skip internal logger lines (CLI prints layout).
308
333
  * @returns {Promise<void>} Resolves when service is restarted
309
334
  * @throws {Error} If service doesn't exist or restart fails
310
335
  *
311
336
  * @example
312
- * await restartService('keycloak');
313
- * // Keycloak service is restarted
337
+ * await restartService('postgres');
338
+ * // Postgres service is restarted
314
339
  */
315
- async function restartService(serviceName) {
340
+ async function restartService(serviceName, options = {}) {
316
341
  if (!serviceName || typeof serviceName !== 'string') {
317
342
  throw new Error('Service name is required and must be a string');
318
343
  }
319
- const validServices = ['postgres', 'redis', 'pgadmin', 'redis-commander', 'traefik'];
344
+ const validServices = getRestartableInfraServiceNames();
320
345
  if (!validServices.includes(serviceName)) {
321
346
  throw new Error(`Invalid service name. Must be one of: ${validServices.join(', ')}`);
322
347
  }
@@ -329,12 +354,17 @@ async function restartService(serviceName) {
329
354
  throw new Error('Infrastructure not properly configured');
330
355
  }
331
356
 
357
+ const suppressLog = Boolean(options && options.suppressProgressLog);
332
358
  await withRunEnv(infraDir, adminSecretsPath, async(runEnvPath) => {
333
- logger.log(`Restarting ${serviceName} service...`);
359
+ if (!suppressLog) {
360
+ logger.log(`Restarting ${serviceName} service...`);
361
+ }
334
362
  const projectName = getInfraProjectName(devId);
335
363
  const composeCmd = await dockerUtils.getComposeCommand();
336
364
  await execAsyncWithCwd(`${composeCmd} -f "${composePath}" -p ${projectName} --env-file "${runEnvPath}" restart ${serviceName}`, { cwd: infraDir });
337
- logger.log(`${serviceName} service restarted successfully`);
365
+ if (!suppressLog) {
366
+ logger.log(`${serviceName} service restarted successfully`);
367
+ }
338
368
  });
339
369
  }
340
370
 
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Tear down AI Fabrix infra Docker resources when compose.yaml / admin-secrets
3
+ * are missing from disk (e.g. after manual deletes). Matches compose naming from
4
+ * `templates/infra/compose.yaml.hbs` and `lib/infrastructure/compose.js`.
5
+ *
6
+ * @fileoverview Orphan Docker teardown for developer-scoped infra stacks
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const dockerUtils = require('../utils/docker');
12
+ const logger = require('../utils/logger');
13
+ const { execAsyncWithCwd } = require('./services');
14
+ const { getInfraProjectName } = require('./helpers');
15
+
16
+ /**
17
+ * Bridge network name for this developer (must match compose generator).
18
+ * @param {string|number} devId - Developer ID
19
+ * @returns {string}
20
+ */
21
+ function getInfraBridgeNetworkName(devId) {
22
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
23
+ return idNum === 0 ? 'infra-aifabrix-network' : `infra-dev${devId}-aifabrix-network`;
24
+ }
25
+
26
+ /**
27
+ * Known infra service container names (postgres, redis, optional UIs).
28
+ * @param {string|number} devId - Developer ID
29
+ * @returns {string[]}
30
+ */
31
+ function getInfraServiceContainerNames(devId) {
32
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
33
+ if (idNum === 0) {
34
+ return ['aifabrix-postgres', 'aifabrix-redis', 'aifabrix-pgadmin', 'aifabrix-redis-commander', 'aifabrix-traefik'];
35
+ }
36
+ return [
37
+ `aifabrix-dev${devId}-postgres`,
38
+ `aifabrix-dev${devId}-redis`,
39
+ `aifabrix-dev${devId}-pgadmin`,
40
+ `aifabrix-dev${devId}-redis-commander`,
41
+ `aifabrix-dev${devId}-traefik`
42
+ ];
43
+ }
44
+
45
+ /**
46
+ * Named volumes declared in infra compose (explicit `name:` entries).
47
+ * @param {string|number} devId - Developer ID
48
+ * @returns {string[]}
49
+ */
50
+ function getInfraNamedVolumeCandidates(devId) {
51
+ const idNum = typeof devId === 'string' ? parseInt(devId, 10) : devId;
52
+ if (idNum === 0) {
53
+ return ['infra_postgres_data', 'infra_redis_data', 'infra_pgadmin_data'];
54
+ }
55
+ return [
56
+ `infra_dev${devId}_postgres_data`,
57
+ `infra_dev${devId}_redis_data`,
58
+ `infra_dev${devId}_pgadmin_data`
59
+ ];
60
+ }
61
+
62
+ /**
63
+ * `docker compose down` using only the Compose project name (works when the
64
+ * compose file was removed but the project still exists in Docker).
65
+ *
66
+ * @param {string|number} devId - Developer ID
67
+ * @param {boolean} withVolumes - Pass `-v` to remove named volumes
68
+ * @returns {Promise<void>}
69
+ */
70
+ async function tryComposeProjectDown(devId, withVolumes) {
71
+ const composeCmd = await dockerUtils.getComposeCommand();
72
+ const projectName = getInfraProjectName(devId);
73
+ const volFlag = withVolumes ? ' -v' : '';
74
+ await execAsyncWithCwd(`${composeCmd} -p "${projectName}" down${volFlag}`);
75
+ }
76
+
77
+ /**
78
+ * @param {string} name - Container name
79
+ * @returns {Promise<void>}
80
+ */
81
+ async function dockerRmForceOne(name) {
82
+ try {
83
+ await execAsyncWithCwd(`docker rm -f "${name}"`);
84
+ logger.log(`Stopped and removed container: ${name}`);
85
+ } catch {
86
+ logger.log(`Container ${name} not running or already removed`);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * @param {string} vol - Volume name
92
+ * @returns {Promise<void>}
93
+ */
94
+ async function dockerVolumeRmForceOne(vol) {
95
+ try {
96
+ await execAsyncWithCwd(`docker volume rm -f "${vol}"`);
97
+ logger.log(`Removed volume: ${vol}`);
98
+ } catch {
99
+ logger.log(`Volume ${vol} not found or already removed`);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Remove containers on the developer infra bridge network (covers strays).
105
+ * @param {string} networkName - Docker network name
106
+ * @returns {Promise<void>}
107
+ */
108
+ async function removeContainersOnNetwork(networkName) {
109
+ let stdout = '';
110
+ try {
111
+ ({ stdout } = await execAsyncWithCwd(
112
+ `docker network inspect "${networkName}" --format '{{json .Containers}}'`
113
+ ));
114
+ } catch {
115
+ return;
116
+ }
117
+ const raw = String(stdout || '').trim();
118
+ if (!raw || raw === 'null' || raw === '{}') return;
119
+ let containers;
120
+ try {
121
+ containers = JSON.parse(raw);
122
+ } catch {
123
+ return;
124
+ }
125
+ for (const endpoint of Object.values(containers)) {
126
+ const name = endpoint && typeof endpoint.Name === 'string' ? endpoint.Name.replace(/^\//, '') : '';
127
+ if (name) await dockerRmForceOne(name);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Remove bridge network if present.
133
+ * @param {string} networkName - Docker network name
134
+ * @returns {Promise<void>}
135
+ */
136
+ async function removeNetworkIfPresent(networkName) {
137
+ try {
138
+ await execAsyncWithCwd(`docker network rm "${networkName}"`);
139
+ logger.log(`Removed network: ${networkName}`);
140
+ } catch {
141
+ logger.log(`Network ${networkName} not removed (still in use or already gone)`);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Last-resort teardown: remove known infra containers, optional volumes, then network.
147
+ *
148
+ * @param {string|number} devId - Developer ID
149
+ * @param {{ removeVolumes?: boolean }} [opts]
150
+ * @returns {Promise<void>}
151
+ */
152
+ async function stopInfraDockerStackOrphaned(devId, opts = {}) {
153
+ const removeVolumes = opts.removeVolumes !== false;
154
+ const networkName = getInfraBridgeNetworkName(devId);
155
+
156
+ for (const name of getInfraServiceContainerNames(devId)) {
157
+ await dockerRmForceOne(name);
158
+ }
159
+
160
+ await removeContainersOnNetwork(networkName);
161
+
162
+ if (removeVolumes) {
163
+ for (const vol of getInfraNamedVolumeCandidates(devId)) {
164
+ await dockerVolumeRmForceOne(vol);
165
+ }
166
+ }
167
+
168
+ await removeNetworkIfPresent(networkName);
169
+ }
170
+
171
+ module.exports = {
172
+ getInfraBridgeNetworkName,
173
+ getInfraServiceContainerNames,
174
+ getInfraNamedVolumeCandidates,
175
+ tryComposeProjectDown,
176
+ stopInfraDockerStackOrphaned
177
+ };
@@ -30,6 +30,23 @@ function extractKvKeysFromEnvContent(content) {
30
30
  return [...keys];
31
31
  }
32
32
 
33
+ /**
34
+ * List builder app directories only (no integration/* apps).
35
+ * Used by `parameters validate` so sample integrations do not require catalog entries.
36
+ * @param {object} pathsUtil - paths module
37
+ * @returns {{ appKey: string, dir: string }[]}
38
+ */
39
+ function listBuilderAppDirsForDiscovery(pathsUtil) {
40
+ const out = [];
41
+ for (const name of pathsUtil.listBuilderAppNames()) {
42
+ const dir = pathsUtil.getBuilderPath(name);
43
+ if (fsRealSync.existsSync(dir)) {
44
+ out.push({ appKey: name, dir });
45
+ }
46
+ }
47
+ return out;
48
+ }
49
+
33
50
  /**
34
51
  * List app directories for discovery (builder first, then integration-only apps).
35
52
  * @param {object} pathsUtil - paths module
@@ -99,13 +116,20 @@ function discoverKvKeysFromEnvTemplatesForHook(pathsUtil, hook, catalog) {
99
116
  }
100
117
 
101
118
  /**
102
- * Full key list for ensureInfraSecrets: catalog exact upInfra + standard miso DB + derived DB + template hooks.
103
- * @param {{ getEnsureOnKeys: Function, keyMatchesEnsureHook: Function }} catalog
119
+ * Keys `ensureInfraSecrets` (`up-infra`) may write locally:
120
+ * - {@link standardUpInfraEnsureKeys} in infra.parameter.yaml (explicit bootstrap + Redis/Postgres core)
121
+ * - `databases-{app}-{i}-*` derived from each workspace app `requires.databases`
122
+ * - `kv://` names from env.template lines whose catalog entry includes `upInfra`
123
+ *
124
+ * Does **not** union every `parameters[].ensureOn: upInfra` catalog entry—those would create dozens of
125
+ * unused Azure/BASH/app placeholders (often emptyString locally). Apps pull those via `resolve` / run when needed.
126
+ *
127
+ * @param {{ getStandardUpInfraBootstrapKeys: Function, keyMatchesEnsureHook: Function }} catalog
104
128
  * @param {object} pathsUtil - paths module
105
129
  * @returns {string[]}
106
130
  */
107
131
  function getAllInfraEnsureKeys(catalog, pathsUtil) {
108
- const set = new Set(catalog.getEnsureOnKeys('upInfra'));
132
+ const set = new Set();
109
133
  for (const k of catalog.getStandardUpInfraBootstrapKeys()) set.add(k);
110
134
  for (const k of deriveDatabaseKvKeysFromWorkspace(pathsUtil)) set.add(k);
111
135
  for (const k of discoverKvKeysFromEnvTemplatesForHook(pathsUtil, 'upInfra', catalog)) set.add(k);
@@ -117,5 +141,6 @@ module.exports = {
117
141
  deriveDatabaseKvKeysFromWorkspace,
118
142
  discoverKvKeysFromEnvTemplatesForHook,
119
143
  getAllInfraEnsureKeys,
120
- listAppDirsForDiscovery
144
+ listAppDirsForDiscovery,
145
+ listBuilderAppDirsForDiscovery
121
146
  };
@@ -133,7 +133,10 @@ function createCatalogApi(doc, exact, patterns) {
133
133
 
134
134
  function isKeyAllowedEmpty(key) {
135
135
  const entry = findEntryForKey(key);
136
- return Boolean(entry && entry.generator && entry.generator.type === 'emptyAllowed');
136
+ const gen = entry && entry.generator;
137
+ // emptyAllowed: e.g. Redis password when no requirepass.
138
+ // emptyString: optional user-supplied keys (OpenAI / Azure OpenAI) — absent resolves like ''.
139
+ return Boolean(gen && (gen.type === 'emptyAllowed' || gen.type === 'emptyString'));
137
140
  }
138
141
 
139
142
  function keyMatchesEnsureHook(key, hook) {
@@ -236,7 +239,7 @@ function readRelaxedUpInfraEnsureKeyList(catalogPath = DEFAULT_CATALOG_PATH) {
236
239
  }
237
240
 
238
241
  /**
239
- * YAML-only: keys whose generator type is emptyAllowed (for ensure backfill behavior).
242
+ * YAML-only: keys whose generator type is emptyAllowed or emptyString (optional / absent OK).
240
243
  *
241
244
  * @param {string} [catalogPath]
242
245
  * @returns {Set<string>|null}
@@ -254,7 +257,7 @@ function readRelaxedEmptyAllowedKeySet(catalogPath = DEFAULT_CATALOG_PATH) {
254
257
  typeof entry.key === 'string' &&
255
258
  entry.key.trim() &&
256
259
  entry.generator &&
257
- entry.generator.type === 'emptyAllowed'
260
+ (entry.generator.type === 'emptyAllowed' || entry.generator.type === 'emptyString')
258
261
  ) {
259
262
  set.add(entry.key.trim());
260
263
  }
@@ -6,38 +6,86 @@
6
6
 
7
7
  const { nodeFs } = require('../internal/node-fs');
8
8
  const path = require('path');
9
- const { listAppDirsForDiscovery, extractKvKeysFromEnvContent } = require('./infra-kv-discovery');
9
+ const {
10
+ listBuilderAppDirsForDiscovery,
11
+ extractKvKeysFromEnvContent
12
+ } = require('./infra-kv-discovery');
10
13
 
11
- /**
12
- * Validate that every kv:// key in workspace env.template files has catalog coverage.
13
- * @param {{ findEntryForKey: Function }} catalog - Loaded catalog API
14
- * @param {object} pathsUtil - paths module
15
- * @returns {{ valid: boolean, errors: string[] }}
16
- */
17
- function validateWorkspaceKvRefsAgainstCatalog(catalog, pathsUtil) {
18
- const errors = [];
14
+ function _scanEnvTemplate(fs, cwd, envPath) {
15
+ const rel = path.relative(cwd, envPath) || envPath;
16
+ try {
17
+ const content = fs.readFileSync(envPath, 'utf8');
18
+ return { rel, keys: extractKvKeysFromEnvContent(content), readError: null };
19
+ } catch (e) {
20
+ return { rel, keys: [], readError: e };
21
+ }
22
+ }
23
+
24
+ function _scanBuilderEnvTemplates(catalog, pathsUtil) {
19
25
  const cwd = process.cwd();
20
26
  const fs = nodeFs();
27
+ const errors = [];
28
+ const scannedApps = [];
29
+ const scannedEnvTemplates = [];
30
+ const kvKeysUnique = new Set();
21
31
 
22
- for (const { dir } of listAppDirsForDiscovery(pathsUtil)) {
32
+ for (const { dir } of listBuilderAppDirsForDiscovery(pathsUtil)) {
33
+ scannedApps.push(path.relative(cwd, dir) || dir);
23
34
  const envPath = path.join(dir, 'env.template');
24
35
  if (!fs.existsSync(envPath)) continue;
25
- let content;
26
- try {
27
- content = fs.readFileSync(envPath, 'utf8');
28
- } catch (e) {
29
- errors.push(`Could not read ${envPath}: ${e.message}`);
36
+ const scan = _scanEnvTemplate(fs, cwd, envPath);
37
+ scannedEnvTemplates.push(scan.rel);
38
+ if (scan.readError) {
39
+ errors.push({
40
+ key: '__read_error__',
41
+ envTemplatePath: scan.rel,
42
+ message: scan.readError.message
43
+ });
30
44
  continue;
31
45
  }
32
- const rel = path.relative(cwd, envPath) || envPath;
33
- for (const k of extractKvKeysFromEnvContent(content)) {
46
+ for (const k of scan.keys) {
47
+ kvKeysUnique.add(k);
34
48
  if (!catalog.findEntryForKey(k)) {
35
- errors.push(`Unknown kv:// key "${k}" in ${rel} — extend lib/schema/infra.parameter.yaml`);
49
+ errors.push({ key: k, envTemplatePath: scan.rel });
36
50
  }
37
51
  }
38
52
  }
39
53
 
40
- return { valid: errors.length === 0, errors };
54
+ return { errors, scannedApps, scannedEnvTemplates, kvKeysUnique };
55
+ }
56
+
57
+ /**
58
+ * Validate that every kv:// key under builder app env.template files has catalog coverage.
59
+ * Integration apps under integration/ are not scanned (they often use ad-hoc kv keys).
60
+ * @param {{ findEntryForKey: Function }} catalog - Loaded catalog API
61
+ * @param {object} pathsUtil - paths module
62
+ * @returns {{
63
+ * valid: boolean,
64
+ * errors: { key: string, envTemplatePath: string, message?: string }[],
65
+ * summary: {
66
+ * scannedApps: string[],
67
+ * scannedEnvTemplates: string[],
68
+ * kvKeysUnique: string[],
69
+ * kvKeysCount: number
70
+ * }
71
+ * }}
72
+ */
73
+ function validateWorkspaceKvRefsAgainstCatalog(catalog, pathsUtil) {
74
+ const { errors, scannedApps, scannedEnvTemplates, kvKeysUnique } = _scanBuilderEnvTemplates(
75
+ catalog,
76
+ pathsUtil
77
+ );
78
+
79
+ return {
80
+ valid: errors.length === 0,
81
+ errors,
82
+ summary: {
83
+ scannedApps: scannedApps.sort(),
84
+ scannedEnvTemplates: scannedEnvTemplates.sort(),
85
+ kvKeysUnique: [...kvKeysUnique].sort(),
86
+ kvKeysCount: kvKeysUnique.size
87
+ }
88
+ };
41
89
  }
42
90
 
43
91
  /**
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Shared datasource resolver utilities.
3
+ *
4
+ * Intentionally CLI-agnostic (no commander/options), so it can be reused by commands and tests.
5
+ *
6
+ * @fileoverview Datasource path + JSON loading helpers
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const { resolveValidateInputPath, resolvePathFromIntegrationDatasourceKey } = require('../datasource/validate');
15
+
16
+ /**
17
+ * @param {string} fileOrKey
18
+ * @returns {string} Absolute path to datasource JSON file
19
+ */
20
+ function resolveDatasourceJsonPath(fileOrKey) {
21
+ return resolveValidateInputPath(String(fileOrKey || '').trim());
22
+ }
23
+
24
+ /**
25
+ * Attempt to resolve a datasource key under integration/<app>/ without throwing.
26
+ *
27
+ * @param {string} datasourceKey
28
+ * @returns {{ ok: true, path: string } | { ok: false, error: string }}
29
+ */
30
+ function tryResolveDatasourceKeyToLocalPath(datasourceKey) {
31
+ try {
32
+ const p = resolvePathFromIntegrationDatasourceKey(String(datasourceKey || '').trim());
33
+ return { ok: true, path: p };
34
+ } catch (e) {
35
+ return { ok: false, error: e?.message || String(e) };
36
+ }
37
+ }
38
+
39
+ /**
40
+ * @param {string} jsonPath
41
+ * @returns {any}
42
+ */
43
+ function readJsonFile(jsonPath) {
44
+ const raw = fs.readFileSync(jsonPath, 'utf8');
45
+ return JSON.parse(raw);
46
+ }
47
+
48
+ module.exports = {
49
+ resolveDatasourceJsonPath,
50
+ tryResolveDatasourceKeyToLocalPath,
51
+ readJsonFile
52
+ };
53
+
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Dimension file helpers for `aifabrix dimension create --file`.
3
+ *
4
+ * @fileoverview Dimension file parsing
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ /**
15
+ * @typedef {Object} DimensionCreateInput
16
+ * @property {string} key
17
+ * @property {string} displayName
18
+ * @property {string} [description]
19
+ * @property {'string'|'number'|'boolean'} dataType
20
+ * @property {boolean} [isRequired]
21
+ * @property {Array<{ value: string, displayName?: string, description?: string }>} [values]
22
+ */
23
+
24
+ /**
25
+ * @param {string} filePath
26
+ * @returns {DimensionCreateInput}
27
+ */
28
+ function readDimensionCreateFile(filePath) {
29
+ const p = path.resolve(String(filePath || '').trim());
30
+ if (!p) {
31
+ throw new Error('--file is required');
32
+ }
33
+ if (!fs.existsSync(p)) {
34
+ throw new Error(`File not found: ${p}`);
35
+ }
36
+ const raw = fs.readFileSync(p, 'utf8');
37
+ let parsed;
38
+ try {
39
+ parsed = JSON.parse(raw);
40
+ } catch (e) {
41
+ throw new Error(`Invalid JSON in ${p}: ${e.message}`);
42
+ }
43
+ if (!parsed || typeof parsed !== 'object') {
44
+ throw new Error(`Dimension file must be a JSON object: ${p}`);
45
+ }
46
+ return parsed;
47
+ }
48
+
49
+ module.exports = {
50
+ readDimensionCreateFile
51
+ };
52
+