@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
@@ -24,8 +24,8 @@ Subcommands:
24
24
 
25
25
  Also: aifabrix secure Encrypt secrets.local.yaml (ISO 27001)
26
26
 
27
- Default "secret set" (no --shared): writes only to your user secrets.local.yaml next to
28
- config.yaml (typically ~/.aifabrix/secrets.local.yaml). Use --shared to write the
27
+ Default "secret set" (no --shared): writes only to your user secrets.local.yaml under the
28
+ resolved AI Fabrix home (config key aifabrix-home, else ~/.aifabrix). Use --shared to write the
29
29
  shared store from config aifabrix-secrets (another file or https), not that user file.
30
30
 
31
31
  Examples:
@@ -46,8 +46,8 @@ Examples:
46
46
 
47
47
  const SECRET_SET_HELP_AFTER = `
48
48
  Where the value is stored:
49
- No --shared User file secrets.local.yaml in your aifabrix config directory (same
50
- folder as config.yaml; often ~/.aifabrix/). Used for app/integration
49
+ No --shared User file secrets.local.yaml under the resolved AI Fabrix home
50
+ (config key aifabrix-home, else ~/.aifabrix/). Used for app/integration
51
51
  secrets and kv:// resolution on your machine.
52
52
  --shared The store set in config.yaml as aifabrix-secrets: a YAML file path or
53
53
  an https secrets API (never the user-only file above unless you point
@@ -70,6 +70,13 @@ Examples:
70
70
  `;
71
71
 
72
72
  const SECRET_REMOVE_ALL_HELP_AFTER = `
73
+ Where keys are removed:
74
+ Default All keys in your user secrets.local.yaml (under resolved aifabrix home,
75
+ often ~/.aifabrix/). This is NOT the shared store unless you deliberately
76
+ point aifabrix-secrets there.
77
+ --shared Every key in the shared store from config aifabrix-secrets (YAML file
78
+ path or https secrets API). Requires that setting; use secret set-secrets-file if needed.
79
+
73
80
  You will be asked to type "yes" to confirm unless you pass --yes.
74
81
 
75
82
  Examples:
@@ -103,9 +110,14 @@ function setupSecretRemoveAllCommand(secretCmd) {
103
110
  const { handleSecretsRemoveAll } = require('../commands/secrets-remove-all');
104
111
  secretCmd
105
112
  .command('remove-all')
106
- .description('Remove all secret keys (requires typing "yes" unless --yes)')
113
+ .description(
114
+ 'Remove every key from user secrets.local.yaml (add --shared for aifabrix-secrets store; confirm unless --yes)'
115
+ )
107
116
  .addHelpText('after', SECRET_REMOVE_ALL_HELP_AFTER)
108
- .option('--shared', 'Remove all from shared secrets (file or remote API)')
117
+ .option(
118
+ '--shared',
119
+ 'Remove every key from shared secrets (config aifabrix-secrets: YAML file or https API)'
120
+ )
109
121
  .option('-y, --yes', 'Skip confirmation prompt (non-interactive / scripts)')
110
122
  .action(async options => {
111
123
  try {
@@ -46,6 +46,8 @@ Examples:
46
46
 
47
47
  $ aifabrix repair hubspot-demo --doc
48
48
  Regenerate README.md from the current deployment manifest only (other drift fixes unchanged).
49
+
50
+ Before overwriting files, repair writes timestamped backups under integration/<systemKey>/backup/ (same as datasource capability copy). Use --no-backup to skip.
49
51
  `;
50
52
 
51
53
  /**
@@ -193,50 +195,93 @@ function setupSplitJsonCommand(program) {
193
195
  });
194
196
  }
195
197
 
198
+ function buildRepairExternalIntegrationOptions(appName, options, format) {
199
+ const all = options.all === true;
200
+ return {
201
+ auth: options.auth,
202
+ doc: options.doc || all,
203
+ dryRun: options.dryRun,
204
+ format,
205
+ rbac: options.rbac || all,
206
+ expose: options.expose || all,
207
+ sync: options.sync || all,
208
+ api: options.api || all,
209
+ test: options.test || all,
210
+ backup: options.backup,
211
+ noBackup: options.noBackup
212
+ };
213
+ }
214
+
215
+ function logRepairResult(options, result) {
216
+ if (options.dryRun && result.updated && result.changes.length > 0) {
217
+ logger.log(chalk.yellow('\nWould apply:'));
218
+ result.changes.forEach(c => logger.log(chalk.gray(` ${c}`)));
219
+ return;
220
+ }
221
+ if (Array.isArray(result.backupPaths) && result.backupPaths.length > 0) {
222
+ result.backupPaths.forEach((p) => logger.log(chalk.gray(`Backup: ${p}`)));
223
+ }
224
+ if (result.updated) {
225
+ logger.log(formatSuccessParagraph('Repaired external integration config.'));
226
+ return;
227
+ }
228
+ if (Array.isArray(result.changes) && result.changes.length > 0) {
229
+ logger.log(formatSuccessParagraph('Repair actions completed.'));
230
+ result.changes.forEach(c => logger.log(chalk.gray(` ${c}`)));
231
+ return;
232
+ }
233
+ if (result.readmeRegenerated) {
234
+ logger.log(formatSuccessParagraph('Regenerated README.md from deployment manifest.'));
235
+ return;
236
+ }
237
+ logger.log(chalk.gray('No changes needed; config already matches files on disk.'));
238
+ }
239
+
240
+ /**
241
+ * Handler for `aifabrix repair <systemKey>`.
242
+ * @param {string} appName
243
+ * @param {object} options - Commander options
244
+ * @returns {Promise<void>}
245
+ */
246
+ async function handleRepairCommand(appName, options) {
247
+ const { repairExternalIntegration } = require('../commands/repair');
248
+ const { detectAppType } = require('../utils/paths');
249
+ const { appPath } = await detectAppType(appName);
250
+ logOfflinePathWhenType(appPath);
251
+ let format = 'yaml';
252
+ try {
253
+ const config = require('../core/config');
254
+ format = (await config.getFormat()) || format;
255
+ } catch (_) {
256
+ /* default yaml when config unavailable */
257
+ }
258
+ const result = await repairExternalIntegration(
259
+ appName,
260
+ buildRepairExternalIntegrationOptions(appName, options, format)
261
+ );
262
+ logRepairResult(options, result);
263
+ }
264
+
196
265
  function setupRepairCommand(program) {
197
266
  program.command('repair <systemKey>')
198
267
  .description('Fix external integration drift (files, RBAC, manifest, …)')
268
+ .option('--all', 'Run all repair actions (api, doc, expose, rbac, sync, test)')
269
+ .option(
270
+ '--api',
271
+ 'Validate and sync API contracts needed for OpenAPI/MCP (uses local OpenAPI specs at integration/<systemKey>/openapi/*.json when present)'
272
+ )
199
273
  .option('--auth <method>', 'Set authentication method (oauth2, aad, apikey, basic, queryParam, oidc, hmac, none); updates system file and env.template')
200
274
  .option('--doc', 'Regenerate README.md from deployment manifest')
201
- .option('--dry-run', 'Report changes only; do not write')
202
- .option('--rbac', 'Ensure RBAC permissions per datasource and add default Admin/Reader roles if none exist')
203
275
  .option('--expose', 'Set exposed.schema on each datasource from all fieldMappings.attributes keys (metadata.<key>); removes deprecated exposed.attributes if present')
276
+ .option('--rbac', 'Ensure RBAC permissions per datasource and add default Admin/Reader roles if none exist')
204
277
  .option('--sync', 'Add default sync section to datasources that lack it')
205
278
  .option('--test', 'Generate testPayload.payloadTemplate and testPayload.expectedResult from attributes')
279
+ .option('--no-backup', 'Skip timestamped copies under integration/<systemKey>/backup/')
280
+ .option('--dry-run', 'Report changes only; do not write')
206
281
  .addHelpText('after', REPAIR_HELP_AFTER)
207
282
  .action(async(appName, options) => {
208
283
  try {
209
- const { repairExternalIntegration } = require('../commands/repair');
210
- const { detectAppType } = require('../utils/paths');
211
- const { appPath } = await detectAppType(appName);
212
- logOfflinePathWhenType(appPath);
213
- let format = 'yaml';
214
- try {
215
- const config = require('../core/config');
216
- format = (await config.getFormat()) || format;
217
- } catch (_) {
218
- // use default yaml when config unavailable
219
- }
220
- const result = await repairExternalIntegration(appName, {
221
- auth: options.auth,
222
- doc: options.doc,
223
- dryRun: options.dryRun,
224
- format,
225
- rbac: options.rbac,
226
- expose: options.expose,
227
- sync: options.sync,
228
- test: options.test
229
- });
230
- if (options.dryRun && result.updated && result.changes.length > 0) {
231
- logger.log(chalk.yellow('\nWould apply:'));
232
- result.changes.forEach(c => logger.log(chalk.gray(` ${c}`)));
233
- } else if (result.updated) {
234
- logger.log(formatSuccessParagraph('Repaired external integration config.'));
235
- } else if (result.readmeRegenerated) {
236
- logger.log(formatSuccessParagraph('Regenerated README.md from deployment manifest.'));
237
- } else {
238
- logger.log(chalk.gray('No changes needed; config already matches files on disk.'));
239
- }
284
+ await handleRepairCommand(appName, options);
240
285
  } catch (error) {
241
286
  handleCommandError(error, 'repair');
242
287
  process.exit(1);
@@ -0,0 +1,128 @@
1
+ /**
2
+ * `datasource capability dimension` command registration.
3
+ *
4
+ * @fileoverview dimension binding CLI
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const logger = require('../utils/logger');
12
+ const {
13
+ formatBlockingError,
14
+ headerKeyValue,
15
+ infoLine,
16
+ formatSuccessLine,
17
+ colorRollupPrefixedLine
18
+ } = require('../utils/cli-test-layout-chalk');
19
+ const { runCapabilityDimension } = require('../datasource/capability/run-capability-dimension');
20
+ const { printCapabilitySuccessFooter } = require('./datasource-capability-output');
21
+
22
+ const CAP_DIMENSION_HELP = `
23
+ Adds or replaces one root **dimensions.<key>** binding (metadata-only; no pipeline).
24
+
25
+ Examples:
26
+ $ aifabrix datasource capability dimension test-e2e-hubspot-companies --dimension market --type local --field country
27
+ $ aifabrix datasource capability dimension test-e2e-hubspot-companies --dimension owner --type fk --via hubspotOwner:owner --actor email
28
+ `;
29
+
30
+ function parseVia(raw) {
31
+ const list = Array.isArray(raw) ? raw : [];
32
+ return list.map((x) => String(x).trim()).filter(Boolean);
33
+ }
34
+
35
+ function logDimensionValidationOutcome(result) {
36
+ logger.log(formatSuccessLine('Local validation passed'));
37
+ if (result.remoteValidation?.ok) {
38
+ logger.log(formatSuccessLine('Remote validation passed'));
39
+ } else {
40
+ logger.log(colorRollupPrefixedLine('⚠ Remote validation skipped (not authenticated)'));
41
+ }
42
+ if (Array.isArray(result.semanticWarnings) && result.semanticWarnings.length > 0) {
43
+ result.semanticWarnings.forEach((w) => logger.log(colorRollupPrefixedLine(`⚠ ${w}`)));
44
+ }
45
+ logger.log(formatSuccessLine('Dimension binding updated'));
46
+ }
47
+
48
+ /**
49
+ * @param {string} fileOrKey
50
+ * @param {object} options - Commander options
51
+ * @returns {Promise<void>}
52
+ */
53
+ async function runDimensionAction(fileOrKey, options) {
54
+ const via = parseVia(options.via);
55
+
56
+ const result = await runCapabilityDimension({
57
+ fileOrKey,
58
+ dimension: options.dimension,
59
+ type: options.type,
60
+ field: options.field,
61
+ via,
62
+ actor: options.actor,
63
+ operator: options.operator,
64
+ required: options.required,
65
+ dryRun: Boolean(options.dryRun),
66
+ noBackup: Boolean(options.noBackup),
67
+ overwrite: Boolean(options.overwrite)
68
+ });
69
+
70
+ if (result.dryRun) {
71
+ logger.log(infoLine('Dry run — planned JSON Patch operations:'));
72
+ logger.log('');
73
+ logger.log(JSON.stringify(result.patchOperations, null, 2));
74
+ return;
75
+ }
76
+
77
+ logDimensionValidationOutcome(result);
78
+
79
+ if (result.backupPath) {
80
+ logger.log(headerKeyValue('Backup:', result.backupPath));
81
+ }
82
+ printCapabilitySuccessFooter(result.resolvedPath, result.updatedSections, 'Updated');
83
+ }
84
+
85
+ /**
86
+ * @param {import('commander').Command} cap
87
+ * @returns {void}
88
+ */
89
+ function setupCapabilityDimensionCommand(cap) {
90
+ cap
91
+ .command('dimension <file-or-key>')
92
+ .description('Add or replace one root dimensions binding (local or FK-backed)')
93
+ .requiredOption('--dimension <key>', 'Dimension key (ABAC-facing key; underscores allowed)')
94
+ .requiredOption('--type <type>', 'local | fk')
95
+ .option('--field <name>', 'For type=local: normalized attribute name in metadataSchema.properties')
96
+ .option(
97
+ '--via <fk:dimension>',
98
+ 'For type=fk: hop as <fkName>:<dimensionKey>; repeat for multi-hop traversal',
99
+ (value, prev) => {
100
+ const list = prev || [];
101
+ list.push(value);
102
+ return list;
103
+ },
104
+ []
105
+ )
106
+ .option('--actor <actor>', 'For type=fk: displayName | email | userId | groups | roles')
107
+ .option('--operator <op>', 'For type=fk: eq | in (default depends on actor)')
108
+ .option('--required', 'Set dimensions.<key>.required=true')
109
+ .option('--no-required', 'Set dimensions.<key>.required=false')
110
+ .option('--dry-run', 'Print JSON Patch operations; do not write')
111
+ .option('--overwrite', 'Replace existing dimensions.<key> binding')
112
+ .option('--no-backup', 'Skip backup copy under integration/<app>/backup/')
113
+ .addHelpText('after', CAP_DIMENSION_HELP)
114
+ .action(async(fileOrKey, options) => {
115
+ try {
116
+ await runDimensionAction(fileOrKey, options);
117
+ } catch (error) {
118
+ logger.error(formatBlockingError(`capability dimension failed: ${error.message}`));
119
+ process.exit(1);
120
+ }
121
+ });
122
+ }
123
+
124
+ module.exports = {
125
+ setupCapabilityDimensionCommand,
126
+ runDimensionAction
127
+ };
128
+
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared success footer for datasource capability mutating commands.
3
+ *
4
+ * @fileoverview capability CLI output footer
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const logger = require('../utils/logger');
10
+ const { formatBulletSection, formatNextActions } = require('../utils/cli-test-layout-chalk');
11
+
12
+ /**
13
+ * @param {string} resolvedPath
14
+ * @param {string[]} updatedSections
15
+ * @param {string} [heading='Updated']
16
+ * @returns {void}
17
+ */
18
+ function printCapabilitySuccessFooter(resolvedPath, updatedSections, heading = 'Updated') {
19
+ const display =
20
+ resolvedPath.includes(' ') ? `"${resolvedPath}"` : resolvedPath;
21
+ logger.log('');
22
+ logger.log(formatBulletSection(`${heading}:`, updatedSections));
23
+ logger.log('');
24
+ logger.log(formatNextActions([`aifabrix datasource validate ${display}`]));
25
+ }
26
+
27
+ module.exports = {
28
+ printCapabilitySuccessFooter
29
+ };
@@ -0,0 +1,140 @@
1
+ /**
2
+ * `datasource capability relate` command registration.
3
+ *
4
+ * @fileoverview relate CLI
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const logger = require('../utils/logger');
10
+ const {
11
+ formatBlockingError,
12
+ headerKeyValue,
13
+ infoLine,
14
+ formatSuccessLine,
15
+ colorRollupPrefixedLine
16
+ } = require('../utils/cli-test-layout-chalk');
17
+ const { runCapabilityRelate } = require('../datasource/capability/run-capability-relate');
18
+ const { printCapabilitySuccessFooter } = require('./datasource-capability-output');
19
+
20
+ const CAP_RELATE_HELP = `
21
+ Adds or replaces one **foreignKeys[]** row (metadata-only; no pipeline). Optional **metadataSchema.properties** stub unless **--skip-metadata-property**.
22
+
23
+ $ aifabrix datasource capability relate hubspot-deals --relation-name company --to hubspot-companies --field companyId --target-field externalId
24
+ `;
25
+
26
+ function parseTargetFields(raw) {
27
+ const tf = raw;
28
+ if (Array.isArray(tf) && tf.length > 0) {
29
+ return tf.map((x) => String(x).trim()).filter(Boolean);
30
+ }
31
+ if (tf !== undefined && tf !== null && String(tf).trim()) {
32
+ return [String(tf).trim()];
33
+ }
34
+ return undefined;
35
+ }
36
+
37
+ function logRelateValidationOutcome(result, joinLeft, joinRight) {
38
+ logger.log(formatSuccessLine('Local validation passed'));
39
+ if (result.remoteValidation?.ok) {
40
+ logger.log(formatSuccessLine('Remote validation passed'));
41
+ } else {
42
+ logger.log(colorRollupPrefixedLine('⚠ Remote validation skipped (not authenticated)'));
43
+ }
44
+ if (Array.isArray(result.semanticWarnings) && result.semanticWarnings.length > 0) {
45
+ result.semanticWarnings.forEach((w) => logger.log(colorRollupPrefixedLine(`⚠ ${w}`)));
46
+ }
47
+ logger.log(formatSuccessLine(`Relation created: ${joinLeft} → ${joinRight}`));
48
+ }
49
+
50
+ /**
51
+ * @param {string} fileOrKey
52
+ * @param {object} options - Commander options
53
+ * @returns {Promise<void>}
54
+ */
55
+ async function runRelateAction(fileOrKey, options) {
56
+ const fields = [];
57
+ if (options.field) {
58
+ fields.push(String(options.field).trim());
59
+ }
60
+ const targetFields = parseTargetFields(options.targetField);
61
+
62
+ const result = await runCapabilityRelate({
63
+ fileOrKey,
64
+ relationName: options.relationName,
65
+ targetDatasource: options.to,
66
+ fields,
67
+ targetFields,
68
+ required: options.required,
69
+ description: options.description,
70
+ dryRun: Boolean(options.dryRun),
71
+ noBackup: Boolean(options.noBackup),
72
+ overwrite: Boolean(options.overwrite),
73
+ addMetadataProperty: !options.skipMetadataProperty
74
+ });
75
+
76
+ if (result.dryRun) {
77
+ logger.log(infoLine('Dry run — planned JSON Patch operations:'));
78
+ logger.log('');
79
+ logger.log(JSON.stringify(result.patchOperations, null, 2));
80
+ return;
81
+ }
82
+
83
+ const joinLeft = fields.length === 1 ? fields[0] : 'fields';
84
+ const joinRight =
85
+ Array.isArray(targetFields) && targetFields.length === 1
86
+ ? `${options.to}.${targetFields[0]}`
87
+ : options.to;
88
+ logRelateValidationOutcome(result, joinLeft, joinRight);
89
+
90
+ if (result.backupPath) {
91
+ logger.log(headerKeyValue('Backup:', result.backupPath));
92
+ }
93
+ printCapabilitySuccessFooter(result.resolvedPath, result.updatedSections, 'Updated');
94
+ }
95
+
96
+ /**
97
+ * @param {import('commander').Command} cap
98
+ * @returns {void}
99
+ */
100
+ function setupCapabilityRelateCommand(cap) {
101
+ cap
102
+ .command('relate <file-or-key>')
103
+ .description(
104
+ 'Add or replace foreignKeys[] metadata (+ optional metadataSchema property for relation name)'
105
+ )
106
+ .requiredOption('--relation-name <name>', 'FK name (camelCase; unique per datasource)')
107
+ .requiredOption('--to <targetDatasource>', 'Target datasource key (cross-system JSON key)')
108
+ .requiredOption('--field <name>', 'Local normalized attribute (foreignKeys.fields[])')
109
+ .option('--description <text>', 'FK description (defaults to a generated description)')
110
+ .option('--required', 'Mark FK required (foreignKeys[].required=true)')
111
+ .option('--no-required', 'Mark FK optional (foreignKeys[].required=false)')
112
+ .option(
113
+ '--target-field <name>',
114
+ 'Target join field(s); repeat flag for composite; omit to use runtime default (externalId)',
115
+ (value, prev) => {
116
+ const list = prev || [];
117
+ list.push(value);
118
+ return list;
119
+ },
120
+ []
121
+ )
122
+ .option('--dry-run', 'Print JSON Patch operations; do not write')
123
+ .option('--overwrite', 'Replace existing foreignKeys row with the same name')
124
+ .option('--skip-metadata-property', 'Do not add metadataSchema.properties.<relationName>')
125
+ .option('--no-backup', 'Skip backup copy under integration/<app>/backup/')
126
+ .addHelpText('after', CAP_RELATE_HELP)
127
+ .action(async(fileOrKey, options) => {
128
+ try {
129
+ await runRelateAction(fileOrKey, options);
130
+ } catch (error) {
131
+ logger.error(formatBlockingError(`capability relate failed: ${error.message}`));
132
+ process.exit(1);
133
+ }
134
+ });
135
+ }
136
+
137
+ module.exports = {
138
+ setupCapabilityRelateCommand,
139
+ runRelateAction
140
+ };