@aifabrix/builder 2.45.0 → 2.45.5

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 (180) hide show
  1. package/.cursor/rules/docs-rules.mdc +3 -1
  2. package/integration/hubspot-test/test-artifacts/wizard-hubspot-credential-real.yaml +1 -1
  3. package/integration/hubspot-test/test-artifacts/wizard-hubspot-env-vars.yaml +1 -1
  4. package/integration/hubspot-test/wizard-hubspot-e2e.yaml +1 -1
  5. package/jest.projects.js +44 -1
  6. package/lib/api/agent-metadata-validation.api.js +93 -0
  7. package/lib/api/audit-api-helpers.js +68 -0
  8. package/lib/api/audit.api.js +119 -0
  9. package/lib/api/auth-cache.api.js +44 -0
  10. package/lib/api/certificates.api.js +4 -4
  11. package/lib/api/credential.api.js +2 -2
  12. package/lib/api/dataplane-health.api.js +137 -0
  13. package/lib/api/dataplane-sync.api.js +27 -0
  14. package/lib/api/datasources-core.api.js +21 -18
  15. package/lib/api/datasources-extended.api.js +22 -22
  16. package/lib/api/dimensions.api.js +3 -0
  17. package/lib/api/external-systems.api.js +13 -13
  18. package/lib/api/governance-scenarios.api.js +39 -0
  19. package/lib/api/groups.api.js +79 -0
  20. package/lib/api/index.js +45 -2
  21. package/lib/api/pipeline.api.js +4 -4
  22. package/lib/api/protection.api.js +157 -0
  23. package/lib/api/records-bulk.api.js +30 -0
  24. package/lib/api/records-search-parse.js +49 -0
  25. package/lib/api/records-search.api.js +29 -0
  26. package/lib/api/types/agent-metadata-validation.types.js +67 -0
  27. package/lib/api/types/audit.types.js +72 -0
  28. package/lib/api/types/dataplane-health.types.js +27 -0
  29. package/lib/api/types/dimensions.types.js +2 -0
  30. package/lib/api/types/governance-scenarios.types.js +39 -0
  31. package/lib/api/types/protection.types.js +69 -0
  32. package/lib/api/types/records-bulk.types.js +35 -0
  33. package/lib/api/types/records-search.types.js +28 -0
  34. package/lib/api/user-groups.api.js +50 -0
  35. package/lib/api/users.api.js +95 -0
  36. package/lib/api/validation-run.api.js +3 -3
  37. package/lib/api/validation-runner.js +3 -3
  38. package/lib/api/wizard-platform.api.js +3 -3
  39. package/lib/api/wizard.api.js +21 -21
  40. package/lib/app/deploy.js +34 -19
  41. package/lib/cli/index.js +4 -2
  42. package/lib/cli/infra-guided.js +22 -3
  43. package/lib/cli/setup-app.help.js +50 -2
  44. package/lib/cli/setup-app.js +33 -24
  45. package/lib/cli/setup-app.test-commands.js +80 -21
  46. package/lib/cli/setup-auth.js +30 -3
  47. package/lib/cli/setup-external-system.js +37 -3
  48. package/lib/cli/setup-utility-help-after.js +45 -0
  49. package/lib/cli/setup-utility-resolve.js +2 -2
  50. package/lib/cli/setup-utility.js +99 -81
  51. package/lib/commands/auth-status-dataplane-version.js +231 -0
  52. package/lib/commands/auth-status-display.js +94 -0
  53. package/lib/commands/auth-status.js +70 -6
  54. package/lib/commands/credential-push.js +10 -5
  55. package/lib/commands/datasource-load-export-cli.js +178 -0
  56. package/lib/commands/datasource-test-trust-cli.js +114 -0
  57. package/lib/commands/datasource-unified-test-cli.js +25 -17
  58. package/lib/commands/datasource-unified-test-cli.options.js +18 -3
  59. package/lib/commands/datasource-unified-test-e2e-cli-helpers.js +5 -5
  60. package/lib/commands/datasource-verify-audit-cli.js +96 -0
  61. package/lib/commands/datasource.js +68 -1
  62. package/lib/commands/dimension-value.js +43 -13
  63. package/lib/commands/dimension.js +38 -7
  64. package/lib/commands/identity-apply-sync.js +131 -0
  65. package/lib/commands/identity-group-cli.js +146 -0
  66. package/lib/commands/identity-membership-role-cache.js +175 -0
  67. package/lib/commands/identity-shared.js +94 -0
  68. package/lib/commands/identity-user-cli.js +158 -0
  69. package/lib/commands/identity.js +43 -0
  70. package/lib/commands/login.js +46 -16
  71. package/lib/commands/protection-cli-leaves.js +286 -0
  72. package/lib/commands/protection.js +50 -0
  73. package/lib/commands/repair.js +13 -3
  74. package/lib/commands/test-e2e-external.js +40 -9
  75. package/lib/commands/test-governance-cli-options.js +47 -0
  76. package/lib/commands/test-governance-command-action.js +55 -0
  77. package/lib/commands/test-governance-external.js +88 -0
  78. package/lib/commands/test-trust-cli-options.js +32 -0
  79. package/lib/commands/test-trust-command-action.js +118 -0
  80. package/lib/commands/test-trust-external.js +127 -0
  81. package/lib/commands/upload.js +34 -13
  82. package/lib/commands/wizard-core.js +3 -0
  83. package/lib/core/config-device-dataplane.js +185 -0
  84. package/lib/datasource/agent-trust-debug-log.js +45 -0
  85. package/lib/datasource/agent-trust-fetch.js +115 -0
  86. package/lib/datasource/agent-trust-map.js +101 -0
  87. package/lib/datasource/agent-trust-mock.js +44 -0
  88. package/lib/datasource/agent-trust-run.js +93 -0
  89. package/lib/datasource/agent-trust-timeout.js +25 -0
  90. package/lib/datasource/audit-evidence-display.js +80 -0
  91. package/lib/datasource/audit-evidence-extract-debug.js +103 -0
  92. package/lib/datasource/audit-evidence-extract.js +177 -0
  93. package/lib/datasource/audit-evidence-matrix-rows.js +158 -0
  94. package/lib/datasource/audit-evidence-operation-aliases.js +63 -0
  95. package/lib/datasource/audit-evidence-poll.js +109 -0
  96. package/lib/datasource/audit-evidence-run.js +177 -0
  97. package/lib/datasource/audit-evidence-verify-helpers.js +167 -0
  98. package/lib/datasource/audit-evidence-verify-matrix.js +177 -0
  99. package/lib/datasource/audit-evidence-verify.js +108 -0
  100. package/lib/datasource/bulk-loader-run-batches.js +88 -0
  101. package/lib/datasource/bulk-loader-service-retry.js +52 -0
  102. package/lib/datasource/bulk-loader-service.js +127 -0
  103. package/lib/datasource/capability/run-capability-dimension.js +2 -1
  104. package/lib/datasource/datasource-exporter-resolve.js +82 -0
  105. package/lib/datasource/datasource-exporter-service.js +144 -0
  106. package/lib/datasource/datasource-identifier.js +148 -0
  107. package/lib/datasource/datasource-load-export-context.js +92 -0
  108. package/lib/datasource/integration-context.js +6 -4
  109. package/lib/datasource/local-data-paths.js +145 -0
  110. package/lib/datasource/log-cleaner.js +241 -0
  111. package/lib/datasource/log-viewer.js +111 -6
  112. package/lib/datasource/record-file-parser.js +85 -0
  113. package/lib/datasource/record-mapper.js +129 -0
  114. package/lib/datasource/resolve-app.js +74 -55
  115. package/lib/datasource/test-e2e.js +26 -2
  116. package/lib/datasource/unified-validation-run-resolve.js +45 -12
  117. package/lib/datasource/unified-validation-run.js +43 -17
  118. package/lib/datasource/validate.js +37 -20
  119. package/lib/external-system/test-system-level.js +25 -4
  120. package/lib/external-system/test.js +3 -1
  121. package/lib/governance/governance-pack-loader.js +70 -0
  122. package/lib/governance/governance-run-display.js +119 -0
  123. package/lib/identity/identity-apply-core.js +108 -0
  124. package/lib/identity/identity-apply-phases.js +244 -0
  125. package/lib/identity/identity-apply-service.js +77 -0
  126. package/lib/identity/identity-csv-parser.js +114 -0
  127. package/lib/internal/fs-real-sync.js +29 -1
  128. package/lib/protection/auth-context.js +45 -0
  129. package/lib/protection/convert-batch.js +90 -0
  130. package/lib/protection/load.js +41 -0
  131. package/lib/protection/paths.js +158 -0
  132. package/lib/protection/preflight-datasource-ready.js +46 -0
  133. package/lib/protection/protection-create-probe-lines.js +142 -0
  134. package/lib/protection/protection-create-probes.js +95 -0
  135. package/lib/protection/protection-create-scaffold.js +281 -0
  136. package/lib/protection/protection-display-helpers.js +445 -0
  137. package/lib/protection/protection-display.js +215 -0
  138. package/lib/protection/protection-preset-registry.js +97 -0
  139. package/lib/protection/report-exit.js +62 -0
  140. package/lib/protection/resolve.js +202 -0
  141. package/lib/protection/run-commands-validate.js +130 -0
  142. package/lib/protection/run-commands.js +216 -0
  143. package/lib/protection/run-protection-create-helpers.js +134 -0
  144. package/lib/protection/run-protection-create-probes-flow.js +43 -0
  145. package/lib/protection/run-protection-create.js +191 -0
  146. package/lib/protection/scope.js +22 -0
  147. package/lib/protection/sync-after-upload.js +64 -0
  148. package/lib/protection/upload-batch.js +127 -0
  149. package/lib/protection/validate-batch.js +113 -0
  150. package/lib/protection/validate-local.js +65 -0
  151. package/lib/resolvers/dimension-file.js +29 -4
  152. package/lib/schema/external-datasource.schema.json +47 -13
  153. package/lib/schema/flag-map-validation-run.json +2 -2
  154. package/lib/schema/protection.schema.json +374 -0
  155. package/lib/schema/type/credential-field-registry.json +636 -0
  156. package/lib/schema/wizard-config.schema.json +1 -1
  157. package/lib/utils/agent-trust-run-display.js +231 -0
  158. package/lib/utils/agent-trust-run-exit.js +59 -0
  159. package/lib/utils/agent-trust-wait-ui.js +94 -0
  160. package/lib/utils/cli-exit-codes.js +30 -0
  161. package/lib/utils/cli-sync-options.js +33 -0
  162. package/lib/utils/credential-push-ui.js +90 -0
  163. package/lib/utils/dataplane-cli-version-gate.js +202 -0
  164. package/lib/utils/dataplane-cli-version-help.js +74 -0
  165. package/lib/utils/datasource-export-display.js +137 -0
  166. package/lib/utils/datasource-load-display.js +174 -0
  167. package/lib/utils/datasource-load-export-ui.js +120 -0
  168. package/lib/utils/datasource-test-run-capability-scope.js +3 -3
  169. package/lib/utils/datasource-test-run-display.js +3 -3
  170. package/lib/utils/datasource-test-run-tty-log.js +5 -1
  171. package/lib/utils/help-builder.js +30 -24
  172. package/lib/utils/manifest-source-emit.js +22 -0
  173. package/lib/utils/semver-compare.js +127 -0
  174. package/lib/utils/upload-pipeline-ui.js +91 -0
  175. package/lib/utils/validation-poll-ui.js +14 -7
  176. package/lib/validation/wizard-config-validator.js +58 -19
  177. package/package.json +1 -1
  178. package/templates/applications/dataplane/env.template +13 -1
  179. package/templates/applications/dataplane/rbac.yaml +4 -0
  180. package/templates/external-system/README.md.hbs +56 -8
@@ -15,40 +15,7 @@ const logger = require('../utils/logger');
15
15
  const { handleCommandError, logOfflinePathWhenType } = require('../utils/cli-utils');
16
16
  const { detectAppType, getDeployJsonPath, getResolveAppPath } = require('../utils/paths');
17
17
  const { setupResolveCommand } = require('./setup-utility-resolve');
18
-
19
- const JSON_HELP_AFTER = `
20
- Example:
21
- $ aifabrix json myapp
22
- Generates *-deploy.json (or application-schema.json) for commit before deploy.
23
- `;
24
-
25
- const VALIDATE_HELP_AFTER = `
26
- Examples:
27
- $ aifabrix validate myapp
28
- $ aifabrix validate myapp --cert-sync
29
- $ aifabrix validate --integration
30
- $ aifabrix validate --builder
31
- `;
32
-
33
- const REPAIR_HELP_AFTER = `
34
- Examples:
35
- $ aifabrix repair hubspot-demo
36
- Align manifest, system/datasource files, RBAC extract, env.template, and deploy JSON under integration/<systemKey>/.
37
-
38
- $ aifabrix repair hubspot-demo --dry-run
39
- Show what would change without writing files.
40
-
41
- $ aifabrix repair hubspot-demo --auth oauth2
42
- Set authentication method; updates the system file and env.template (oauth2, aad, apikey, basic, …).
43
-
44
- $ aifabrix repair hubspot-demo --rbac --expose --sync --test
45
- Optional datasource fixes: RBAC roles/permissions, exposed.schema from attributes, default sync block, testPayload stubs.
46
-
47
- $ aifabrix repair hubspot-demo --doc
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.
51
- `;
18
+ const { JSON_HELP_AFTER, VALIDATE_HELP_AFTER, REPAIR_HELP_AFTER } = require('./setup-utility-help-after');
52
19
 
53
20
  /**
54
21
  * Resolve app path and type for split-json (integration first, then builder).
@@ -122,7 +89,7 @@ function logSplitJsonResult(result) {
122
89
  }
123
90
 
124
91
  function setupJsonCommand(program) {
125
- program.command('json <app>')
92
+ program.command('json <appKey|systemKey>')
126
93
  .description('Write deployment JSON to disk for version control')
127
94
  .addHelpText('after', JSON_HELP_AFTER)
128
95
  .action(async(appName, options) => {
@@ -156,7 +123,7 @@ function setupJsonCommand(program) {
156
123
  }
157
124
 
158
125
  function setupSplitJsonCommand(program) {
159
- program.command('split-json <app>')
126
+ program.command('split-json <appKey|systemKey>')
160
127
  .description('Split deploy JSON into env.template, application.yaml, rbac, README, …')
161
128
  .option('-o, --output <dir>', 'Output directory for component files (defaults to same directory as JSON)')
162
129
  .action(async(appName, options) => {
@@ -263,32 +230,54 @@ function setupRepairCommand(program) {
263
230
  });
264
231
  }
265
232
 
233
+ async function resolveConvertTargetFormat(options) {
234
+ const config = require('../core/config');
235
+ const effectiveFormat = options.format || (await config.getFormat());
236
+ if (!effectiveFormat) {
237
+ throw new Error(
238
+ 'Option --format is required and must be \'json\' or \'yaml\' (or set default with aifabrix dev set-format)'
239
+ );
240
+ }
241
+ const normalized = effectiveFormat.trim().toLowerCase();
242
+ if (normalized !== 'json' && normalized !== 'yaml') {
243
+ throw new Error('Option --format must be \'json\' or \'yaml\'');
244
+ }
245
+ return normalized;
246
+ }
247
+
248
+ function logConvertCommandResults(converted, deleted) {
249
+ logger.log(formatSuccessParagraph('Convert complete.'));
250
+ converted.forEach((p) => logger.log(` • ${p}`));
251
+ if (deleted.length > 0) {
252
+ logger.log(chalk.gray(' Removed old files:'));
253
+ deleted.forEach((p) => logger.log(chalk.gray(` ${p}`)));
254
+ }
255
+ }
256
+
257
+ async function runConvertCommandForApp(appName, options) {
258
+ const normalized = await resolveConvertTargetFormat(options);
259
+ const { isProtectionScope } = require('../protection/scope');
260
+ if (isProtectionScope(appName)) {
261
+ const { runConvertProtectionBatch } = require('../protection/convert-batch');
262
+ const { converted, deleted } = await runConvertProtectionBatch(normalized, {
263
+ force: options.force
264
+ });
265
+ logConvertCommandResults(converted, deleted);
266
+ return;
267
+ }
268
+ const { runConvert } = require('../commands/convert');
269
+ const { converted, deleted } = await runConvert(appName, { format: normalized, force: options.force });
270
+ logConvertCommandResults(converted, deleted);
271
+ }
272
+
266
273
  function setupConvertCommand(program) {
267
- program.command('convert <app>')
268
- .description('Convert integration config files between JSON and YAML')
274
+ program.command('convert <appKey|systemKey>')
275
+ .description('Convert config files between JSON and YAML (builder/<appKey>/ or integration/<systemKey>/)')
269
276
  .option('--format <format>', 'Target format: json | yaml (required unless config format is set)')
270
277
  .option('-f, --force', 'Skip confirmation prompt')
271
278
  .action(async(appName, options) => {
272
279
  try {
273
- const config = require('../core/config');
274
- const effectiveFormat = options.format || (await config.getFormat());
275
- if (!effectiveFormat) {
276
- throw new Error(
277
- 'Option --format is required and must be \'json\' or \'yaml\' (or set default with aifabrix dev set-format)'
278
- );
279
- }
280
- const normalized = effectiveFormat.trim().toLowerCase();
281
- if (normalized !== 'json' && normalized !== 'yaml') {
282
- throw new Error('Option --format must be \'json\' or \'yaml\'');
283
- }
284
- const { runConvert } = require('../commands/convert');
285
- const { converted, deleted } = await runConvert(appName, { format: normalized, force: options.force });
286
- logger.log(formatSuccessParagraph('Convert complete.'));
287
- converted.forEach(p => logger.log(` • ${p}`));
288
- if (deleted.length > 0) {
289
- logger.log(chalk.gray(' Removed old files:'));
290
- deleted.forEach(p => logger.log(chalk.gray(` ${p}`)));
291
- }
280
+ await runConvertCommandForApp(appName, options);
292
281
  } catch (error) {
293
282
  handleCommandError(error, 'convert');
294
283
  process.exit(1);
@@ -297,8 +286,8 @@ function setupConvertCommand(program) {
297
286
  }
298
287
 
299
288
  function setupShowCommand(program) {
300
- program.command('show <app>')
301
- .description('Show app from local tree (default) or controller (--online)')
289
+ program.command('show <appKey|systemKey>')
290
+ .description('Show builder app or external system from local tree (default) or controller (--online)')
302
291
  .option('--online', 'Fetch application data from the controller')
303
292
  .option('--json', 'Output as JSON')
304
293
  .option(
@@ -368,51 +357,80 @@ async function runValidateBatchBranch(validate, opts) {
368
357
  return true;
369
358
  }
370
359
 
360
+ async function runProtectionValidateBranch(appOrFile, opts) {
361
+ const { validateProtectionBatchCommand } = require('../protection/validate-batch');
362
+ const code = await validateProtectionBatchCommand(
363
+ {
364
+ env: opts.env,
365
+ json: (opts.format || '').toLowerCase() === 'json',
366
+ warningsAsErrors: opts.warningsAsErrors === true,
367
+ simulate: opts.simulate === true,
368
+ verbose: opts.verbose === true
369
+ },
370
+ logger
371
+ );
372
+ if (code !== 0) {
373
+ process.exit(code);
374
+ }
375
+ }
376
+
377
+ function buildValidateJsonPayload(result, appOrFile) {
378
+ let payload = result;
379
+ try {
380
+ if (result?.appPath && appOrFile && typeof appOrFile === 'string') {
381
+ if (!(fs.existsSync(appOrFile) && fs.statSync(appOrFile).isFile())) {
382
+ const { getManifestSourcePayload } = require('../utils/manifest-source-emit');
383
+ payload = { ...result, manifestSource: getManifestSourcePayload(appOrFile, result.appPath) };
384
+ }
385
+ }
386
+ } catch {
387
+ /* keep plain result */
388
+ }
389
+ return payload;
390
+ }
391
+
392
+ function displaySingleValidateResult(appOrFile, result, outFormat, validate) {
393
+ emitManifestAfterValidateIfApplicable(appOrFile, result, outFormat);
394
+ if (outFormat === 'json') {
395
+ logger.log(JSON.stringify(buildValidateJsonPayload(result, appOrFile), null, 2));
396
+ return;
397
+ }
398
+ validate.displayValidationResults(result);
399
+ }
400
+
371
401
  async function runValidateCommand(appOrFile, options) {
372
402
  const validate = require('../validation/validate');
373
403
  const opts = options.opts ? options.opts() : options;
404
+ const { isProtectionScope } = require('../protection/scope');
405
+ if (isProtectionScope(appOrFile)) {
406
+ await runProtectionValidateBranch(appOrFile, opts);
407
+ return;
408
+ }
374
409
  if (await runValidateBatchBranch(validate, opts)) {
375
410
  return;
376
411
  }
377
-
378
412
  const outFormat = (opts.format || 'default').toLowerCase();
379
-
380
413
  if (!appOrFile || typeof appOrFile !== 'string') {
381
414
  logger.log(chalk.red('App name or file path is required, or use --integration / --builder'));
382
415
  process.exit(1);
383
416
  }
384
-
385
417
  const result = await validate.validateAppOrFile(appOrFile, {
386
418
  ...opts,
387
419
  certSync: opts.certSync === true
388
420
  });
389
- emitManifestAfterValidateIfApplicable(appOrFile, result, outFormat);
390
- if (outFormat === 'json') {
391
- let payload = result;
392
- try {
393
- if (result?.appPath && appOrFile && typeof appOrFile === 'string') {
394
- if (!(fs.existsSync(appOrFile) && fs.statSync(appOrFile).isFile())) {
395
- const { getManifestSourcePayload } = require('../utils/manifest-source-emit');
396
- payload = { ...result, manifestSource: getManifestSourcePayload(appOrFile, result.appPath) };
397
- }
398
- }
399
- } catch {
400
- /* keep plain result */
401
- }
402
- logger.log(JSON.stringify(payload, null, 2));
403
- } else {
404
- validate.displayValidationResults(result);
405
- }
421
+ displaySingleValidateResult(appOrFile, result, outFormat, validate);
406
422
  if (!result.valid) process.exit(1);
407
423
  }
408
424
 
409
425
  function setupValidateDiffCommands(program) {
410
- program.command('validate [appOrFile]')
411
- .description('Validate one app/file or all under integration/ or builder/')
426
+ program.command('validate [appKey|systemKey|file]')
427
+ .description('Validate appKey, systemKey, file path, .protection batch, or all under integration/ and builder/')
412
428
  .addHelpText('after', VALIDATE_HELP_AFTER)
413
429
  .option('--format <format>', 'Output format: json | default (human-readable)')
414
430
  .option('--integration', 'Validate all applications under integration/')
415
431
  .option('--builder', 'Validate all applications under builder/')
432
+ .option('--warnings-as-errors', 'For .protection: pass strict=true to dataplane validate')
433
+ .option('--simulate', 'For .protection: run simulate after validate')
416
434
  .option(
417
435
  '--cert-sync',
418
436
  'After successful validation of an external integration, refresh the system file certification block from the dataplane (requires login)'
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Auth-status dataplane version refresh + compatibility resolution (plan 142.0).
3
+ *
4
+ * Extracted from `auth-status.js` to keep that file under the 500-line limit
5
+ * (`.cursor/rules/project-rules.mdc → File and Function Size Limits`). The
6
+ * helpers here are pure-ish (only depend on the dataplane health API and the
7
+ * `device.<controllerUrl>` cache module) so they are easy to mock in unit
8
+ * tests.
9
+ *
10
+ * @fileoverview Resolve dataplane version + Builder CLI compatibility for auth status
11
+ * @author AI Fabrix Team
12
+ * @version 2.0.0
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ const { fetchDataplaneGeneralHealth } = require('../api/dataplane-health.api');
18
+ const {
19
+ updateDeviceDataplaneVersions,
20
+ getDeviceDataplaneVersions
21
+ } = require('../core/config-device-dataplane');
22
+ const {
23
+ getInstalledCliVersion
24
+ } = require('../utils/dataplane-cli-version-gate');
25
+ const { isAtLeast, isValidSemver } = require('../utils/semver-compare');
26
+
27
+ /**
28
+ * Per plan 142.0: invalid semver from the dataplane is logged but does **not**
29
+ * block the CLI; missing min version means no enforcement.
30
+ *
31
+ * @param {string} cliVersion
32
+ * @param {string|undefined} minBuilderCliVersion
33
+ * @returns {boolean}
34
+ */
35
+ function computeCompatibility(cliVersion, minBuilderCliVersion) {
36
+ if (!minBuilderCliVersion) return true;
37
+ if (!isValidSemver(minBuilderCliVersion) || !isValidSemver(cliVersion)) {
38
+ return true;
39
+ }
40
+ return isAtLeast(cliVersion, minBuilderCliVersion);
41
+ }
42
+
43
+ /**
44
+ * Read `device.<controllerUrl>` cache without throwing.
45
+ * @private
46
+ * @async
47
+ * @param {string} controllerUrl
48
+ * @returns {Promise<Object|null>}
49
+ */
50
+ async function safeReadCache(controllerUrl) {
51
+ if (!controllerUrl) return null;
52
+ try {
53
+ return await getDeviceDataplaneVersions(controllerUrl);
54
+ } catch (_error) {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Refresh dataplane health, persist `device.<controllerUrl>` version cache,
61
+ * and derive the compatibility verdict against the installed Builder CLI.
62
+ * Falls back to the cached entry when the live probe fails so `auth status`
63
+ * still shows the last-known values offline.
64
+ *
65
+ * @async
66
+ * @param {string} controllerUrl - Controller URL (cache key)
67
+ * @param {string} dataplaneUrl - Dataplane base URL for the health probe
68
+ * @returns {Promise<{
69
+ * dataplaneVersion: string|undefined,
70
+ * minBuilderCliVersion: string|undefined,
71
+ * cliVersion: string,
72
+ * compatible: boolean
73
+ * }>}
74
+ */
75
+ async function refreshDataplaneVersionInfo(controllerUrl, dataplaneUrl) {
76
+ const cliVersion = getInstalledCliVersion();
77
+ let dataplaneVersion;
78
+ let minBuilderCliVersion;
79
+
80
+ try {
81
+ const snapshot = await fetchDataplaneGeneralHealth(dataplaneUrl);
82
+ if (snapshot) {
83
+ dataplaneVersion = snapshot.version;
84
+ minBuilderCliVersion = snapshot.minBuilderCliVersion;
85
+ try {
86
+ await updateDeviceDataplaneVersions(controllerUrl, {
87
+ version: dataplaneVersion,
88
+ minBuilderCliVersion: minBuilderCliVersion || null
89
+ });
90
+ } catch (_persistError) {
91
+ // cache write failure must not break the display
92
+ }
93
+ }
94
+ } catch (_fetchError) {
95
+ // ignore — fall through to cached values
96
+ }
97
+
98
+ if (!dataplaneVersion && !minBuilderCliVersion) {
99
+ const cached = await safeReadCache(controllerUrl);
100
+ if (cached) {
101
+ dataplaneVersion = cached.version;
102
+ minBuilderCliVersion = cached.minBuilderCliVersion;
103
+ }
104
+ }
105
+
106
+ const compatible = computeCompatibility(cliVersion, minBuilderCliVersion);
107
+ return { dataplaneVersion, minBuilderCliVersion, cliVersion, compatible };
108
+ }
109
+
110
+ /**
111
+ * Load cached versions only (no live probe). Used when the dataplane is
112
+ * unreachable so `auth status` can still surface a known compatibility floor.
113
+ *
114
+ * @async
115
+ * @param {string} controllerUrl
116
+ * @returns {Promise<{
117
+ * dataplaneVersion: string|undefined,
118
+ * minBuilderCliVersion: string|undefined,
119
+ * cliVersion: string,
120
+ * compatible: boolean
121
+ * }>}
122
+ */
123
+ async function loadCachedVersionInfo(controllerUrl) {
124
+ const cliVersion = getInstalledCliVersion();
125
+ const cached = await safeReadCache(controllerUrl);
126
+ const dataplaneVersion = cached ? cached.version : undefined;
127
+ const minBuilderCliVersion = cached ? cached.minBuilderCliVersion : undefined;
128
+ return {
129
+ dataplaneVersion,
130
+ minBuilderCliVersion,
131
+ cliVersion,
132
+ compatible: computeCompatibility(cliVersion, minBuilderCliVersion)
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Resolve bearer auth after login (device token preferred, else client app token).
138
+ * @private
139
+ * @async
140
+ * @param {string} controllerUrl
141
+ * @param {string} environment
142
+ * @param {Object} loginOptions
143
+ * @returns {Promise<Object|null>}
144
+ */
145
+ async function resolveAuthConfigAfterLogin(controllerUrl, environment, loginOptions) {
146
+ const { getOrRefreshDeviceToken, getDeploymentAuth } = require('../utils/token-manager');
147
+ const device = await getOrRefreshDeviceToken(controllerUrl);
148
+ if (device && device.token) {
149
+ return { type: 'bearer', token: device.token };
150
+ }
151
+ if (!loginOptions.app) {
152
+ return null;
153
+ }
154
+ try {
155
+ return await getDeploymentAuth(controllerUrl, environment, loginOptions.app);
156
+ } catch (_authError) {
157
+ return null;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Resolve dataplane URL after login (mirrors auth-status silent resolver).
163
+ * @private
164
+ * @async
165
+ * @param {string} controllerUrl
166
+ * @param {string} environment
167
+ * @param {Object} authConfig
168
+ * @returns {Promise<string|null>}
169
+ */
170
+ async function resolveDataplaneUrlAfterLogin(controllerUrl, environment, authConfig) {
171
+ const { findDataplaneServiceAppKey } = require('./wizard-dataplane');
172
+ const { getDataplaneUrl } = require('../datasource/deploy');
173
+ try {
174
+ const dataplaneAppKey = await findDataplaneServiceAppKey(
175
+ controllerUrl,
176
+ environment,
177
+ authConfig
178
+ );
179
+ if (dataplaneAppKey) {
180
+ return await getDataplaneUrl(controllerUrl, dataplaneAppKey, environment, authConfig);
181
+ }
182
+ return await getDataplaneUrl(controllerUrl, 'dataplane', environment, authConfig);
183
+ } catch (_resolveError) {
184
+ return null;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Best-effort health refresh after login when dataplane URL is discoverable.
190
+ * Never throws — cache failures must not block login success.
191
+ *
192
+ * @async
193
+ * @param {string} controllerUrl - Controller URL (device cache key)
194
+ * @param {Object} [loginOptions]
195
+ * @param {string} [loginOptions.app] - App name (credentials login) for client-token auth
196
+ * @returns {Promise<void>}
197
+ */
198
+ async function tryRefreshDataplaneVersionAfterLogin(controllerUrl, loginOptions = {}) {
199
+ if (!controllerUrl || typeof controllerUrl !== 'string') {
200
+ return;
201
+ }
202
+ try {
203
+ const { resolveEnvironment } = require('../core/config');
204
+ const environment = await resolveEnvironment();
205
+ const authConfig = await resolveAuthConfigAfterLogin(
206
+ controllerUrl,
207
+ environment,
208
+ loginOptions
209
+ );
210
+ if (!authConfig) {
211
+ return;
212
+ }
213
+ const dataplaneUrl = await resolveDataplaneUrlAfterLogin(
214
+ controllerUrl,
215
+ environment,
216
+ authConfig
217
+ );
218
+ if (dataplaneUrl) {
219
+ await refreshDataplaneVersionInfo(controllerUrl, dataplaneUrl);
220
+ }
221
+ } catch (_error) {
222
+ // best-effort only
223
+ }
224
+ }
225
+
226
+ module.exports = {
227
+ refreshDataplaneVersionInfo,
228
+ loadCachedVersionInfo,
229
+ computeCompatibility,
230
+ tryRefreshDataplaneVersionAfterLogin
231
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Auth-status dataplane version block + blocking upgrade UX (plan 142.0).
3
+ *
4
+ * Kept in a separate module so `auth-status.js` stays under the 500-line
5
+ * limit and so the same display helpers can be reused by future dataplane
6
+ * gate surfaces (`upload`, `wizard`, `deploy`, …).
7
+ *
8
+ * @fileoverview TTY rendering for dataplane / Builder CLI compatibility
9
+ * @author AI Fabrix Team
10
+ * @version 2.0.0
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const chalk = require('chalk');
16
+ const logger = require('../utils/logger');
17
+ const {
18
+ successGlyph,
19
+ failureGlyph
20
+ } = require('../utils/cli-test-layout-chalk');
21
+ const {
22
+ formatCliVersionGateError
23
+ } = require('../utils/dataplane-cli-version-help');
24
+
25
+ /**
26
+ * Format a single key/value version line indented under the dataplane block.
27
+ * @private
28
+ * @param {string} label
29
+ * @param {string|undefined|null} value
30
+ * @returns {string}
31
+ */
32
+ function versionLine(label, value) {
33
+ const display = value && String(value).trim() ? chalk.cyan(value) : chalk.gray('—');
34
+ return ` ${label}: ${display}`;
35
+ }
36
+
37
+ /**
38
+ * Render the dataplane version subsection on `aifabrix auth status`.
39
+ *
40
+ * Designed to be called immediately after the existing connection/status line,
41
+ * so the output reads as a single grouped block per layout.md §HEADER BLOCK.
42
+ *
43
+ * @param {Object} params
44
+ * @param {boolean} params.connected - Whether dataplane reachability probe passed
45
+ * @param {string|undefined} params.dataplaneVersion - From `device.<controllerUrl>.dataplane-version`
46
+ * @param {string|undefined} params.minBuilderCliVersion - From `device.<controllerUrl>.dataplane-min-cli-version`
47
+ * @param {string} params.cliVersion - Installed Builder CLI version
48
+ * @param {boolean} params.compatible - true when CLI ≄ minimum (or no minimum enforced)
49
+ * @returns {void}
50
+ */
51
+ function displayDataplaneVersionSection({
52
+ connected,
53
+ dataplaneVersion,
54
+ minBuilderCliVersion,
55
+ cliVersion,
56
+ compatible
57
+ }) {
58
+ if (!connected && !dataplaneVersion) {
59
+ // Nothing to add — keep auth status quiet when the dataplane probe failed
60
+ // and no cached version is known (the reachability line already told the user).
61
+ return;
62
+ }
63
+ logger.log(versionLine('Dataplane version', dataplaneVersion));
64
+ logger.log(versionLine('Min Builder CLI', minBuilderCliVersion));
65
+ logger.log(versionLine('This CLI', cliVersion));
66
+ if (!minBuilderCliVersion) {
67
+ logger.log(` Compatibility: ${chalk.gray('Not enforced')}`);
68
+ return;
69
+ }
70
+ if (compatible) {
71
+ logger.log(` Compatibility: ${successGlyph()} ${chalk.green('OK')}`);
72
+ } else {
73
+ logger.log(` Compatibility: ${failureGlyph()} ${chalk.red('Upgrade required')}`);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Render the blocking upgrade block (red āœ– + Next actions) under the
79
+ * dataplane subsection when the CLI is incompatible.
80
+ *
81
+ * @param {Object} params
82
+ * @param {string} params.required - Required minimum semver
83
+ * @param {string} params.installed - Currently installed semver
84
+ * @returns {void}
85
+ */
86
+ function displayCliUpgradeRequired({ required, installed }) {
87
+ logger.log('');
88
+ logger.log(formatCliVersionGateError(required, installed));
89
+ }
90
+
91
+ module.exports = {
92
+ displayDataplaneVersionSection,
93
+ displayCliUpgradeRequired
94
+ };
@@ -19,6 +19,19 @@ const { resolveControllerUrl } = require('../utils/controller-url');
19
19
  const { findDataplaneServiceAppKey } = require('./wizard-dataplane');
20
20
  const { getDataplaneUrl } = require('../datasource/deploy');
21
21
  const { checkDataplaneHealth } = require('../utils/dataplane-health');
22
+ const {
23
+ displayDataplaneVersionSection,
24
+ displayCliUpgradeRequired
25
+ } = require('./auth-status-display');
26
+ const {
27
+ refreshDataplaneVersionInfo,
28
+ loadCachedVersionInfo
29
+ } = require('./auth-status-dataplane-version');
30
+ const {
31
+ EXIT_NOT_AUTHENTICATED,
32
+ EXIT_CLI_VERSION_INCOMPATIBLE,
33
+ EXIT_OK
34
+ } = require('../utils/cli-exit-codes');
22
35
 
23
36
  /**
24
37
  * Format expiration date for display
@@ -290,6 +303,10 @@ function displayOpenApiDocs(controllerUrl, dataplaneUrl) {
290
303
  * @param {Object} [dataplaneInfo] - Optional dataplane URL and health
291
304
  * @param {string|null} [dataplaneInfo.url] - Dataplane URL
292
305
  * @param {boolean} [dataplaneInfo.connected] - Whether dataplane is reachable
306
+ * @param {string|undefined} [dataplaneInfo.dataplaneVersion]
307
+ * @param {string|undefined} [dataplaneInfo.minBuilderCliVersion]
308
+ * @param {string} [dataplaneInfo.cliVersion]
309
+ * @param {boolean} [dataplaneInfo.compatible]
293
310
  */
294
311
  function displayStatus(controllerUrl, environment, tokenInfo, dataplaneInfo) {
295
312
  logger.log(chalk.bold('\nšŸ” Authentication Status\n'));
@@ -308,18 +325,61 @@ function displayStatus(controllerUrl, environment, tokenInfo, dataplaneInfo) {
308
325
 
309
326
  if (tokenInfo.authenticated && dataplaneInfo) {
310
327
  displayDataplaneSection(dataplaneInfo.url, dataplaneInfo.connected);
328
+ displayDataplaneVersionSection({
329
+ connected: dataplaneInfo.connected,
330
+ dataplaneVersion: dataplaneInfo.dataplaneVersion,
331
+ minBuilderCliVersion: dataplaneInfo.minBuilderCliVersion,
332
+ cliVersion: dataplaneInfo.cliVersion,
333
+ compatible: dataplaneInfo.compatible
334
+ });
335
+ if (dataplaneInfo.minBuilderCliVersion && dataplaneInfo.compatible === false) {
336
+ displayCliUpgradeRequired({
337
+ required: dataplaneInfo.minBuilderCliVersion,
338
+ installed: dataplaneInfo.cliVersion
339
+ });
340
+ }
311
341
  }
312
342
 
313
343
  logger.log('');
314
344
  }
315
345
 
346
+ /**
347
+ * Resolve the dataplane block for auth status (URL, reachability, versions,
348
+ * compatibility). Returns null when no dataplane is discovered.
349
+ *
350
+ * @async
351
+ * @function buildDataplaneInfo
352
+ * @param {string} controllerUrl
353
+ * @param {string} environment
354
+ * @param {Object} authConfig - Bearer auth config built from a valid token
355
+ * @returns {Promise<Object|null>}
356
+ */
357
+ async function buildDataplaneInfo(controllerUrl, environment, authConfig) {
358
+ const dataplaneUrl = await resolveDataplaneUrlSilent(controllerUrl, environment, authConfig);
359
+ if (!dataplaneUrl) {
360
+ return { url: null, connected: false };
361
+ }
362
+ const connected = await checkDataplaneHealth(dataplaneUrl, 5000);
363
+ const versionInfo = connected
364
+ ? await refreshDataplaneVersionInfo(controllerUrl, dataplaneUrl)
365
+ : await loadCachedVersionInfo(controllerUrl);
366
+ return {
367
+ url: dataplaneUrl,
368
+ connected,
369
+ dataplaneVersion: versionInfo.dataplaneVersion,
370
+ minBuilderCliVersion: versionInfo.minBuilderCliVersion,
371
+ cliVersion: versionInfo.cliVersion,
372
+ compatible: versionInfo.compatible
373
+ };
374
+ }
375
+
316
376
  /**
317
377
  * Handle auth status command
318
378
  * Controller and environment come from config.yaml (set via aifabrix login or aifabrix auth config).
319
379
  * @async
320
380
  * @function handleAuthStatus
321
381
  * @param {Object} options - Command options
322
- * @param {boolean} [options.validate] - If true, exit with code 1 when not authenticated
382
+ * @param {boolean} [options.validate] - If true, exit with code 1 when not authenticated, or code 3 when CLI is older than dataplane minimum
323
383
  * @returns {Promise<void>} Resolves when status is displayed
324
384
  */
325
385
  async function handleAuthStatus(options = {}) {
@@ -338,15 +398,19 @@ async function handleAuthStatus(options = {}) {
338
398
  let dataplaneInfo = null;
339
399
  if (tokenInfo && tokenInfo.authenticated && tokenInfo.token) {
340
400
  const authConfig = { type: 'bearer', token: tokenInfo.token };
341
- const dataplaneUrl = await resolveDataplaneUrlSilent(controllerUrl, environment, authConfig);
342
- const connected = dataplaneUrl ? await checkDataplaneHealth(dataplaneUrl, 5000) : false;
343
- dataplaneInfo = { url: dataplaneUrl, connected };
401
+ dataplaneInfo = await buildDataplaneInfo(controllerUrl, environment, authConfig);
344
402
  }
345
403
 
346
404
  displayStatus(controllerUrl, environment, tokenInfo, dataplaneInfo);
347
405
 
348
- if (options.validate && (!tokenInfo || !tokenInfo.authenticated)) {
349
- process.exit(1);
406
+ if (options.validate) {
407
+ if (!tokenInfo || !tokenInfo.authenticated) {
408
+ process.exit(EXIT_NOT_AUTHENTICATED);
409
+ }
410
+ if (dataplaneInfo && dataplaneInfo.minBuilderCliVersion && dataplaneInfo.compatible === false) {
411
+ process.exit(EXIT_CLI_VERSION_INCOMPATIBLE);
412
+ }
413
+ process.exit(EXIT_OK);
350
414
  }
351
415
  }
352
416