@aifabrix/builder 2.55.0 → 2.55.2

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 (70) hide show
  1. package/lib/agent-kit/host-approve.js +48 -6
  2. package/lib/agent-kit/init.js +2 -2
  3. package/lib/agent-kit/remote-prompts.js +46 -13
  4. package/lib/agent-kit/run.js +1 -1
  5. package/lib/agent-kit/start.js +310 -0
  6. package/lib/app/deploy.js +7 -4
  7. package/lib/app/offer-create-from-template.js +184 -0
  8. package/lib/cli/setup-app.js +8 -1
  9. package/lib/cli/setup-auth.js +11 -2
  10. package/lib/commands/agent-kit-help.js +74 -4
  11. package/lib/commands/agent-kit.js +83 -12
  12. package/lib/commands/auth-status-user.js +99 -0
  13. package/lib/commands/auth-status.js +14 -4
  14. package/lib/commands/datasource-load-export-cli.js +7 -2
  15. package/lib/commands/datasource-test-trust-cli.js +5 -1
  16. package/lib/commands/governance-command-action.js +5 -4
  17. package/lib/commands/identity-membership-role-cache.js +6 -1
  18. package/lib/commands/lifecycle-command-action.js +16 -15
  19. package/lib/commands/login-credentials.js +8 -3
  20. package/lib/commands/login-device.js +14 -5
  21. package/lib/commands/login.js +14 -1
  22. package/lib/commands/logout.js +50 -15
  23. package/lib/commands/role-assistant.js +24 -12
  24. package/lib/commands/verify-governance-command-action.js +9 -7
  25. package/lib/commands/verify-operations-command-action.js +9 -8
  26. package/lib/commands/verify-trust-command-action.js +6 -5
  27. package/lib/external-system/download.js +1 -2
  28. package/lib/generator/helpers.js +12 -1
  29. package/lib/integration-definition/apply-static-enterprise.js +5 -2
  30. package/lib/role-assistant/knowledge-sync-settings-push.js +172 -0
  31. package/lib/role-assistant/knowledge-sync.js +9 -126
  32. package/lib/role-assistant/test-cases-evaluate.js +12 -1
  33. package/lib/role-assistant/test-cases-evidence.js +44 -5
  34. package/lib/role-assistant/test-cases-lifecycle.js +9 -3
  35. package/lib/role-assistant/test-runner.js +3 -0
  36. package/lib/schema/infra-parameter.schema.json +51 -3
  37. package/lib/schema/infra.parameter.yaml +476 -40
  38. package/lib/schema/type/document-storage.json +1 -1
  39. package/lib/utils/api-required-permissions.js +271 -0
  40. package/lib/utils/api.js +16 -13
  41. package/lib/utils/cli-json-exit.js +41 -0
  42. package/lib/utils/cli-utils.js +23 -5
  43. package/lib/utils/controller-url.js +78 -4
  44. package/lib/utils/deployment-errors.js +15 -0
  45. package/lib/utils/deployment-validation-helpers.js +4 -1
  46. package/lib/utils/device-code.js +31 -10
  47. package/lib/utils/error-formatters/error-parser.js +16 -1
  48. package/lib/utils/error-formatters/permission-errors.js +19 -5
  49. package/lib/utils/keycloak-realm-ready.js +5 -2
  50. package/lib/utils/logger.js +1 -0
  51. package/lib/utils/paths.js +26 -5
  52. package/lib/utils/secrets-helpers.js +2 -2
  53. package/lib/utils/write-stream-and-wait.js +63 -0
  54. package/package.json +20 -6
  55. package/templates/agent-kit/agent-kit.yaml +1 -1
  56. package/templates/agent-kit/instructions/AGENTKIT.md +25 -4
  57. package/templates/agent-kit/instructions/root.AGENTS.md +1 -1
  58. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
  59. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +1 -1
  60. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +2 -1
  61. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +1 -0
  62. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +1 -0
  63. package/templates/agent-kit/skills/shared/hosts.md +13 -26
  64. package/templates/agent-kit/skills/shared/login.md +16 -0
  65. package/templates/agent-kit/workspace/README.md +1 -1
  66. package/templates/applications/dataplane/application.yaml +1 -2
  67. package/templates/applications/dataplane/env.template +5 -5
  68. package/templates/applications/miso-controller/application.yaml +21 -2
  69. package/templates/applications/miso-controller/env.template +89 -17
  70. package/templates/applications/miso-controller/rbac.yaml +5 -0
@@ -34,6 +34,7 @@ const {
34
34
  displayStoredClientTokensSection,
35
35
  tryResolveClientTokenAuth
36
36
  } = require('./auth-status-client-tokens');
37
+ const { parseAuthUserResponse } = require('./auth-status-user');
37
38
 
38
39
  /**
39
40
  * Format expiration date for display
@@ -71,11 +72,13 @@ async function checkDeviceToken(controllerUrl) {
71
72
  const response = await getAuthUser(controllerUrl, authConfig);
72
73
 
73
74
  if (response.success && response.data) {
75
+ const parsed = parseAuthUserResponse(response, deviceToken.token);
74
76
  return {
75
77
  type: 'Device Token',
76
78
  token: deviceToken.token,
77
- authenticated: response.data.authenticated !== false,
78
- user: response.data.user,
79
+ authenticated: parsed.authenticated,
80
+ user: parsed.user,
81
+ username: parsed.username,
79
82
  expiresAt: deviceToken.expiresAt
80
83
  };
81
84
  }
@@ -113,11 +116,13 @@ async function validateClientToken(token, controllerUrl, environment, appName, e
113
116
  const response = await getAuthUser(controllerUrl, authConfig);
114
117
 
115
118
  if (response.success && response.data) {
119
+ const parsed = parseAuthUserResponse(response, token);
116
120
  return {
117
121
  type: 'Client Token',
118
122
  token: token,
119
- authenticated: response.data.authenticated !== false,
120
- user: response.data.user,
123
+ authenticated: parsed.authenticated,
124
+ user: parsed.user,
125
+ username: parsed.username,
121
126
  expiresAt: expiresAt,
122
127
  appName: appName
123
128
  };
@@ -185,6 +190,11 @@ function displayTokenInfo(tokenInfo) {
185
190
  logger.log(` Status: ${statusIcon} ${statusText}`);
186
191
  logger.log(` Token Type: ${chalk.cyan(tokenInfo.type)}`);
187
192
 
193
+ if (tokenInfo.authenticated) {
194
+ const username = tokenInfo.username || (tokenInfo.user && tokenInfo.user.username);
195
+ logger.log(` User: ${username ? chalk.cyan(username) : chalk.gray('—')}`);
196
+ }
197
+
188
198
  if (tokenInfo.appName) {
189
199
  logger.log(` Application: ${chalk.cyan(tokenInfo.appName)}`);
190
200
  }
@@ -7,6 +7,7 @@
7
7
  'use strict';
8
8
 
9
9
  const logger = require('../utils/logger');
10
+ const { writeJsonEnvelopeAndExit } = require('../utils/cli-json-exit');
10
11
  const { formatBlockingError } = require('../utils/cli-test-layout-chalk');
11
12
  const { runDatasourceLoad } = require('../datasource/bulk-loader-service');
12
13
  const { runDatasourceExport } = require('../datasource/datasource-exporter-service');
@@ -82,7 +83,9 @@ async function datasourceLoadAction(datasourceKey, options) {
82
83
 
83
84
  if (options.json) {
84
85
  const { context: _ctx, ...payload } = result;
85
- logger.log(JSON.stringify(payload, null, 2));
86
+ // exitCode 0 — write and resolve, never exit here; this action always
87
+ // exits unconditionally below with the real computed code.
88
+ await writeJsonEnvelopeAndExit(payload, 0);
86
89
  } else {
87
90
  displayDatasourceLoadTTY(result, { environment: options.env, verbose: options.verbose });
88
91
  }
@@ -112,7 +115,9 @@ async function datasourceExportAction(datasourceKey, options) {
112
115
 
113
116
  if (options.json) {
114
117
  const { context: _ctx, ...payload } = result;
115
- logger.log(JSON.stringify(payload, null, 2));
118
+ // exitCode 0 — write and resolve, never exit here; this action always
119
+ // exits unconditionally below with the real computed code.
120
+ await writeJsonEnvelopeAndExit(payload, 0);
116
121
  } else {
117
122
  displayDatasourceExportTTY(result, { environment: options.env, verbose: options.verbose });
118
123
  }
@@ -7,6 +7,7 @@
7
7
  'use strict';
8
8
 
9
9
  const logger = require('../utils/logger');
10
+ const { writeJsonEnvelopeAndExit } = require('../utils/cli-json-exit');
10
11
  const { formatBlockingError } = require('../utils/cli-test-layout-chalk');
11
12
  const { runDatasourceAgentTrust } = require('../datasource/agent-trust-run');
12
13
  const { writeTrustDebugLogAndPrint } = require('../datasource/agent-trust-debug-log');
@@ -52,7 +53,10 @@ async function finalizeDatasourceVerifyTrust(datasourceKey, options, result) {
52
53
  strict: options.strict === true
53
54
  };
54
55
  if (options.json) {
55
- logger.log(JSON.stringify(result.trustRun, null, 2));
56
+ // exitCode 0 here means "write and resolve, never exit" — the real exit
57
+ // code is decided below and the caller exits unconditionally, even on
58
+ // success, so process.exit() must stay solely the caller's job.
59
+ await writeJsonEnvelopeAndExit(result.trustRun, 0);
56
60
  } else if (options.summary) {
57
61
  const tr = result.trustRun;
58
62
  logger.log(
@@ -6,6 +6,7 @@
6
6
 
7
7
  const logger = require('../utils/logger');
8
8
  const { handleCommandError } = require('../utils/cli-utils');
9
+ const { writeJsonEnvelopeAndExit } = require('../utils/cli-json-exit');
9
10
  const { runGovernanceGenerateForExternalSystem } = require('./governance-generate-external');
10
11
  const { runGovernanceVerifyForExternalSystem } = require('./governance-verify-external');
11
12
  const { runGovernanceUploadForExternalSystem } = require('./governance-upload-external');
@@ -75,12 +76,12 @@ async function governanceVerifyHandler(systemKey, options, cmd) {
75
76
  try {
76
77
  const opts = normalizeGovernanceOptions(options, cmd);
77
78
  const result = await runGovernanceVerifyForExternalSystem(systemKey, opts);
79
+ const exitCode = exitCodeFromVerdict(result.verdict);
78
80
  if (opts.json) {
79
- logger.log(JSON.stringify(buildJsonEnvelope(result, opts.verbose), null, 2));
80
- } else {
81
- displayVerifyGovernanceTTY(result.systemKey, result, { details: opts.verbose });
81
+ await writeJsonEnvelopeAndExit(buildJsonEnvelope(result, opts.verbose), exitCode);
82
+ return;
82
83
  }
83
- const exitCode = exitCodeFromVerdict(result.verdict);
84
+ displayVerifyGovernanceTTY(result.systemKey, result, { details: opts.verbose });
84
85
  if (exitCode !== 0) {
85
86
  process.exit(exitCode);
86
87
  }
@@ -35,7 +35,12 @@ const ROLE_LIST_COLUMNS = [
35
35
  label: 'Groups',
36
36
  width: 44,
37
37
  value: (r) => {
38
- const groups = Array.isArray(r.groups) ? r.groups.filter(Boolean) : [];
38
+ const groups = Array.isArray(r.groups)
39
+ ? r.groups.filter(Boolean)
40
+ : (r.mappings || [])
41
+ .filter((mapping) => mapping?.isActive !== false)
42
+ .map((mapping) => mapping?.groupName)
43
+ .filter(Boolean);
39
44
  return groups.length > 0 ? groups.join(', ') : '—';
40
45
  }
41
46
  }
@@ -6,8 +6,8 @@
6
6
 
7
7
  'use strict';
8
8
 
9
- const logger = require('../utils/logger');
10
9
  const { handleCommandError } = require('../utils/cli-utils');
10
+ const { writeJsonEnvelopeAndExit } = require('../utils/cli-json-exit');
11
11
  const { cliOptsSkipPublish } = require('../utils/cli-sync-options');
12
12
  const { runLifecycleForExternalSystem } = require('./lifecycle-external');
13
13
  const { displayLifecycleReportTTY } = require('../lifecycle/report-display');
@@ -65,6 +65,12 @@ async function runLifecycleCommandAction(appName, options, cmd) {
65
65
  const result = await runLifecycleForExternalSystem(appName, opts);
66
66
  const report = result.report;
67
67
 
68
+ const pillarVerdict = lifecycleVerdictFromReport(report);
69
+ const exitCode =
70
+ pillarVerdict === VERDICT.VERIFIED
71
+ ? 0
72
+ : exitCodeFromVerdict(pillarVerdict, { partial: pillarVerdict === VERDICT.NOT_VERIFIED });
73
+
68
74
  if (opts.json) {
69
75
  const envelope = {
70
76
  systemKey: result.systemKey,
@@ -82,22 +88,17 @@ async function runLifecycleCommandAction(appName, options, cmd) {
82
88
  if (opts.verbose) {
83
89
  envelope.datasources = report.datasources;
84
90
  }
85
- logger.log(JSON.stringify(envelope, null, 2));
86
- } else {
87
- displayLifecycleReportTTY(result.systemKey, report, {
88
- details: opts.verbose,
89
- openApiPublished: result.openApiPublished === true,
90
- certificationProgress: result.certificationProgress ?? null,
91
- dataplaneUrl: result.dataplaneUrl ?? null,
92
- dataplaneUiBaseUrl: result.dataplaneUiBaseUrl ?? null
93
- });
91
+ await writeJsonEnvelopeAndExit(envelope, exitCode);
92
+ return;
94
93
  }
95
94
 
96
- const pillarVerdict = lifecycleVerdictFromReport(report);
97
- const exitCode =
98
- pillarVerdict === VERDICT.VERIFIED
99
- ? 0
100
- : exitCodeFromVerdict(pillarVerdict, { partial: pillarVerdict === VERDICT.NOT_VERIFIED });
95
+ displayLifecycleReportTTY(result.systemKey, report, {
96
+ details: opts.verbose,
97
+ openApiPublished: result.openApiPublished === true,
98
+ certificationProgress: result.certificationProgress ?? null,
99
+ dataplaneUrl: result.dataplaneUrl ?? null,
100
+ dataplaneUiBaseUrl: result.dataplaneUiBaseUrl ?? null
101
+ });
101
102
  if (exitCode !== 0) {
102
103
  process.exit(exitCode);
103
104
  }
@@ -15,6 +15,7 @@ const { getToken } = require('../api/auth.api');
15
15
  const { formatApiError } = require('../utils/api-error-handler');
16
16
  const { loadClientCredentials } = require('../utils/token-manager');
17
17
  const logger = require('../utils/logger');
18
+ const { writeStreamAndWait } = require('../utils/write-stream-and-wait');
18
19
 
19
20
  /**
20
21
  * Tries to load credentials from secrets file
@@ -136,7 +137,7 @@ function handleLoginError(response, appName) {
136
137
  * @param {Object} response - API response
137
138
  * @returns {Object} Token data object
138
139
  */
139
- function extractTokenData(response) {
140
+ async function extractTokenData(response) {
140
141
  // OpenAPI spec response: { success: boolean, token: string, expiresIn: number, expiresAt: string, ... }
141
142
  // Handle both flat and nested response structures (some APIs wrap in data field)
142
143
  // If response.data exists, use it; otherwise use response directly
@@ -146,7 +147,10 @@ function extractTokenData(response) {
146
147
  if (!responseData || !responseData.token) {
147
148
  logger.error(formatBlockingError('Invalid response: missing token'));
148
149
  if (responseData) {
149
- logger.error(chalk.gray(`Response structure: ${JSON.stringify(responseData, null, 2)}`));
150
+ await writeStreamAndWait(
151
+ process.stderr,
152
+ `${chalk.gray(`Response structure: ${JSON.stringify(responseData, null, 2)}`)}\n`
153
+ );
150
154
  }
151
155
  process.exit(1);
152
156
  }
@@ -190,9 +194,10 @@ async function handleCredentialsLogin(controllerUrl, appName, clientId, clientSe
190
194
 
191
195
  if (!response.success) {
192
196
  handleLoginError(response, appName);
197
+ return;
193
198
  }
194
199
 
195
- const responseData = extractTokenData(response);
200
+ const responseData = await extractTokenData(response);
196
201
  const expiresAt = calculateTokenExpiration(responseData);
197
202
 
198
203
  return {
@@ -15,6 +15,7 @@ const ora = require('ora');
15
15
  const { setCurrentEnvironment, saveDeviceToken, setControllerUrl } = require('../core/config');
16
16
  const { initiateDeviceCodeFlow } = require('../api/auth.api');
17
17
  const { pollDeviceCodeToken, displayDeviceCodeInfo } = require('../utils/api');
18
+ const { resolveDeviceVisitUrl } = require('../utils/device-code');
18
19
  const logger = require('../utils/logger');
19
20
  const { formatSuccessLine } = require('../utils/cli-layout-chalk');
20
21
 
@@ -42,6 +43,10 @@ async function getEnvironmentKey(environment) {
42
43
  return envKey;
43
44
  }
44
45
 
46
+ if (!process.stdin || process.stdin.isTTY === false) {
47
+ return 'dev';
48
+ }
49
+
45
50
  const envPrompt = await inquirer.prompt([{
46
51
  type: 'input',
47
52
  name: 'environment',
@@ -255,10 +260,8 @@ function logDeviceFlowError(deviceError, opts) {
255
260
  }
256
261
 
257
262
  function printCompactDeviceUrl(deviceCodeResponse) {
258
- const visitUrl =
259
- deviceCodeResponse.verification_uri_complete ||
260
- `${deviceCodeResponse.verification_uri}?user_code=${deviceCodeResponse.user_code}`;
261
- logger.log('\nOpen browser to authenticate:');
263
+ const visitUrl = resolveDeviceVisitUrl(deviceCodeResponse);
264
+ logger.log('\nOpen this URL to authenticate:');
262
265
  logger.log(chalk.yellow(`${visitUrl}\n`));
263
266
  }
264
267
 
@@ -289,7 +292,13 @@ async function handleDeviceCodeLogin(controllerUrl, environment, online, scope,
289
292
  const deviceCodeResponse = convertDeviceCodeResponse(apiResponse);
290
293
 
291
294
  if (!opts.compact) {
292
- displayDeviceCodeInfo(deviceCodeResponse.user_code, deviceCodeResponse.verification_uri, logger, chalk);
295
+ displayDeviceCodeInfo(
296
+ deviceCodeResponse.user_code,
297
+ deviceCodeResponse.verification_uri,
298
+ logger,
299
+ chalk,
300
+ deviceCodeResponse.verification_uri_complete
301
+ );
293
302
  } else {
294
303
  printCompactDeviceUrl(deviceCodeResponse);
295
304
  }
@@ -16,9 +16,13 @@ const { setCurrentEnvironment, saveClientToken, setControllerUrl } = require('..
16
16
  const logger = require('../utils/logger');
17
17
  const { handleCredentialsLogin } = require('./login-credentials');
18
18
  const { handleDeviceCodeLogin } = require('./login-device');
19
- const { resolveControllerUrl } = require('../utils/controller-url');
19
+ const { resolveControllerUrl, discoverControllerBaseUrl } = require('../utils/controller-url');
20
20
  const { tryRefreshDataplaneVersionAfterLogin } = require('./auth-status-dataplane-version');
21
21
 
22
+ function isInteractiveStdin() {
23
+ return Boolean(process.stdin && process.stdin.isTTY);
24
+ }
25
+
22
26
  /**
23
27
  * Determine and validate authentication method
24
28
  * @async
@@ -34,6 +38,10 @@ async function determineAuthMethod(method) {
34
38
  return method;
35
39
  }
36
40
 
41
+ if (!isInteractiveStdin()) {
42
+ return 'device';
43
+ }
44
+
37
45
  const authMethod = await inquirer.prompt([{
38
46
  type: 'list',
39
47
  name: 'method',
@@ -86,6 +94,8 @@ async function normalizeControllerUrl(options) {
86
94
  controllerUrl = await resolveControllerUrl();
87
95
  }
88
96
  controllerUrl = String(controllerUrl).trim().replace(/\/+$/, '');
97
+ const inputUrl = controllerUrl;
98
+ controllerUrl = await discoverControllerBaseUrl(controllerUrl);
89
99
  if (!options.skipControllerUrlPersist) {
90
100
  const persistUrl = options.tokenControllerUrl
91
101
  ? String(options.tokenControllerUrl).trim().replace(/\/+$/, '')
@@ -93,6 +103,9 @@ async function normalizeControllerUrl(options) {
93
103
  await setControllerUrl(persistUrl);
94
104
  }
95
105
  if (!options.compact) {
106
+ if (controllerUrl !== inputUrl) {
107
+ logger.log(chalk.gray(`Using controller ${controllerUrl}`));
108
+ }
96
109
  logger.log(chalk.gray(`Controller URL: ${controllerUrl}`));
97
110
  }
98
111
  return controllerUrl;
@@ -132,11 +132,54 @@ async function clearClientTokens(options) {
132
132
  return 0;
133
133
  }
134
134
 
135
+ function normalizeTokenType(value) {
136
+ const type = value && String(value).toLowerCase();
137
+ if (type && type !== 'device' && type !== 'client') {
138
+ throw new Error('--type must be either "device" or "client"');
139
+ }
140
+ return type;
141
+ }
142
+
143
+ function validateLogoutOptionCombination(options, type) {
144
+ if (options.app && !options.environment) {
145
+ throw new Error('--app requires --environment option');
146
+ }
147
+ if (type === 'client' && options.controller) {
148
+ throw new Error('--controller can only be used with --type device');
149
+ }
150
+ if (type === 'device' && (options.environment || options.app)) {
151
+ throw new Error('--environment and --app can only be used with --type client');
152
+ }
153
+ }
154
+
155
+ function validateLogoutSelectors(options) {
156
+ if ('controller' in options && options.controller !== undefined && options.controller !== null) {
157
+ validateControllerUrl(options.controller);
158
+ }
159
+ if ('environment' in options && options.environment !== undefined && options.environment !== null) {
160
+ validateEnvironmentKey(options.environment);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Validate logout option combinations and normalize the token type.
166
+ * @param {Object} options - Raw logout options
167
+ * @returns {Object} Validated options with a normalized type
168
+ * @throws {Error} If an option or option combination is invalid
169
+ */
170
+ function normalizeLogoutOptions(options) {
171
+ const type = normalizeTokenType(options.type);
172
+ validateLogoutOptionCombination(options, type);
173
+ validateLogoutSelectors(options);
174
+ return { ...options, type };
175
+ }
176
+
135
177
  /**
136
178
  * Handle logout command action
137
179
  * @async
138
180
  * @function handleLogout
139
181
  * @param {Object} options - Logout options
182
+ * @param {string} [options.type] - Token type to clear (device or client)
140
183
  * @param {string} [options.controller] - Clear device tokens for specific controller
141
184
  * @param {string} [options.environment] - Clear client tokens for specific environment
142
185
  * @param {string} [options.app] - Clear client tokens for specific app (requires --environment)
@@ -147,24 +190,17 @@ async function handleLogout(options) {
147
190
  const configPath = CONFIG_FILE;
148
191
  logger.log(chalk.blue('\n🔓 Clearing authentication tokens...\n'));
149
192
 
150
- // Validate options
151
- if (options.app && !options.environment) {
152
- throw new Error('--app requires --environment option');
153
- }
154
-
155
- if ('controller' in options && options.controller !== undefined && options.controller !== null) {
156
- validateControllerUrl(options.controller);
157
- }
158
-
159
- if ('environment' in options && options.environment !== undefined && options.environment !== null) {
160
- validateEnvironmentKey(options.environment);
161
- }
193
+ const normalizedOptions = normalizeLogoutOptions(options);
162
194
 
163
195
  // Clear device tokens
164
- const deviceTokensCleared = await clearDeviceTokens(options);
196
+ const deviceTokensCleared = normalizedOptions.type === 'client'
197
+ ? 0
198
+ : await clearDeviceTokens(normalizedOptions);
165
199
 
166
200
  // Clear client tokens
167
- const clientTokensCleared = await clearClientTokens(options);
201
+ const clientTokensCleared = normalizedOptions.type === 'device'
202
+ ? 0
203
+ : await clearClientTokens(normalizedOptions);
168
204
 
169
205
  // Summary
170
206
  const totalCleared = deviceTokensCleared + clientTokensCleared;
@@ -178,4 +214,3 @@ async function handleLogout(options) {
178
214
  }
179
215
 
180
216
  module.exports = { handleLogout };
181
-
@@ -64,7 +64,8 @@ Subcommands:
64
64
  copy <roleKey> Capture one Work Hub execution as a local YAML test case
65
65
 
66
66
  Package workspace (top-level commands — same digital-worker package):
67
- download / validate / upload / deploy <roleKey> Manage role-assistant/ra-<roleKey>/
67
+ download / validate / upload ra-<roleKey> Manage role-assistant/ra-<roleKey>/
68
+ upload publishes settings and sidecars; operator Make available is a separate action
68
69
 
69
70
  Examples:
70
71
  $ aifabrix role-assistant test customer-support-bot
@@ -72,11 +73,11 @@ Examples:
72
73
  $ aifabrix role-assistant test customer-support-bot --list-cases
73
74
  $ aifabrix role-assistant test customer-support-bot --env tst --json
74
75
  $ aifabrix role-assistant copy customer-support-bot --suite 001 --reference a9f09fb8-d9eb-477f-8bb4-59e670fc82a4
75
- $ aifabrix upload customer-support-bot --env dev
76
+ $ aifabrix upload ra-customer-support-bot --env dev
76
77
  $ aifabrix test ra-customer-support-bot
77
78
 
78
79
  Notes:
79
- - Local packages live under role-assistant/ra-<roleKey>/ (bare <roleKey> in CLI args).
80
+ - role-assistant test/copy use bare <roleKey>; top-level package commands use ra-<roleKey>.
80
81
  - test uses the public/running worker by default; pass --publish (or upload) to refresh from local files.
81
82
  - copy writes tests/<suite>/<caseId>.yaml from a Work Hub execution id; it does not publish.
82
83
  - See: aifabrix role-assistant test -h
@@ -267,19 +268,30 @@ async function runRoleAssistantTestAction(roleKey, options, cmd) {
267
268
  const portalUrl = buildExternalSystemUiUrl(result.dataplaneUiBaseUrl, systemKey);
268
269
  const caseResult = await resolveSelectedCaseResults(systemKey, opts, result, verdict, portalUrl);
269
270
 
271
+ const exitCode = computeRoleAssistantExitCode(verdict, caseResult);
272
+
270
273
  if (opts.json) {
271
- logger.log(
272
- JSON.stringify(
273
- buildRoleAssistantTestEnvelope(displayKey, verdict, portalUrl, result, opts, caseResult),
274
- null,
275
- 2
276
- )
274
+ // Large piped stdout writes on Linux are non-blocking; exiting right
275
+ // after console.log() can truncate the JSON before the OS write
276
+ // completes. Wait for the write to flush before exiting non-zero.
277
+ const payload = JSON.stringify(
278
+ logger.sanitizeValue(
279
+ buildRoleAssistantTestEnvelope(displayKey, verdict, portalUrl, result, opts, caseResult)
280
+ ),
281
+ null,
282
+ 2
277
283
  );
278
- } else {
279
- displayRoleAssistantTestSummary(displayKey, verdict, portalUrl, result, caseResult);
284
+ return new Promise((resolve) => {
285
+ process.stdout.write(`${payload}\n`, () => {
286
+ if (exitCode !== 0) {
287
+ process.exit(exitCode);
288
+ }
289
+ resolve(exitCode);
290
+ });
291
+ });
280
292
  }
281
293
 
282
- const exitCode = computeRoleAssistantExitCode(verdict, caseResult);
294
+ displayRoleAssistantTestSummary(displayKey, verdict, portalUrl, result, caseResult);
283
295
  if (exitCode !== 0) {
284
296
  process.exit(exitCode);
285
297
  }
@@ -6,6 +6,7 @@
6
6
 
7
7
  const logger = require('../utils/logger');
8
8
  const { handleCommandError } = require('../utils/cli-utils');
9
+ const { writeJsonEnvelopeAndExit } = require('../utils/cli-json-exit');
9
10
  const { runVerifyGovernanceCombined } = require('./verify-governance-combined-external');
10
11
  const { normalizeTestGovernanceCliOptions } = require('./test-governance-cli-options');
11
12
  const { displayVerifyGovernanceTTY } = require('../lifecycle/report-display');
@@ -33,16 +34,17 @@ async function runVerifyGovernanceCommandAction(appName, options, cmd) {
33
34
  const opts = normalizeVerifyGovernanceCliOptions(options, cmd);
34
35
  const result = await runVerifyGovernanceCombined(appName, opts);
35
36
 
37
+ const exitCode = exitCodeFromVerdict(result.verdict);
38
+
36
39
  if (opts.json) {
37
- logger.log(JSON.stringify(buildJsonEnvelope(result, opts.verbose), null, 2));
38
- } else {
39
- if (opts.verbose && !opts.pack && typeof result.generated === 'number' && result.generated > 0) {
40
- logger.log(formatSuccessLine(`Generated packs for ${result.generated} datasource(s)`));
41
- }
42
- displayVerifyGovernanceTTY(result.systemKey, result, { details: opts.verbose });
40
+ await writeJsonEnvelopeAndExit(buildJsonEnvelope(result, opts.verbose), exitCode);
41
+ return;
43
42
  }
44
43
 
45
- const exitCode = exitCodeFromVerdict(result.verdict);
44
+ if (opts.verbose && !opts.pack && typeof result.generated === 'number' && result.generated > 0) {
45
+ logger.log(formatSuccessLine(`Generated packs for ${result.generated} datasource(s)`));
46
+ }
47
+ displayVerifyGovernanceTTY(result.systemKey, result, { details: opts.verbose });
46
48
  if (exitCode !== 0) {
47
49
  process.exit(exitCode);
48
50
  }
@@ -6,8 +6,8 @@
6
6
 
7
7
  'use strict';
8
8
 
9
- const logger = require('../utils/logger');
10
9
  const { handleCommandError } = require('../utils/cli-utils');
10
+ const { writeJsonEnvelopeAndExit } = require('../utils/cli-json-exit');
11
11
  const { cliOptsSkipPublish } = require('../utils/cli-sync-options');
12
12
  const { runVerifyOperationsForExternalSystem } = require('./verify-operations-external');
13
13
  const { displayVerifyOperationsTTY } = require('../lifecycle/report-display');
@@ -37,16 +37,17 @@ async function runVerifyOperationsCommandAction(appName, options, cmd) {
37
37
  const opts = normalizeVerifyOperationsCliOptions(options, cmd);
38
38
  const result = await runVerifyOperationsForExternalSystem(appName, opts);
39
39
 
40
+ const exitCode = exitCodeFromVerdict(result.verdict);
41
+
40
42
  if (opts.json) {
41
- logger.log(JSON.stringify(buildJsonEnvelope(result, opts.verbose), null, 2));
42
- } else {
43
- displayVerifyOperationsTTY(appName, result, {
44
- details: opts.verbose,
45
- warnings: result.warnings
46
- });
43
+ await writeJsonEnvelopeAndExit(buildJsonEnvelope(result, opts.verbose), exitCode);
44
+ return;
47
45
  }
48
46
 
49
- const exitCode = exitCodeFromVerdict(result.verdict);
47
+ displayVerifyOperationsTTY(appName, result, {
48
+ details: opts.verbose,
49
+ warnings: result.warnings
50
+ });
50
51
  if (exitCode !== 0) {
51
52
  process.exit(exitCode);
52
53
  }
@@ -6,8 +6,8 @@
6
6
 
7
7
  'use strict';
8
8
 
9
- const logger = require('../utils/logger');
10
9
  const { handleCommandError } = require('../utils/cli-utils');
10
+ const { writeJsonEnvelopeAndExit } = require('../utils/cli-json-exit');
11
11
  const { runVerifyTrustForExternalSystem } = require('./verify-trust-external');
12
12
  const { normalizeTestTrustCliOptions } = require('./test-trust-cli-options');
13
13
  const { displayVerifyTrustTTY } = require('../lifecycle/report-display');
@@ -30,6 +30,8 @@ async function runVerifyTrustCommandAction(appName, options, cmd) {
30
30
  timeout: opts.timeout
31
31
  });
32
32
 
33
+ const exitCode = exitCodeFromVerdict(result.verdict);
34
+
33
35
  if (opts.json) {
34
36
  const envelope = buildJsonEnvelope(
35
37
  {
@@ -44,12 +46,11 @@ async function runVerifyTrustCommandAction(appName, options, cmd) {
44
46
  },
45
47
  opts.details || opts.verbose
46
48
  );
47
- logger.log(JSON.stringify(envelope, null, 2));
48
- } else {
49
- displayVerifyTrustTTY(result.systemKey, result, { details: opts.verbose || opts.details });
49
+ await writeJsonEnvelopeAndExit(envelope, exitCode);
50
+ return;
50
51
  }
51
52
 
52
- const exitCode = exitCodeFromVerdict(result.verdict);
53
+ displayVerifyTrustTTY(result.systemKey, result, { details: opts.verbose || opts.details });
53
54
  if (exitCode !== 0) {
54
55
  process.exit(exitCode);
55
56
  }
@@ -74,9 +74,8 @@ async function setupAuthenticationAndDataplane(systemKey, _options, _config) {
74
74
  }
75
75
 
76
76
  const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
77
- logger.log(formatProgress('Resolving dataplane URL…'));
77
+ // discoverDataplaneUrl already logs progress and success
78
78
  const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
79
- logger.log(formatSuccessLine(`Dataplane URL: ${dataplaneUrl}`));
80
79
 
81
80
  return { authConfig, dataplaneUrl };
82
81
  }
@@ -234,6 +234,18 @@ function determineVariableLocation(value, key) {
234
234
  required = true;
235
235
  }
236
236
 
237
+ if (location === 'keyvault' && value.startsWith('kv://')) {
238
+ try {
239
+ const { getInfraParameterCatalog } = require('../parameters/infra-parameter-catalog');
240
+ const entry = getInfraParameterCatalog().findEntryForKey(value.slice('kv://'.length));
241
+ if (entry?.scope === 'infra' && entry.generator?.type === 'emptyAllowed') {
242
+ required = false;
243
+ }
244
+ } catch {
245
+ // Keep the conservative required=true default when the catalog is unavailable.
246
+ }
247
+ }
248
+
237
249
  return { location, required };
238
250
  }
239
251
 
@@ -344,4 +356,3 @@ module.exports = {
344
356
  validatePortalInput,
345
357
  parseEnvironmentVariables
346
358
  };
347
-
@@ -52,12 +52,12 @@ function setRecordFetch(op) {
52
52
  let found = false;
53
53
  for (const step of steps) {
54
54
  if (step && step.fetch && typeof step.fetch === 'object') {
55
- step.fetch.source = 'record';
55
+ step.fetch = { source: 'record' };
56
56
  found = true;
57
57
  }
58
58
  }
59
59
  if (!found) {
60
- steps.unshift({ fetch: { source: 'record', query: {} } });
60
+ steps.unshift({ fetch: { source: 'record' } });
61
61
  if (!steps.some((row) => row && row.output)) {
62
62
  steps.push({ output: { mode: 'records' } });
63
63
  }
@@ -87,6 +87,9 @@ function ensureRecordCapability(body, capKey) {
87
87
  if (!ops[capKey] || typeof ops[capKey] !== 'object') {
88
88
  ops[capKey] = { enabled: true, description: `Local ${capKey}`, steps: [] };
89
89
  }
90
+ if (capKey === 'create' || capKey === 'update') {
91
+ ops[capKey].shape = capKey;
92
+ }
90
93
  setRecordFetch(/** @type {Record<string, unknown>} */ (ops[capKey]));
91
94
  }
92
95