@aifabrix/builder 2.50.0 → 2.51.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 (103) hide show
  1. package/.cursor/plans/157.1-security-quality-violations-by-type.csv +374 -0
  2. package/.cursor/rules/assistant-driven-integration-build.mdc +40 -0
  3. package/integration/hubspot-test/create-hubspot.js +1 -16
  4. package/lib/api/evidence-kinds.api.js +128 -0
  5. package/lib/api/pipeline.api.js +5 -1
  6. package/lib/api/resource-types.api.js +4 -4
  7. package/lib/api/roles.api.js +121 -0
  8. package/lib/api/types/evidence-kinds.types.js +42 -0
  9. package/lib/api/types/roles.types.js +36 -0
  10. package/lib/app/certification-verify-rows.js +1 -1
  11. package/lib/app/show.js +2 -2
  12. package/lib/certification/merge-certification-from-artifact.js +1 -1
  13. package/lib/channels/channel-artifact-generator.js +2 -1
  14. package/lib/cli/index.js +4 -0
  15. package/lib/cli/setup-app.js +22 -5
  16. package/lib/cli/setup-dev.js +23 -7
  17. package/lib/cli/setup-utility-help-after.js +12 -0
  18. package/lib/cli/setup-utility-validate.js +41 -2
  19. package/lib/commands/deploy-gate-failure-details.js +21 -0
  20. package/lib/commands/deploy-gate-orchestrator.js +64 -18
  21. package/lib/commands/dev-cli-handlers.js +6 -3
  22. package/lib/commands/dev-init.js +7 -109
  23. package/lib/commands/dev-refresh.js +197 -0
  24. package/lib/commands/evidence-kind.js +428 -0
  25. package/lib/commands/governance-command-action.js +4 -2
  26. package/lib/commands/repair-openapi-export.js +1 -1
  27. package/lib/commands/repair-openapi-sync.js +14 -6
  28. package/lib/commands/resource-type.js +101 -18
  29. package/lib/commands/role.js +452 -0
  30. package/lib/commands/setup.js +1 -1
  31. package/lib/commands/validate-json.js +230 -0
  32. package/lib/commands/verify-operations-step-plan.js +78 -40
  33. package/lib/core/audit-logger.js +1 -1
  34. package/lib/core/secrets-load.js +1 -3
  35. package/lib/datasource/capability/relate-operations.js +1 -1
  36. package/lib/generator/helpers.js +13 -5
  37. package/lib/generator/split-variables.js +9 -3
  38. package/lib/infrastructure/helpers-docker-check.js +6 -0
  39. package/lib/infrastructure/helpers.js +1 -1
  40. package/lib/programmatic/auth-config.js +3 -2
  41. package/lib/programmatic/bearer-auth.js +1 -1
  42. package/lib/programmatic/enterprise-mcp-client.js +18 -2
  43. package/lib/programmatic/manifest-section-registry.js +18 -1
  44. package/lib/programmatic/openapi-result-schemas.js +28 -11
  45. package/lib/programmatic/openapi-schema-descriptions-core.js +5 -2
  46. package/lib/programmatic/openapi-schema-helpers.js +16 -1
  47. package/lib/programmatic/openapi-spec.js +1 -1
  48. package/lib/programmatic/run-repository-emcp.js +11 -3
  49. package/lib/programmatic/run-template-workspace.js +14 -1
  50. package/lib/resolvers/evidence-kind-file.js +230 -0
  51. package/lib/resolvers/resource-type-file.js +36 -1
  52. package/lib/resolvers/role-file.js +227 -0
  53. package/lib/schema/application-schema.json +22 -2
  54. package/lib/schema/external-datasource.schema.json +85 -2
  55. package/lib/schema/external-system.schema.json +16 -2
  56. package/lib/schema/infra.parameter.yaml +25 -1
  57. package/lib/schema/type/credential-field-registry.json +19 -3
  58. package/lib/utils/api.js +8 -2
  59. package/lib/utils/app-register-config.js +4 -3
  60. package/lib/utils/cli-sync-options.js +59 -3
  61. package/lib/utils/cli-utils.js +11 -3
  62. package/lib/utils/dev-ca-install.js +5 -6
  63. package/lib/utils/dev-config.js +2 -2
  64. package/lib/utils/dev-list-sort.js +145 -0
  65. package/lib/utils/dev-platform-tls.js +0 -63
  66. package/lib/utils/docker-build.js +46 -27
  67. package/lib/utils/docker-daemon-tls-ca.js +1 -1
  68. package/lib/utils/docker-exec.js +91 -6
  69. package/lib/utils/env-copy.js +1 -3
  70. package/lib/utils/error-formatters/error-parser.js +0 -3
  71. package/lib/utils/external-env-template.js +1 -1
  72. package/lib/utils/external-system-readiness-compact-blocks.js +28 -1
  73. package/lib/utils/health-check-request.js +2 -3
  74. package/lib/utils/help-builder.js +3 -0
  75. package/lib/utils/image-name.js +22 -4
  76. package/lib/utils/logger.js +37 -4
  77. package/lib/utils/manifest-source-emit.js +1 -1
  78. package/lib/utils/paths.js +32 -4
  79. package/lib/utils/pipeline-upload-auth-retry.js +4 -0
  80. package/lib/utils/postgres-platform-bootstrap.js +24 -17
  81. package/lib/utils/postgres-wipe.js +5 -2
  82. package/lib/utils/remote-dev-auth.js +39 -4
  83. package/lib/utils/resolve-docker-image-ref.js +36 -6
  84. package/lib/utils/token-manager.js +16 -0
  85. package/lib/utils/upload-sync-options.js +17 -3
  86. package/lib/validation/openapi-contract-surface-validator.js +4 -0
  87. package/lib/validation/validator.js +6 -5
  88. package/package.json +34 -12
  89. package/scripts/export-builder-api-evidence.js +2 -2
  90. package/scripts/lib/plan-actor.js +2 -2
  91. package/scripts/lib/plan-meta.js +1 -1
  92. package/scripts/lib/plan-parse.js +1 -1
  93. package/scripts/lib/plan-prevalidate-gates.js +12 -1
  94. package/scripts/lib/plan-run-report.js +13 -2
  95. package/scripts/pnpm/lib/resolve-aifabrix-bin.mjs +10 -10
  96. package/templates/applications/dataplane/application.yaml +26 -4
  97. package/templates/applications/dataplane/env.template +28 -9
  98. package/templates/applications/dataplane/rbac.yaml +64 -2
  99. package/templates/applications/miso-controller/application.yaml +10 -1
  100. package/templates/applications/miso-controller/env.template +10 -7
  101. package/templates/applications/miso-controller/rbac.yaml +72 -0
  102. package/templates/channels/slack/integration/slack-system.json +60 -2
  103. package/templates/python/requirements.txt +1 -1
@@ -247,11 +247,15 @@ async function testDatasourceViaPipeline({ dataplaneUrl, systemKey, datasourceKe
247
247
  * @returns {Promise<Object>} API envelope `{ success, data }` where `data` is PublicationResult (uploadId, system, datasources, generateMcpContract, …)
248
248
  * @throws {Error} If upload fails
249
249
  */
250
+ /** Multi-datasource publish often exceeds the default 30s HTTP timeout. */
251
+ const PIPELINE_UPLOAD_TIMEOUT_MS = 300000;
252
+
250
253
  async function uploadApplicationViaPipeline(dataplaneUrl, authConfig, payload) {
251
254
  const body = { ...payload, status: payload.status ?? 'draft' };
252
255
  const client = createDataplaneApiClient(dataplaneUrl, authConfig);
253
256
  return await client.post('/api/v1/pipeline/upload', {
254
- body
257
+ body,
258
+ timeoutMs: PIPELINE_UPLOAD_TIMEOUT_MS
255
259
  });
256
260
  }
257
261
 
@@ -12,7 +12,7 @@ const BASE = '/api/v1/resource-types';
12
12
 
13
13
  /**
14
14
  * List resource types (paginated)
15
- * @requiresPermission {Controller} resourceTypes:read
15
+ * @requiresPermission {Controller} resource-types:read
16
16
  * @async
17
17
  * @function listResourceTypes
18
18
  * @param {string} controllerUrl
@@ -34,7 +34,7 @@ async function listResourceTypes(controllerUrl, authConfig, options = {}) {
34
34
 
35
35
  /**
36
36
  * Get a resource type by id or key
37
- * @requiresPermission {Controller} resourceTypes:read
37
+ * @requiresPermission {Controller} resource-types:read
38
38
  * @async
39
39
  * @function getResourceType
40
40
  * @param {string} controllerUrl
@@ -49,7 +49,7 @@ async function getResourceType(controllerUrl, authConfig, resourceTypeIdOrKey) {
49
49
 
50
50
  /**
51
51
  * Create a resource type
52
- * @requiresPermission {Controller} resourceTypes:create
52
+ * @requiresPermission {Controller} resource-types:create
53
53
  * @async
54
54
  * @function createResourceType
55
55
  * @param {string} controllerUrl
@@ -75,7 +75,7 @@ async function createResourceType(controllerUrl, authConfig, body) {
75
75
 
76
76
  /**
77
77
  * Create-if-missing (idempotent): if a resource type already exists for the key, return success.
78
- * @requiresPermission {Controller} resourceTypes:read + resourceTypes:create
78
+ * @requiresPermission {Controller} resource-types:read + resource-types:create
79
79
  * @async
80
80
  * @function createResourceTypeIdempotent
81
81
  * @param {string} controllerUrl
@@ -0,0 +1,121 @@
1
+ /**
2
+ * @fileoverview Business roles API (Controller /api/v1/roles)
3
+ * @author AI Fabrix Team
4
+ * @version 1.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { ApiClient } = require('./index');
10
+
11
+ const BASE = '/api/v1/roles';
12
+
13
+ /**
14
+ * List business roles (paginated)
15
+ * @requiresPermission {Controller} roles:read
16
+ * @async
17
+ * @param {string} controllerUrl
18
+ * @param {Object} authConfig
19
+ * @param {Object} [options]
20
+ * @returns {Promise<Object>}
21
+ */
22
+ async function listRoles(controllerUrl, authConfig, options = {}) {
23
+ const client = new ApiClient(controllerUrl, authConfig);
24
+ return await client.get(BASE, { params: options });
25
+ }
26
+
27
+ /**
28
+ * Get a business role by id or key
29
+ * @requiresPermission {Controller} roles:read
30
+ * @async
31
+ * @param {string} controllerUrl
32
+ * @param {Object} authConfig
33
+ * @param {string} roleIdOrKey
34
+ * @returns {Promise<Object>}
35
+ */
36
+ async function getRole(controllerUrl, authConfig, roleIdOrKey) {
37
+ const client = new ApiClient(controllerUrl, authConfig);
38
+ return await client.get(`${BASE}/${encodeURIComponent(roleIdOrKey)}`);
39
+ }
40
+
41
+ /**
42
+ * @param {Object} payload
43
+ * @param {string} key
44
+ * @param {*} value
45
+ * @returns {void}
46
+ */
47
+ function assignOptionalString(payload, key, value) {
48
+ if (value !== undefined && value !== null && value !== '') {
49
+ payload[key] = value;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * @param {import('./types/roles.types').CreateBusinessRoleRequest} body
55
+ * @returns {Object}
56
+ */
57
+ function buildCreateRoleBody(body) {
58
+ const payload = {
59
+ key: body.key,
60
+ domainKey: body.domainKey,
61
+ displayName: body.displayName
62
+ };
63
+ assignOptionalString(payload, 'description', body.description);
64
+ assignOptionalString(payload, 'purpose', body.purpose);
65
+ assignOptionalString(payload, 'businessOutcome', body.businessOutcome);
66
+ assignOptionalString(payload, 'source', body.source);
67
+ if (body.isActive !== undefined && body.isActive !== null) {
68
+ payload.isActive = Boolean(body.isActive);
69
+ }
70
+ if (body.metadata && typeof body.metadata === 'object') {
71
+ payload.metadata = body.metadata;
72
+ }
73
+ return payload;
74
+ }
75
+
76
+ /**
77
+ * Create a business role
78
+ * @requiresPermission {Controller} roles:create
79
+ * @async
80
+ * @param {string} controllerUrl
81
+ * @param {Object} authConfig
82
+ * @param {import('./types/roles.types').CreateBusinessRoleRequest} body
83
+ * @returns {Promise<Object>}
84
+ */
85
+ async function createRole(controllerUrl, authConfig, body) {
86
+ const client = new ApiClient(controllerUrl, authConfig);
87
+ return await client.post(BASE, { body: buildCreateRoleBody(body) });
88
+ }
89
+
90
+ /**
91
+ * Create-if-missing (idempotent): if a role already exists for the key, return success.
92
+ * @requiresPermission {Controller} roles:read + roles:create
93
+ * @async
94
+ * @param {string} controllerUrl
95
+ * @param {Object} authConfig
96
+ * @param {import('./types/roles.types').CreateBusinessRoleRequest} body
97
+ * @returns {Promise<{ created: boolean, response: Object }>}
98
+ */
99
+ async function createRoleIdempotent(controllerUrl, authConfig, body) {
100
+ const key = String(body?.key || '').trim();
101
+ if (!key) {
102
+ throw new Error('Role key is required');
103
+ }
104
+ const getRes = await getRole(controllerUrl, authConfig, key);
105
+ if (getRes && getRes.success === true) {
106
+ return { created: false, response: getRes };
107
+ }
108
+ const createRes = await createRole(controllerUrl, authConfig, body);
109
+ if (createRes && createRes.success === true) {
110
+ return { created: true, response: createRes };
111
+ }
112
+ const msg = createRes?.error || createRes?.formattedError || 'Failed to create role';
113
+ throw new Error(msg);
114
+ }
115
+
116
+ module.exports = {
117
+ listRoles,
118
+ getRole,
119
+ createRole,
120
+ createRoleIdempotent
121
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @fileoverview Evidence kind API type definitions (Controller /api/v1/evidence-kinds)
3
+ * @author AI Fabrix Team
4
+ * @version 1.0.0
5
+ */
6
+
7
+ /**
8
+ * Create evidence kind request body (API shape uses domainKey)
9
+ * @typedef {Object} CreateEvidenceKindRequest
10
+ * @property {string} key - Catalog key (lowercase, digits, underscores)
11
+ * @property {string} domainKey - Enterprise domain key
12
+ * @property {string} displayName - Human-readable name
13
+ * @property {string} purpose - Purpose (required by Controller)
14
+ * @property {string} [description] - Optional description
15
+ * @property {string} [expectedUse] - Expected use guidance
16
+ * @property {string} [defaultSeverity] - low | medium | high | critical
17
+ * @property {boolean} [requiresApproval] - Approval flag
18
+ * @property {boolean} [isActive] - Active flag
19
+ * @property {number} [sortOrder] - Sort order
20
+ * @property {string} [source] - Provenance source
21
+ * @property {Object} [metadata] - Extensions
22
+ */
23
+
24
+ /**
25
+ * Evidence kind record
26
+ * @typedef {Object} EvidenceKindRecord
27
+ * @property {string} id - Controller id
28
+ * @property {string} key - Catalog key
29
+ * @property {string} domainKey - Enterprise domain key
30
+ * @property {string} displayName - Display name
31
+ * @property {string} purpose - Purpose
32
+ * @property {string} [description] - Description
33
+ * @property {string} [expectedUse] - Expected use
34
+ * @property {string} [defaultSeverity] - Default severity
35
+ * @property {boolean} requiresApproval - Approval flag
36
+ * @property {boolean} isActive - Active flag
37
+ * @property {number} sortOrder - Sort order
38
+ * @property {string} [source] - Source
39
+ * @property {Object} [metadata] - Metadata
40
+ */
41
+
42
+ module.exports = {};
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @fileoverview Business role API type definitions (Controller /api/v1/roles)
3
+ * @author AI Fabrix Team
4
+ * @version 1.0.0
5
+ */
6
+
7
+ /**
8
+ * Create business role request body (API shape uses domainKey)
9
+ * @typedef {Object} CreateBusinessRoleRequest
10
+ * @property {string} key - Catalog key (lowercase, hyphens)
11
+ * @property {string} domainKey - Enterprise domain key
12
+ * @property {string} displayName - Human-readable name
13
+ * @property {string} [description] - Optional description
14
+ * @property {string} [purpose] - Role purpose
15
+ * @property {string} [businessOutcome] - Business outcome
16
+ * @property {boolean} [isActive] - Active flag
17
+ * @property {string} [source] - Provenance source
18
+ * @property {Object} [metadata] - Compiler fields and extensions
19
+ */
20
+
21
+ /**
22
+ * Business role record
23
+ * @typedef {Object} BusinessRoleRecord
24
+ * @property {string} id - Controller id
25
+ * @property {string} key - Catalog key
26
+ * @property {string} domainKey - Enterprise domain key
27
+ * @property {string} displayName - Display name
28
+ * @property {string} [description] - Description
29
+ * @property {string} [purpose] - Purpose
30
+ * @property {string} [businessOutcome] - Business outcome
31
+ * @property {boolean} isActive - Active flag
32
+ * @property {string} [source] - Source
33
+ * @property {Object} [metadata] - Metadata
34
+ */
35
+
36
+ module.exports = {};
@@ -16,7 +16,7 @@ const { getActiveIntegrationCertificate, verifyIntegrationCertificate } = requir
16
16
  function certificateIdString(id) {
17
17
  if (id === undefined || id === null) return null;
18
18
  if (typeof id === 'string') return id.trim() || null;
19
- if (typeof id === 'object' && id !== null && typeof id.id === 'string') return id.id.trim() || null;
19
+ if (typeof id === 'object' && typeof id.id === 'string') return id.id.trim() || null;
20
20
  return null;
21
21
  }
22
22
 
package/lib/app/show.js CHANGED
@@ -347,7 +347,7 @@ async function getShowAuthToken(controllerUrl, config) {
347
347
  }
348
348
 
349
349
  const formatted = formatAuthenticationError({
350
- controllerUrl: controllerUrl || undefined,
350
+ controllerUrl: undefined,
351
351
  message: 'No valid authentication found'
352
352
  });
353
353
  logger.error(formatted);
@@ -477,7 +477,7 @@ function resolveExternalSystemMeta(res, config, system, application, appKey) {
477
477
 
478
478
  function buildBaseExternalResult(configData, appKey, openapiFiles, openapiEndpoints, res) {
479
479
  const system = configData.system || configData;
480
- const dataSources = configData.dataSources || configData.dataSources || [];
480
+ const dataSources = configData.dataSources || [];
481
481
  const application = configData.application || configData.app || {};
482
482
  const config = res && res.config && typeof res.config === 'object' ? res.config : {};
483
483
  const meta = resolveExternalSystemMeta(res, config, system, application, appKey);
@@ -13,7 +13,7 @@
13
13
  function certificateIdToString(id) {
14
14
  if (id === undefined || id === null) return '';
15
15
  if (typeof id === 'string') return id.trim();
16
- if (typeof id === 'object' && id !== null && typeof id.id === 'string') return String(id.id).trim();
16
+ if (typeof id === 'object' && typeof id.id === 'string') return String(id.id).trim();
17
17
  return String(id).trim();
18
18
  }
19
19
 
@@ -16,6 +16,7 @@ const { getChannelTarget } = require('./channel-targets');
16
16
  const { BUILDER_ROOT } = require('./channel-integration-template');
17
17
 
18
18
  const SECRET_PATTERNS = [/xoxb-/i, /xoxp-/i, /client_secret/i, /signing_secret/i, /Bearer\s+/i];
19
+ const PLACEHOLDER_DOMAIN_RE = /\byour-domain\.com\b/i;
19
20
 
20
21
  /**
21
22
  * @param {string} content
@@ -27,7 +28,7 @@ function assertNoSecretsInArtifact(content) {
27
28
  throw new Error('Generated artifact must not contain secret-like values');
28
29
  }
29
30
  }
30
- if (content.includes('your-domain.com')) {
31
+ if (PLACEHOLDER_DOMAIN_RE.test(content)) {
31
32
  throw new Error('Generated artifact still contains placeholder domain your-domain.com');
32
33
  }
33
34
  }
package/lib/cli/index.js CHANGED
@@ -23,6 +23,8 @@ const { setupAppCommands: setupAppManagementCommands } = require('../commands/ap
23
23
  const { setupDatasourceCommands } = require('../commands/datasource');
24
24
  const { setupDimensionCommands } = require('../commands/dimension');
25
25
  const { setupResourceTypeCommands } = require('../commands/resource-type');
26
+ const { setupRoleCommands } = require('../commands/role');
27
+ const { setupEvidenceKindCommands } = require('../commands/evidence-kind');
26
28
  const { setupEnterpriseKnowledgeCommands } = require('../commands/enterprise-knowledge');
27
29
  const { setupIdentityCommands } = require('../commands/identity');
28
30
  const { setupProtectionCommands } = require('../commands/protection');
@@ -73,6 +75,8 @@ function setupCoreCliCommands(program) {
73
75
  function setupExtendedCliCommands(program) {
74
76
  setupDimensionCommands(program);
75
77
  setupResourceTypeCommands(program);
78
+ setupRoleCommands(program);
79
+ setupEvidenceKindCommands(program);
76
80
  setupEnterpriseKnowledgeCommands(program);
77
81
  setupIdentityCommands(program);
78
82
  setupProtectionCommands(program);
@@ -62,6 +62,8 @@ Notes:
62
62
  - Login or set MISO_CONTROLLER_URL + environment (aifabrix login / aifabrix auth).
63
63
  - CI: register once (aifabrix app register <appKey>), store DEV_MISO_CLIENTID and DEV_MISO_CLIENTSECRET in pipeline secrets.
64
64
  - upload = dataplane publish for dev iteration; deploy = controller validate + deploy (Azure or local Docker).
65
+ - Default output is a compact status card; use -v for expanded preflight and readiness detail.
66
+ - Controller deploy always polls to a terminal status (no --poll / --no-poll on this command).
65
67
  - --repository-url overrides repo URL for pipeline validate (default: application.yaml repository.repositoryUrl).
66
68
  `;
67
69
 
@@ -219,7 +221,7 @@ Examples:
219
221
  });
220
222
  }
221
223
 
222
- function setupBuildRunLogsDownCommands(program) {
224
+ function registerBuildCommand(program) {
223
225
  program.command('build <appKey>')
224
226
  .description('Build Docker image for builder appKey (auto-detect runtime)')
225
227
  .option('-l, --language <lang>', 'Override language detection')
@@ -243,9 +245,9 @@ function setupBuildRunLogsDownCommands(program) {
243
245
  process.exit(1);
244
246
  }
245
247
  });
248
+ }
246
249
 
247
- registerRunCommand(program);
248
-
250
+ function registerLogsCommand(program) {
249
251
  program.command('logs <appKey>')
250
252
  .description('Tail container logs for appKey (optional env summary; secrets masked)')
251
253
  .option('-f', 'Follow log stream')
@@ -256,14 +258,22 @@ function setupBuildRunLogsDownCommands(program) {
256
258
  try {
257
259
  const { runAppLogs } = require('../commands/app-logs');
258
260
  const tailNum = parseInt(options.tail, 10);
259
- const level = options.level !== undefined && options.level !== null && options.level !== '' ? String(options.level).trim() : undefined;
260
- await runAppLogs(appName, { follow: options.f, tail: Number.isNaN(tailNum) ? 100 : tailNum, level });
261
+ const level = options.level !== undefined && options.level !== null && options.level !== ''
262
+ ? String(options.level).trim()
263
+ : undefined;
264
+ await runAppLogs(appName, {
265
+ follow: options.f,
266
+ tail: Number.isNaN(tailNum) ? 100 : tailNum,
267
+ level
268
+ });
261
269
  } catch (error) {
262
270
  handleCommandError(error, 'logs');
263
271
  process.exit(1);
264
272
  }
265
273
  });
274
+ }
266
275
 
276
+ function registerDownAppCommand(program) {
267
277
  program.command('down-app <appKey>')
268
278
  .description('Stop and remove container for appKey (--volumes removes data volume)')
269
279
  .option('--volumes', 'Remove application Docker volume')
@@ -278,6 +288,13 @@ function setupBuildRunLogsDownCommands(program) {
278
288
  });
279
289
  }
280
290
 
291
+ function setupBuildRunLogsDownCommands(program) {
292
+ registerBuildCommand(program);
293
+ registerRunCommand(program);
294
+ registerLogsCommand(program);
295
+ registerDownAppCommand(program);
296
+ }
297
+
281
298
  function setupShellTestStopCommands(program) {
282
299
  program.command('stop <appKey>')
283
300
  .description('Alias for down-app: stop and remove container for appKey')
@@ -12,7 +12,8 @@ const config = require('../core/config');
12
12
  const logger = require('../utils/logger');
13
13
  const { handleCommandError } = require('../utils/cli-utils');
14
14
  const { setupDevPathAndFormatCommands } = require('./setup-dev-path-commands');
15
- const { runDevInit, runDevRefresh } = require('../commands/dev-init');
15
+ const { runDevInit } = require('../commands/dev-init');
16
+ const { runDevRefresh, ensureDevCertFreshIfDue } = require('../commands/dev-refresh');
16
17
  const { displayDevConfig } = require('../commands/dev-show-display');
17
18
  const {
18
19
  handleDevList,
@@ -49,11 +50,21 @@ Examples:
49
50
  Requires a configured remote Builder Server and an admin client certificate (same machine setup as dev list / dev pin). After add, run dev pin <id> once to create a PIN for aifabrix dev init.
50
51
  `;
51
52
 
53
+ /** Appended to `dev list --help` (sort examples). */
54
+ const DEV_LIST_HELP_AFTER = `
55
+ Examples:
56
+ $ aifabrix dev list
57
+ $ aifabrix dev list --sort id
58
+ $ aifabrix dev list --sort -until
59
+
60
+ Sort fields: id, name, email, until. Prefix - for descending (default: name ascending).
61
+ `;
62
+
52
63
  /** Appended to `aifabrix dev --help` (overview; keep subcommand .description lines short). */
53
64
  const DEV_GROUP_HELP_AFTER = `
54
65
  Categories:
55
66
  Local config show, set-id, set-scoped-resources, set-env-config, set-home, set-work, set-format, print-home, print-work
56
- Onboarding init (PIN onboarding), refresh (sync settings / renew cert)
67
+ Onboarding init (PIN onboarding), refresh (sync settings / renew cert); show auto-renews when due
57
68
  Remote admin list, add, update, pin, delete, platform-install, platform-update, platform-install-cli (need remote-server + client cert in ~/.aifabrix)
58
69
  Sync / stop down (Mutagen; optional --apps for containers)
59
70
 
@@ -168,9 +179,10 @@ async function handleSetScopedResources(value) {
168
179
  function setupDevShowAndSetId(dev) {
169
180
  dev
170
181
  .command('show')
171
- .description('Show dev ports and ~/.aifabrix config')
182
+ .description('Show dev ports and ~/.aifabrix config (auto-renews cert when due)')
172
183
  .action(async() => {
173
184
  try {
185
+ await ensureDevCertFreshIfDue();
174
186
  const devId = await config.getDeveloperId();
175
187
  await displayDevConfig(devId);
176
188
  } catch (error) {
@@ -286,9 +298,14 @@ function setupDevListAddCommands(dev) {
286
298
  dev
287
299
  .command('list')
288
300
  .description('List developers (remote Builder Server)')
289
- .action(async() => {
301
+ .option(
302
+ '--sort <field>',
303
+ 'Sort field/direction (id|name|email|until; prefix - for descending; default name)'
304
+ )
305
+ .addHelpText('after', DEV_LIST_HELP_AFTER)
306
+ .action(async(options) => {
290
307
  try {
291
- await handleDevList();
308
+ await handleDevList(options);
292
309
  } catch (error) {
293
310
  handleCommandError(error, 'dev list');
294
311
  process.exit(1);
@@ -398,8 +415,6 @@ function setupDevUpdatePinDeleteCommands(dev) {
398
415
  process.exit(1);
399
416
  }
400
417
  });
401
-
402
- setupDevDownCommand(dev);
403
418
  }
404
419
 
405
420
  /**
@@ -457,6 +472,7 @@ function setupDevPlatformCommands(dev) {
457
472
  function setupDevUserCommands(dev) {
458
473
  setupDevListAddCommands(dev);
459
474
  setupDevUpdatePinDeleteCommands(dev);
475
+ setupDevDownCommand(dev);
460
476
  setupDevPlatformCommands(dev);
461
477
  }
462
478
 
@@ -18,6 +18,17 @@ Examples:
18
18
  $ aifabrix validate --builder
19
19
  `;
20
20
 
21
+ const VALIDATE_JSON_HELP_AFTER = `
22
+ Examples:
23
+ $ aifabrix validate-json myapp
24
+ $ aifabrix validate-json my-system
25
+ $ aifabrix validate-json --integration
26
+ $ aifabrix validate-json --builder
27
+ $ aifabrix validate-json --integration --builder --format json
28
+
29
+ Runs full local validation first; writes *-deploy.json only when validation succeeds (offline).
30
+ `;
31
+
21
32
  const REPAIR_HELP_AFTER = `
22
33
  Examples:
23
34
  $ aifabrix repair hubspot-demo
@@ -63,6 +74,7 @@ Examples:
63
74
  module.exports = {
64
75
  JSON_HELP_AFTER,
65
76
  VALIDATE_HELP_AFTER,
77
+ VALIDATE_JSON_HELP_AFTER,
66
78
  REPAIR_HELP_AFTER,
67
79
  RESOLVE_HELP_AFTER
68
80
  };
@@ -12,7 +12,7 @@ const fs = require('fs');
12
12
  const chalk = require('chalk');
13
13
  const logger = require('../utils/logger');
14
14
  const { handleCommandError } = require('../utils/cli-utils');
15
- const { VALIDATE_HELP_AFTER } = require('./setup-utility-help-after');
15
+ const { VALIDATE_HELP_AFTER, VALIDATE_JSON_HELP_AFTER } = require('./setup-utility-help-after');
16
16
 
17
17
  /**
18
18
  * @param {string} appOrFile
@@ -154,7 +154,7 @@ async function runValidateCommand(appOrFile, options) {
154
154
  /**
155
155
  * @param {import('commander').Command} program
156
156
  */
157
- function setupValidateDiffCommands(program) {
157
+ function setupValidateCommand(program) {
158
158
  program.command('validate [appKey|systemKey|file]')
159
159
  .description('Validate appKey, systemKey, file path, .protection batch, or all under integration/ and builder/')
160
160
  .addHelpText('after', VALIDATE_HELP_AFTER)
@@ -173,7 +173,37 @@ function setupValidateDiffCommands(program) {
173
173
  process.exit(1);
174
174
  });
175
175
  });
176
+ }
177
+
178
+ /**
179
+ * @param {import('commander').Command} program
180
+ */
181
+ function setupValidateJsonCommand(program) {
182
+ program.command('validate-json [appKey|systemKey]')
183
+ .description(
184
+ 'Validate locally then write *-deploy.json only when validation succeeds (offline; no file-path mode)'
185
+ )
186
+ .addHelpText('after', VALIDATE_JSON_HELP_AFTER)
187
+ .option('--format <format>', 'Output format: json | default (human-readable)')
188
+ .option('--integration', 'Validate and write deploy JSON for all apps under integration/')
189
+ .option('--builder', 'Validate and write deploy JSON for all apps under builder/')
190
+ .action((appKey, options) => {
191
+ const { runValidateJsonCommand } = require('../commands/validate-json');
192
+ runValidateJsonCommand(appKey, options)
193
+ .then(({ exitCode }) => {
194
+ if (exitCode !== 0) process.exit(exitCode);
195
+ })
196
+ .catch((error) => {
197
+ handleCommandError(error, 'validate-json');
198
+ process.exit(1);
199
+ });
200
+ });
201
+ }
176
202
 
203
+ /**
204
+ * @param {import('commander').Command} program
205
+ */
206
+ function setupDiffCommand(program) {
177
207
  program.command('diff <file1> <file2>')
178
208
  .description('Diff two config files (optional schema validate)')
179
209
  .option('--no-validate', 'Skip schema validation (type check still applied)')
@@ -191,6 +221,15 @@ function setupValidateDiffCommands(program) {
191
221
  });
192
222
  }
193
223
 
224
+ /**
225
+ * @param {import('commander').Command} program
226
+ */
227
+ function setupValidateDiffCommands(program) {
228
+ setupValidateCommand(program);
229
+ setupValidateJsonCommand(program);
230
+ setupDiffCommand(program);
231
+ }
232
+
194
233
  module.exports = {
195
234
  setupValidateDiffCommands,
196
235
  runValidateCommand
@@ -124,6 +124,27 @@ function formatIntegrationGateFailure(result) {
124
124
  if (result?.message) {
125
125
  return String(result.message);
126
126
  }
127
+ const rows = Array.isArray(result?.datasourceResults) ? result.datasourceResults : [];
128
+ const hints = [];
129
+ for (const row of rows) {
130
+ if (!row || row.success !== false) {
131
+ continue;
132
+ }
133
+ const key = row.key || row.datasourceKey || 'datasource';
134
+ const issues = row.datasourceTestRun?.validation?.issues;
135
+ if (Array.isArray(issues) && issues.length > 0) {
136
+ const first = issues[0];
137
+ const code = first.code ? `${first.code}: ` : '';
138
+ hints.push(`${key}: ${code}${first.message || 'validation failed'}`);
139
+ continue;
140
+ }
141
+ if (row.error) {
142
+ hints.push(`${key}: ${row.error}`);
143
+ }
144
+ }
145
+ if (hints.length > 0) {
146
+ return hints.slice(0, 3).join('; ');
147
+ }
127
148
  return 'integration test did not succeed';
128
149
  }
129
150