@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,7 @@ const containerHelpers = require('../utils/app-run-containers');
15
15
  const { validateAppName } = require('../app/push');
16
16
 
17
17
  const { execWithDockerEnv } = require('../utils/docker-exec');
18
+ const { maskEnvLine } = require('../utils/log-redaction');
18
19
 
19
20
  /** Default number of log lines */
20
21
  const DEFAULT_TAIL_LINES = 100;
@@ -43,36 +44,11 @@ const LEVEL_JSON_NUMERIC_REGEX = /"level"\s*:\s*(\d+)/;
43
44
  /** Fallback: line contains whole-word "error" or "Error" when no other level detected (catches stack traces, "Error: msg", etc.) */
44
45
  const ERROR_WORD_FALLBACK_REGEX = /\berror\b/i;
45
46
 
46
- /** Env key patterns that indicate a secret (mask value) */
47
- const SECRET_KEY_PATTERN = /password|secret|token|credential|api[_-]?key/i;
47
+ /** Keycloak / Java style: validation failure without the word "error" (would be dropped by `-l error` otherwise) */
48
+ const VALIDATION_FAILURE_LINE_REGEX = /\bvalidation\s+failed\b/i;
48
49
 
49
- /** Prefixes to strip before checking key (avoids masking KEYCLOAK_SERVER_URL etc.) */
50
- const KEY_PREFIXES_TO_STRIP = /^KEYCLOAK_|^KEY_VAULT_/;
51
-
52
- /** URL with embedded credentials: scheme://user:password@host → scheme://user:***@host */
53
- const URL_CREDENTIAL_PATTERN = /(\w+:\/\/)([^:@]*):([^@]+)@/g;
54
-
55
- /**
56
- * Masks a single env line if the key looks like a secret or value contains URL credentials
57
- * @param {string} line - Line in form KEY=value
58
- * @returns {string} Same line or KEY=*** or value with masked URL credentials
59
- */
60
- function maskEnvLine(line) {
61
- const eq = line.indexOf('=');
62
- if (eq <= 0) return line;
63
- const key = line.slice(0, eq);
64
- const value = line.slice(eq + 1);
65
-
66
- const keyForCheck = key.replace(KEY_PREFIXES_TO_STRIP, '');
67
- const isSecretKey = SECRET_KEY_PATTERN.test(keyForCheck);
68
-
69
- const maskedValue = value.replace(URL_CREDENTIAL_PATTERN, '$1$2:***@');
70
- const hasUrlCredentials = maskedValue !== value;
71
-
72
- if (isSecretKey) return `${key}=***`;
73
- if (hasUrlCredentials) return `${key}=${maskedValue}`;
74
- return line;
75
- }
50
+ /** Other failure phrases treated as error severity for level filtering */
51
+ const FAILURE_PHRASE_AS_ERROR_REGEX = /\b(fatal|failed to run|unable to (start|run)|exception in thread)\b/i;
76
52
 
77
53
  /** Normalize level string to canonical 'debug'|'info'|'warn'|'error'. */
78
54
  function normalizeLevel(raw) {
@@ -104,9 +80,66 @@ function getLogLevel(line) {
104
80
  const jsonNum = line.match(LEVEL_JSON_NUMERIC_REGEX);
105
81
  if (jsonNum) return numericLevelToName(parseInt(jsonNum[1], 10));
106
82
  if (ERROR_WORD_FALLBACK_REGEX.test(line)) return 'error';
83
+ if (VALIDATION_FAILURE_LINE_REGEX.test(line)) return 'error';
84
+ if (FAILURE_PHRASE_AS_ERROR_REGEX.test(line)) return 'error';
85
+ return null;
86
+ }
87
+
88
+ /** Lines after an `error`-level line often have no level prefix (e.g. Keycloak config validation details). */
89
+ const ERROR_FILTER_CONTEXT_UNCLASSIFIED_MAX = 50;
90
+
91
+ /**
92
+ * Whether to show a log line when `--level` filtering is on. After an emitted error line, includes
93
+ * up to {@link ERROR_FILTER_CONTEXT_UNCLASSIFIED_MAX} following lines with no parseable level so
94
+ * stack traces and multi-line validation output are not dropped.
95
+ *
96
+ * @param {string} line
97
+ * @param {string|null} minLevel - normalized LOG_LEVELS value or null
98
+ * @param {{ pendingAfterError: number }} state - mutable; only used when minLevel is `error`
99
+ * @returns {boolean}
100
+ */
101
+ /**
102
+ * @param {string|null} level
103
+ * @param {string} minLevel
104
+ * @param {{ pendingAfterError: number }} state
105
+ * @returns {boolean|null} true if line shown by sticky rule; null to continue
106
+ */
107
+ function tryStickyErrorContinuation(level, minLevel, state) {
108
+ if (minLevel !== 'error' || state.pendingAfterError <= 0) {
109
+ return null;
110
+ }
111
+ if (level === null || level === undefined) {
112
+ state.pendingAfterError -= 1;
113
+ return true;
114
+ }
115
+ if (level !== 'error') {
116
+ state.pendingAfterError = 0;
117
+ }
107
118
  return null;
108
119
  }
109
120
 
121
+ function shouldShowFilteredLogLine(line, minLevel, state) {
122
+ if (minLevel === null || minLevel === undefined || minLevel === '' || !LOG_LEVELS.includes(minLevel)) {
123
+ return true;
124
+ }
125
+ if (line.trim() === '' && minLevel === 'error' && state.pendingAfterError > 0) {
126
+ return true;
127
+ }
128
+ const level = getLogLevel(line);
129
+ const sticky = tryStickyErrorContinuation(level, minLevel, state);
130
+ if (sticky === true) {
131
+ return true;
132
+ }
133
+
134
+ if (passesLevelFilter(level, minLevel)) {
135
+ if (minLevel === 'error' && level === 'error') {
136
+ state.pendingAfterError = ERROR_FILTER_CONTEXT_UNCLASSIFIED_MAX;
137
+ }
138
+ return true;
139
+ }
140
+ return false;
141
+ }
142
+
110
143
  /**
111
144
  * Whether a line's level passes the minimum level filter (show this level and above).
112
145
  * @param {string|null} lineLevel - Level from getLogLevel (null treated as 'info')
@@ -143,7 +176,12 @@ async function dumpMaskedEnv(containerName) {
143
176
  lines.forEach((line) => logger.log(maskEnvLine(line)));
144
177
  logger.log(chalk.gray('\n--- Logs ---\n'));
145
178
  } catch (err) {
146
- logger.log(chalk.gray('(Could not read container env; container may be stopped)\n'));
179
+ logger.log(
180
+ chalk.gray(
181
+ '(Could not read container env — the container may be stopped, restarting, or not ready yet. ' +
182
+ 'Docker log output below is still shown.)\n'
183
+ )
184
+ );
147
185
  }
148
186
  }
149
187
 
@@ -175,8 +213,9 @@ async function runDockerLogs(containerName, options) {
175
213
  const proc = spawn('docker', args, { stdio: ['inherit', 'pipe', 'pipe'], env: dockerEnv });
176
214
  proc.on('error', reject);
177
215
 
216
+ const filterState = { pendingAfterError: 0 };
178
217
  function onLine(line) {
179
- if (passesLevelFilter(getLogLevel(line), minLevel)) {
218
+ if (shouldShowFilteredLogLine(line, minLevel, filterState)) {
180
219
  process.stdout.write(line + '\n');
181
220
  }
182
221
  }
@@ -241,8 +280,11 @@ async function runDockerLogsFollow(containerName, tail, minLevel) {
241
280
  logger.log(chalk.red(`Error: ${err.message}`));
242
281
  process.exit(1);
243
282
  });
283
+ const filterState = { pendingAfterError: 0 };
244
284
  function onLine(line) {
245
- if (passesLevelFilter(getLogLevel(line), level)) process.stdout.write(line + '\n');
285
+ if (shouldShowFilteredLogLine(line, level, filterState)) {
286
+ process.stdout.write(line + '\n');
287
+ }
246
288
  }
247
289
  const rlOut = readline.createInterface({ input: proc.stdout, crlfDelay: Infinity });
248
290
  rlOut.on('line', onLine);
@@ -301,4 +343,10 @@ async function runAppLogs(appKey, options = {}) {
301
343
  }
302
344
  }
303
345
 
304
- module.exports = { runAppLogs, maskEnvLine, getLogLevel, passesLevelFilter };
346
+ module.exports = {
347
+ runAppLogs,
348
+ maskEnvLine,
349
+ getLogLevel,
350
+ passesLevelFilter,
351
+ shouldShowFilteredLogLine
352
+ };
@@ -8,53 +8,144 @@
8
8
  * @version 2.0.0
9
9
  */
10
10
 
11
+ const chalk = require('chalk');
11
12
  const { formatBlockingError, formatSuccessLine } = require('../utils/cli-test-layout-chalk');
12
13
  const {
13
14
  setControllerUrl,
14
15
  setCurrentEnvironment,
15
- getControllerUrl
16
+ getControllerUrl,
17
+ getRegisteredControllerUrls,
18
+ getConfig
16
19
  } = require('../core/config');
17
20
  const {
18
21
  validateControllerUrl,
19
22
  validateEnvironment,
20
23
  checkUserLoggedIn
21
24
  } = require('../utils/auth-config-validator');
22
- const { getControllerUrlFromLoggedInUser } = require('../utils/controller-url');
25
+ const { hasStoredDeviceTokenForController } = require('../utils/controller-url');
23
26
  const logger = require('../utils/logger');
24
27
 
28
+ /**
29
+ * True when user ran --set-controller with no URL (pick from config.yaml).
30
+ * @param {unknown} setController - Commander option value
31
+ * @returns {boolean}
32
+ */
33
+ function isInteractiveControllerPick(setController) {
34
+ return (
35
+ setController === true ||
36
+ (typeof setController === 'string' && setController.trim() === '')
37
+ );
38
+ }
39
+
40
+ /**
41
+ * True when a non-empty controller URL string was passed.
42
+ * @param {unknown} setController - Commander option value
43
+ * @returns {boolean}
44
+ */
45
+ function hasExplicitControllerUrl(setController) {
46
+ return typeof setController === 'string' && setController.trim() !== '';
47
+ }
48
+
49
+ /**
50
+ * Normalize controller URL for equality checks (trailing slashes).
51
+ * @param {string|null|undefined} url - URL or empty
52
+ * @returns {string}
53
+ */
54
+ function normalizeForCompare(url) {
55
+ if (!url || typeof url !== 'string') return '';
56
+ return url.trim().replace(/\/+$/, '');
57
+ }
58
+
59
+ function throwNoRegisteredControllers() {
60
+ const msg =
61
+ 'No controllers are registered in config. Run "aifabrix login" first, or set a controller with "aifabrix auth --set-controller <url>".';
62
+ logger.error(formatBlockingError(msg));
63
+ throw new Error(msg);
64
+ }
65
+
66
+ function throwNonInteractiveControllerPick() {
67
+ const msg =
68
+ 'Cannot choose a controller without a URL in non-interactive mode. Run: aifabrix auth --set-controller <url>';
69
+ logger.error(formatBlockingError(msg));
70
+ throw new Error(msg);
71
+ }
72
+
73
+ /**
74
+ * Pick default controller from URLs in config (`controller` + `device` keys), or set the only one.
75
+ * @async
76
+ * @returns {Promise<void>}
77
+ */
78
+ async function handleSelectRegisteredController() {
79
+ const urls = await getRegisteredControllerUrls();
80
+
81
+ if (urls.length === 0) {
82
+ throwNoRegisteredControllers();
83
+ }
84
+
85
+ if (!process.stdin.isTTY) {
86
+ throwNonInteractiveControllerPick();
87
+ }
88
+
89
+ if (urls.length === 1) {
90
+ const sole = urls[0];
91
+ const current = await getControllerUrl();
92
+ if (normalizeForCompare(current) === normalizeForCompare(sole)) {
93
+ logger.log(formatSuccessLine(`Default controller is already set to ${sole}.`));
94
+ logger.log(
95
+ chalk.white('To add another controller, run: aifabrix auth --set-controller <url>')
96
+ );
97
+ return;
98
+ }
99
+ await handleSetController(sole);
100
+ return;
101
+ }
102
+
103
+ const inquirer = require('inquirer');
104
+ const { controllerUrl } = await inquirer.prompt([
105
+ {
106
+ type: 'list',
107
+ name: 'controllerUrl',
108
+ message: 'Select default controller:',
109
+ choices: urls
110
+ }
111
+ ]);
112
+ await handleSetController(controllerUrl);
113
+ }
114
+
25
115
  /**
26
116
  * Handle set-controller command
27
- * Allows setting the default controller when no credentials are stored, or when already logged in to that controller.
28
- * If credentials exist for a different controller, throws with a clear message.
117
+ * Allows setting the default controller when there are no device tokens, or when a token exists
118
+ * for the target URL (including switching among multiple logged-in controllers). If device tokens
119
+ * exist only for other controller URLs, throws with a clear message.
29
120
  *
30
121
  * @async
31
122
  * @function handleSetController
32
123
  * @param {string} url - Controller URL to set
33
124
  * @returns {Promise<void>}
34
- * @throws {Error} If validation fails or credentials exist for another controller
125
+ * @throws {Error} If validation fails or credentials exist only for other controllers
35
126
  */
36
127
  async function handleSetController(url) {
37
128
  try {
38
129
  validateControllerUrl(url);
39
130
  const normalizedUrl = url.trim().replace(/\/+$/, '');
40
131
 
41
- const loggedInControllerUrl = await getControllerUrlFromLoggedInUser();
42
- if (!loggedInControllerUrl) {
43
- // No stored credentials: allow setting controller so "aifabrix login" opens the right place
44
- await setControllerUrl(url);
45
- logger.log(formatSuccessLine(`Controller URL set to: ${url}`));
46
- return;
47
- }
132
+ const userConfig = await getConfig();
133
+ const device =
134
+ userConfig.device && typeof userConfig.device === 'object' ? userConfig.device : {};
135
+ const deviceKeys = Object.keys(device);
136
+ const hasTokenForTarget = await hasStoredDeviceTokenForController(normalizedUrl);
48
137
 
49
- const normalizedLoggedIn = loggedInControllerUrl.trim().replace(/\/+$/, '');
50
- if (normalizedLoggedIn === normalizedUrl) {
138
+ if (deviceKeys.length === 0 || hasTokenForTarget) {
51
139
  await setControllerUrl(url);
52
140
  logger.log(formatSuccessLine(`Controller URL set to: ${url}`));
53
141
  return;
54
142
  }
55
143
 
144
+ const otherKey =
145
+ deviceKeys.find((k) => normalizeForCompare(k) !== normalizeForCompare(normalizedUrl)) ||
146
+ deviceKeys[0];
56
147
  throw new Error(
57
- `You have credentials for another controller (${loggedInControllerUrl}).\n` +
148
+ `You have credentials for another controller (${otherKey.trim().replace(/\/+$/, '')}).\n` +
58
149
  'To use a different controller either run "aifabrix login" with that controller, or run "aifabrix logout" first to clear credentials, then set the new controller with "aifabrix auth --set-controller <url>".'
59
150
  );
60
151
  } catch (error) {
@@ -107,20 +198,27 @@ async function handleSetEnvironment(environment) {
107
198
  * @async
108
199
  * @function handleAuthConfig
109
200
  * @param {Object} options - Command options
110
- * @param {string} [options.setController] - Controller URL to set
201
+ * @param {string|boolean} [options.setController] - Controller URL, or true when flag has no value
111
202
  * @param {string} [options.setEnvironment] - Environment to set
112
203
  * @returns {Promise<void>}
113
204
  * @throws {Error} If command fails
114
205
  */
115
206
  async function handleAuthConfig(options) {
116
- if (!options.setController && !options.setEnvironment) {
207
+ const pick = isInteractiveControllerPick(options.setController);
208
+ const hasUrl = hasExplicitControllerUrl(options.setController);
209
+
210
+ if (!pick && !hasUrl && !options.setEnvironment) {
117
211
  throw new Error(
118
212
  'No action specified. Use "aifabrix auth --set-controller <url>" or "aifabrix auth --set-environment <env>".'
119
213
  );
120
214
  }
121
- if (options.setController) {
215
+
216
+ if (pick) {
217
+ await handleSelectRegisteredController();
218
+ } else if (hasUrl) {
122
219
  await handleSetController(options.setController);
123
220
  }
221
+
124
222
  if (options.setEnvironment) {
125
223
  await handleSetEnvironment(options.setEnvironment);
126
224
  }
@@ -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
+ };