@aifabrix/builder 2.55.0 → 2.56.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 (101) 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/api/pipeline.api.js +32 -3
  7. package/lib/app/deploy.js +13 -9
  8. package/lib/app/offer-create-from-template.js +184 -0
  9. package/lib/cli/setup-app.js +15 -7
  10. package/lib/cli/setup-auth.js +11 -2
  11. package/lib/cli/setup-external-system-help.js +15 -3
  12. package/lib/cli/setup-external-system.js +21 -3
  13. package/lib/commands/agent-kit-help.js +74 -4
  14. package/lib/commands/agent-kit.js +83 -12
  15. package/lib/commands/auth-status-user.js +99 -0
  16. package/lib/commands/auth-status.js +14 -4
  17. package/lib/commands/datasource-load-export-cli.js +7 -2
  18. package/lib/commands/datasource-test-trust-cli.js +5 -1
  19. package/lib/commands/governance-command-action.js +5 -4
  20. package/lib/commands/identity-membership-role-cache.js +6 -1
  21. package/lib/commands/lifecycle-command-action.js +16 -15
  22. package/lib/commands/login-credentials.js +8 -3
  23. package/lib/commands/login-device.js +14 -5
  24. package/lib/commands/login.js +14 -1
  25. package/lib/commands/logout.js +50 -15
  26. package/lib/commands/repair-datasource-auto-rbac.js +4 -9
  27. package/lib/commands/role-assistant.js +24 -12
  28. package/lib/commands/upload-openapi-sync-followup.js +59 -0
  29. package/lib/commands/upload-qualification-client.js +131 -0
  30. package/lib/commands/upload-qualification-followups.js +104 -0
  31. package/lib/commands/upload-qualification-poll.js +143 -0
  32. package/lib/commands/upload.js +37 -156
  33. package/lib/commands/verify-governance-command-action.js +9 -7
  34. package/lib/commands/verify-operations-command-action.js +9 -8
  35. package/lib/commands/verify-trust-command-action.js +6 -5
  36. package/lib/deployment/deploy-poll-ui.js +14 -10
  37. package/lib/deployment/deployer-status.js +4 -4
  38. package/lib/external-system/deploy.js +46 -51
  39. package/lib/external-system/download.js +1 -2
  40. package/lib/generator/helpers.js +14 -46
  41. package/lib/generator/index.js +0 -4
  42. package/lib/integration-definition/apply-static-enterprise.js +5 -2
  43. package/lib/programmatic/builder-api-request.js +8 -4
  44. package/lib/programmatic/capability-registry.js +2 -0
  45. package/lib/programmatic/get-deploy-status.js +45 -0
  46. package/lib/programmatic/get-upload-status.js +45 -0
  47. package/lib/programmatic/http-accepted-response.js +31 -0
  48. package/lib/programmatic/openapi-descriptions.js +14 -4
  49. package/lib/programmatic/request-timeout.js +21 -15
  50. package/lib/programmatic/route-handler-map.js +6 -0
  51. package/lib/programmatic/run-deploy.js +7 -3
  52. package/lib/programmatic/run-upload.js +11 -7
  53. package/lib/role-assistant/knowledge-sync-settings-push.js +172 -0
  54. package/lib/role-assistant/knowledge-sync.js +9 -126
  55. package/lib/role-assistant/test-cases-evaluate.js +12 -1
  56. package/lib/role-assistant/test-cases-evidence.js +44 -5
  57. package/lib/role-assistant/test-cases-lifecycle.js +9 -3
  58. package/lib/role-assistant/test-runner.js +3 -0
  59. package/lib/schema/infra-parameter.schema.json +51 -3
  60. package/lib/schema/infra.parameter.yaml +482 -47
  61. package/lib/schema/type/document-storage.json +1 -1
  62. package/lib/utils/api-required-permissions.js +271 -0
  63. package/lib/utils/api.js +16 -13
  64. package/lib/utils/cli-json-exit.js +41 -0
  65. package/lib/utils/cli-utils.js +26 -7
  66. package/lib/utils/controller-url.js +78 -4
  67. package/lib/utils/deployment-errors.js +15 -0
  68. package/lib/utils/deployment-validation-helpers.js +4 -1
  69. package/lib/utils/device-code.js +31 -10
  70. package/lib/utils/error-formatters/error-parser.js +16 -1
  71. package/lib/utils/error-formatters/permission-errors.js +19 -5
  72. package/lib/utils/keycloak-realm-ready.js +5 -2
  73. package/lib/utils/logger.js +1 -0
  74. package/lib/utils/paths.js +26 -5
  75. package/lib/utils/qualification-poll-ui.js +136 -0
  76. package/lib/utils/secrets-helpers.js +2 -2
  77. package/lib/utils/write-stream-and-wait.js +63 -0
  78. package/package.json +22 -7
  79. package/templates/agent-kit/agent-kit.yaml +1 -1
  80. package/templates/agent-kit/instructions/AGENTKIT.md +25 -4
  81. package/templates/agent-kit/instructions/root.AGENTS.md +1 -1
  82. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
  83. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +1 -1
  84. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +2 -1
  85. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +1 -0
  86. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +1 -0
  87. package/templates/agent-kit/skills/shared/hosts.md +13 -26
  88. package/templates/agent-kit/skills/shared/login.md +16 -0
  89. package/templates/agent-kit/workspace/README.md +1 -1
  90. package/templates/applications/builder-api/env.template +4 -0
  91. package/templates/applications/dataplane/application.yaml +1 -2
  92. package/templates/applications/dataplane/env.template +5 -5
  93. package/templates/applications/keycloak/Dockerfile +12 -5
  94. package/templates/applications/keycloak/application.yaml +2 -2
  95. package/templates/applications/keycloak/cimd-lenient-parse/AbstractClientIdMetadataDocumentExecutor.java +3 -3
  96. package/templates/applications/keycloak/cimd-lenient-parse/SOURCE.md +6 -6
  97. package/templates/applications/keycloak/env.template +13 -1
  98. package/templates/applications/miso-controller/application.yaml +21 -2
  99. package/templates/applications/miso-controller/env.template +97 -28
  100. package/templates/applications/miso-controller/rbac.yaml +5 -0
  101. package/lib/generator/deploy-manifest-azure-kv.js +0 -81
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `aifabrix agent-kit` command group.
3
- * @fileoverview Install, init, status, update, validate, remote create
3
+ * @fileoverview Install, init, status, update, validate, remote create, start
4
4
  */
5
5
 
6
6
  'use strict';
@@ -9,6 +9,7 @@ const logger = require('../utils/logger');
9
9
  const { handleCommandError } = require('../utils/cli-utils');
10
10
  const { formatSuccessLine, formatBlockingError, formatWarningLine } = require('../utils/cli-layout-chalk');
11
11
  const { runInstall, runUpdate } = require('../agent-kit/run');
12
+ const { runStart } = require('../agent-kit/start');
12
13
  const path = require('path');
13
14
  const { runInit, runRemoteCreate } = require('../agent-kit/init');
14
15
  const { enrichInitHostOptions, enrichInitRemoteOptions, enrichRemoteOptions, enrichApproveAllOptions } = require('../agent-kit/remote-prompts');
@@ -26,7 +27,11 @@ const {
26
27
  UPDATE_HELP,
27
28
  VALIDATE_HELP,
28
29
  INIT_HELP,
29
- REMOTE_HELP
30
+ REMOTE_GROUP_HELP,
31
+ REMOTE_HELP,
32
+ START_HELP,
33
+ HOST_OPTION_HELP,
34
+ APPROVE_ALL_OPTION_HELP
30
35
  } = require('./agent-kit-help');
31
36
 
32
37
  /**
@@ -91,6 +96,34 @@ function emitRemoteReport(json, asJson) {
91
96
  logger.log(formatSuccessLine(json.dryRun ? 'Dry-run complete' : 'Done'));
92
97
  }
93
98
 
99
+ /**
100
+ * @param {object} json
101
+ * @param {boolean} asJson
102
+ * @returns {void}
103
+ */
104
+ function emitStartReport(json, asJson) {
105
+ if (asJson) {
106
+ logger.log(JSON.stringify(json, null, 2));
107
+ return;
108
+ }
109
+ logger.log(`App ${json.app}`);
110
+ logger.log(`Session ${json.session}`);
111
+ if (json.remoteControlName) {
112
+ logger.log(`Remote ${json.remoteControlName}`);
113
+ }
114
+ logger.log(`Path ${json.path}`);
115
+ logger.log(`Launch ${json.launch.join(' ')}`);
116
+ if (json.executed && json.detach) {
117
+ logger.log(formatSuccessLine('Session started in background'));
118
+ return;
119
+ }
120
+ if (json.executed) {
121
+ logger.log(formatSuccessLine('Session ended'));
122
+ return;
123
+ }
124
+ logger.log(formatSuccessLine(json.dryRun ? 'Dry-run complete' : 'Ready'));
125
+ }
126
+
94
127
  /**
95
128
  * @param {Function} runner
96
129
  * @param {object} options
@@ -125,6 +158,7 @@ function setupAgentKitCommands(program) {
125
158
  addUpdateCommand(group);
126
159
  addValidateCommand(group);
127
160
  addRemoteCommand(group);
161
+ addStartCommand(group);
128
162
  }
129
163
 
130
164
  /**
@@ -134,16 +168,16 @@ function addInitCommand(group) {
134
168
  group
135
169
  .command('init [nameOrPath]')
136
170
  .description('Create a customer workspace skeleton, git repo, and Agent Kit')
137
- .option('--host <target>', 'all, cursor, codex, or claude (omit on TTY to choose)', 'all')
171
+ .option('--host <target>', HOST_OPTION_HELP, 'all')
138
172
  .option('--scm <kind>', 'git or none', 'git')
139
- .option('--remote', 'Create private hosting remote and push (requires auth)')
173
+ .option('--remote', 'Create private GitHub remote and push (requires auth)')
140
174
  .option('--no-remote', 'Force local-only (skip remote prompt on TTY)')
141
175
  .option('--org <owner>', 'Hosting org or user (required with --remote when non-TTY)')
142
176
  .option('--visibility <mode>', 'private or public', 'private')
143
177
  .option('--license <kind>', 'mit or none', 'none')
144
178
  .option('--force', 'Allow non-empty target directory')
145
179
  .option('--yes', 'Silent: skip TTY confirm (pair with --host and --org); also auto-approves host permissions')
146
- .option('--approve-all', 'Write Claude Code and Codex auto-approve configs')
180
+ .option('--approve-all [host]', APPROVE_ALL_OPTION_HELP)
147
181
  .option('--skip-approve-all', 'Do not write Claude Code or Codex auto-approve configs')
148
182
  .option('--dry-run', 'Plan writes without changing files')
149
183
  .option('--json', 'Machine-readable output')
@@ -177,10 +211,13 @@ function addInitCommand(group) {
177
211
  * @param {import('commander').Command} group
178
212
  */
179
213
  function addRemoteCommand(group) {
180
- const remote = group.command('remote').description('Hosting remotes for an Agent Kit workspace');
214
+ const remote = group
215
+ .command('remote')
216
+ .description('Create a GitHub repository for this workspace and push')
217
+ .addHelpText('after', REMOTE_GROUP_HELP);
181
218
  remote
182
219
  .command('create')
183
- .description('Create a private GitHub remote and push the current workspace')
220
+ .description('Create a private GitHub repository (origin) and push this workspace')
184
221
  .option('--org <owner>', 'Hosting org or user (required when non-TTY)')
185
222
  .option('--name <repo>', 'Remote repository name (default: directory basename)')
186
223
  .option('--visibility <mode>', 'private or public', 'private')
@@ -215,8 +252,8 @@ function addInstallCommand(group) {
215
252
  group
216
253
  .command('install')
217
254
  .description('Install the Agent Kit into the current git repository')
218
- .option('--host <target>', 'all, cursor, codex, or claude', 'all')
219
- .option('--approve-all', 'Write Claude Code and Codex auto-approve configs')
255
+ .option('--host <target>', HOST_OPTION_HELP, 'all')
256
+ .option('--approve-all [host]', APPROVE_ALL_OPTION_HELP)
220
257
  .option('--skip-approve-all', 'Do not write Claude Code or Codex auto-approve configs')
221
258
  .option('--yes', 'Silent: auto-approve host permission configs without asking')
222
259
  .option('--dry-run', 'Plan writes without changing files')
@@ -225,7 +262,8 @@ function addInstallCommand(group) {
225
262
  .action(async(options) => {
226
263
  try {
227
264
  const interactive = Boolean(process.stdout.isTTY) && !options.json;
228
- const merged = await enrichApproveAllOptions(options, { interactive });
265
+ const withHost = await enrichInitHostOptions(options, { interactive });
266
+ const merged = await enrichApproveAllOptions(withHost, { interactive });
229
267
  await runLeaf(runInstall, merged, true);
230
268
  } catch (error) {
231
269
  handleCommandError(error, 'agent-kit install');
@@ -287,8 +325,8 @@ function addUpdateCommand(group) {
287
325
  group
288
326
  .command('update')
289
327
  .description('Update managed Agent Kit files from this Builder release')
290
- .option('--host <target>', 'all, cursor, codex, or claude (omit to keep recorded hosts)')
291
- .option('--approve-all', 'Write Claude Code and Codex auto-approve configs')
328
+ .option('--host <target>', `${HOST_OPTION_HELP} (omit to keep recorded hosts)`)
329
+ .option('--approve-all [host]', APPROVE_ALL_OPTION_HELP)
292
330
  .option('--skip-approve-all', 'Do not write Claude Code or Codex auto-approve configs')
293
331
  .option('--yes', 'Silent: auto-approve host permission configs without asking')
294
332
  .option('--dry-run', 'Plan writes without changing files')
@@ -343,4 +381,37 @@ async function runValidateLeaf(options) {
343
381
  }
344
382
  }
345
383
 
384
+ /**
385
+ * @param {import('commander').Command} group
386
+ */
387
+ function addStartCommand(group) {
388
+ group
389
+ .command('start <app>')
390
+ .description('Start Claude Code or Codex in tmux (does not install skills)')
391
+ .option('--approve-all', 'Write auto-approve config for this app only')
392
+ .option('--skip-approve-all', 'Do not write auto-approve configs')
393
+ .option('--yes', 'Same as --approve-all')
394
+ .option('--detach', 'Create the tmux session in the background (no attach)')
395
+ .option(
396
+ '--name <session>',
397
+ 'tmux session name (default: last folder of this git workspace)'
398
+ )
399
+ .option(
400
+ '--remote-control',
401
+ 'Claude Code only: same session on this PC, claude.ai/code, and the Claude mobile app'
402
+ )
403
+ .option('--dry-run', 'Print the launch command without starting it')
404
+ .option('--json', 'Machine-readable output')
405
+ .addHelpText('after', START_HELP)
406
+ .action(async(app, options) => {
407
+ try {
408
+ const json = runStart({ ...options, app });
409
+ emitStartReport(json, Boolean(options.json));
410
+ } catch (error) {
411
+ handleCommandError(error, 'agent-kit start');
412
+ process.exitCode = 1;
413
+ }
414
+ });
415
+ }
416
+
346
417
  module.exports = { setupAgentKitCommands };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Resolve the signed-in username for `aifabrix auth status`.
3
+ *
4
+ * @fileoverview Unwrap controller auth/user payloads and pick a display username
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { unwrapControllerData } = require('../identity/identity-response-helpers');
12
+
13
+ /**
14
+ * @param {...*} values
15
+ * @returns {string}
16
+ */
17
+ function firstNonEmptyString(...values) {
18
+ for (const value of values) {
19
+ if (typeof value === 'string' && value.trim()) {
20
+ return value.trim();
21
+ }
22
+ }
23
+ return '';
24
+ }
25
+
26
+ /**
27
+ * Decode a JWT payload for display only. Does not verify the signature.
28
+ * @param {string} token
29
+ * @returns {Object|null}
30
+ */
31
+ function decodeJwtPayload(token) {
32
+ if (!token || typeof token !== 'string') {
33
+ return null;
34
+ }
35
+ const parts = token.split('.');
36
+ if (parts.length < 2) {
37
+ return null;
38
+ }
39
+ try {
40
+ const b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
41
+ const padded = b64.padEnd(Math.ceil(b64.length / 4) * 4, '=');
42
+ const payload = JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
43
+ return payload && typeof payload === 'object' ? payload : null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * @param {Object|null|undefined} user
51
+ * @param {string} [token]
52
+ * @returns {string}
53
+ */
54
+ function resolveDisplayUsername(user, token) {
55
+ const fromUser = firstNonEmptyString(
56
+ user && user.username,
57
+ user && user.preferredUsername,
58
+ user && user.preferred_username,
59
+ user && user.email
60
+ );
61
+ if (fromUser) {
62
+ return fromUser;
63
+ }
64
+ const payload = decodeJwtPayload(token);
65
+ return firstNonEmptyString(
66
+ payload && payload.preferred_username,
67
+ payload && payload.preferredUsername,
68
+ payload && payload.email,
69
+ payload && payload.upn
70
+ );
71
+ }
72
+
73
+ /**
74
+ * @param {Object|null|undefined} response
75
+ * @param {string} [token]
76
+ * @returns {{ authenticated: boolean, user: Object|null, username: string }}
77
+ */
78
+ function parseAuthUserResponse(response, token) {
79
+ const payload = unwrapControllerData(response);
80
+ if (!payload || typeof payload !== 'object') {
81
+ return { authenticated: false, user: null, username: '' };
82
+ }
83
+ const rawUser = payload.user && typeof payload.user === 'object' ? payload.user : null;
84
+ const username = resolveDisplayUsername(rawUser, token);
85
+ const user = username && (!rawUser || !rawUser.username)
86
+ ? { ...(rawUser || {}), username }
87
+ : rawUser;
88
+ return {
89
+ authenticated: payload.authenticated !== false,
90
+ user,
91
+ username
92
+ };
93
+ }
94
+
95
+ module.exports = {
96
+ decodeJwtPayload,
97
+ resolveDisplayUsername,
98
+ parseAuthUserResponse
99
+ };
@@ -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;