@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
@@ -10,9 +10,50 @@ const fs = require('../internal/fs-real-sync');
10
10
 
11
11
  const CLAUDE_REL = '.claude/settings.local.json';
12
12
  const CODEX_REL = '.codex/config.toml';
13
+ const APPROVE_HOSTS = ['claude', 'codex'];
13
14
  const IGNORE_COMMENT = '# Host auto-approve (machine-local; aifabrix agent-kit)';
14
15
  const IGNORE_LINES = [CLAUDE_REL, CODEX_REL];
15
16
 
17
+ /**
18
+ * --approve-all [host]: true/'all' = every selected host; claude|codex = one tool.
19
+ * @param {unknown} value
20
+ * @returns {{ requested: boolean, target: string|null }}
21
+ */
22
+ function parseApproveAll(value) {
23
+ if (value === undefined || value === false || value === '') {
24
+ return { requested: false, target: null };
25
+ }
26
+ if (value === true || value === 'all') {
27
+ return { requested: true, target: 'all' };
28
+ }
29
+ const target = String(value);
30
+ if (APPROVE_HOSTS.includes(target)) {
31
+ return { requested: true, target };
32
+ }
33
+ throw new Error('Invalid --approve-all. Use all, claude, or codex.');
34
+ }
35
+
36
+ /**
37
+ * Subset of managed hosts that receive auto-approve files.
38
+ * @param {string[]} [managedHosts]
39
+ * @param {unknown} approveAll
40
+ * @returns {string[]}
41
+ */
42
+ function resolveApproveHosts(managedHosts, approveAll) {
43
+ const parsed = parseApproveAll(approveAll);
44
+ if (!parsed.requested) {
45
+ return [];
46
+ }
47
+ const eligible = (managedHosts || []).filter((h) => APPROVE_HOSTS.includes(h));
48
+ if (parsed.target === 'all') {
49
+ return eligible;
50
+ }
51
+ if (!eligible.includes(parsed.target)) {
52
+ throw new Error(`--approve-all ${parsed.target} requires --host ${parsed.target} or --host all.`);
53
+ }
54
+ return [parsed.target];
55
+ }
56
+
16
57
  /**
17
58
  * This workspace for writes. Read(~/**) is home-anchored. Bash(aifabrix *)
18
59
  * allows the delivery CLI without granting every shell command.
@@ -101,15 +142,13 @@ function listApproveWrites(options) {
101
142
  * @returns {{ requested: boolean, files: { path: string, action: string }[] }}
102
143
  */
103
144
  function writeApproveAllFiles(options) {
104
- if (!options.approveAll) {
145
+ const parsed = parseApproveAll(options.approveAll);
146
+ if (!parsed.requested) {
105
147
  return { requested: false, files: [] };
106
148
  }
149
+ const hosts = resolveApproveHosts(options.hosts, options.approveAll);
107
150
  const fsApi = options.fs || fs;
108
- const ctx = {
109
- repoRoot: options.repoRoot,
110
- hosts: options.hosts || [],
111
- fsApi
112
- };
151
+ const ctx = { repoRoot: options.repoRoot, hosts, fsApi };
113
152
  const dryRun = Boolean(options.dryRun);
114
153
  const force = Boolean(options.force);
115
154
  const files = listApproveWrites(ctx).map((row) =>
@@ -124,6 +163,9 @@ function writeApproveAllFiles(options) {
124
163
  module.exports = {
125
164
  CLAUDE_REL,
126
165
  CODEX_REL,
166
+ APPROVE_HOSTS,
167
+ parseApproveAll,
168
+ resolveApproveHosts,
127
169
  buildClaudeSettings,
128
170
  buildCodexConfig,
129
171
  writeApproveAllFiles
@@ -152,7 +152,7 @@ function runInit(options) {
152
152
  dryRun,
153
153
  targetAbs,
154
154
  host: options.host || 'all',
155
- approveAll: Boolean(options.approveAll),
155
+ approveAll: options.approveAll,
156
156
  force: Boolean(options.force),
157
157
  gitResult,
158
158
  phases
@@ -195,7 +195,7 @@ function installKitPhase(p) {
195
195
  const kit = runInstall({
196
196
  cwd: p.targetAbs,
197
197
  host: p.host,
198
- approveAll: Boolean(p.approveAll),
198
+ approveAll: p.approveAll,
199
199
  force: Boolean(p.force)
200
200
  });
201
201
  p.phases.push({ phase: 'agent-kit', state: kit.state });
@@ -7,6 +7,7 @@
7
7
 
8
8
  const { execFileSync } = require('child_process');
9
9
  const { ALL_HOSTS, HOST_LABELS, normalizeHosts } = require('./hosts');
10
+ const { APPROVE_HOSTS, parseApproveAll } = require('./host-approve');
10
11
 
11
12
  /**
12
13
  * @param {{ execFileSync?: Function }} [options]
@@ -198,18 +199,57 @@ async function enrichInitRemoteOptions(options, deps = {}) {
198
199
  }
199
200
 
200
201
  /**
201
- * TTY asks; --approve-all / --yes / non-TTY auto-approve; --skip-approve-all skips.
202
+ * @param {object} inquirer
203
+ * @param {string[]} eligible
204
+ * @returns {Promise<true|false|string>}
205
+ */
206
+ async function askApproveAllChoice(inquirer, eligible) {
207
+ if (eligible.length === 1) {
208
+ const host = eligible[0];
209
+ const { approveAll } = await inquirer.prompt([
210
+ {
211
+ type: 'confirm',
212
+ name: 'approveAll',
213
+ message: `Auto-approve ${HOST_LABELS[host] || host} tool permissions in this workspace?`,
214
+ default: true
215
+ }
216
+ ]);
217
+ return Boolean(approveAll);
218
+ }
219
+ const { approveAll } = await inquirer.prompt([
220
+ {
221
+ type: 'list',
222
+ name: 'approveAll',
223
+ message: 'Which coding host should auto-approve tools in this workspace?',
224
+ default: 'all',
225
+ choices: [
226
+ { name: 'Both Claude Code and Codex', value: 'all' },
227
+ { name: 'Claude Code only', value: 'claude' },
228
+ { name: 'Codex only', value: 'codex' },
229
+ { name: 'Skip (the coding app will ask each time)', value: 'skip' }
230
+ ]
231
+ }
232
+ ]);
233
+ return approveAll === 'skip' ? false : approveAll;
234
+ }
235
+
236
+ /**
237
+ * TTY asks; --approve-all [host] / --yes / non-TTY auto-approve; --skip-approve-all skips.
202
238
  * @param {object} options
203
239
  * @param {{ interactive?: boolean, inquirer?: object, ask?: boolean }} [deps]
204
240
  * @returns {Promise<object>}
205
241
  */
206
242
  async function enrichApproveAllOptions(options, deps = {}) {
207
243
  const hosts = normalizeHosts(options.host);
208
- const needsHost = hosts.includes('claude') || hosts.includes('codex');
209
- if (!needsHost || options.skipApproveAll) {
244
+ const eligible = hosts.filter((h) => APPROVE_HOSTS.includes(h));
245
+ if (!eligible.length || options.skipApproveAll) {
210
246
  return { ...options, approveAll: false };
211
247
  }
212
- if (options.approveAll || options.yes) {
248
+ if (options.approveAll) {
249
+ parseApproveAll(options.approveAll);
250
+ return options;
251
+ }
252
+ if (options.yes) {
213
253
  return { ...options, approveAll: true };
214
254
  }
215
255
  if (deps.ask === false) {
@@ -219,15 +259,8 @@ async function enrichApproveAllOptions(options, deps = {}) {
219
259
  return { ...options, approveAll: true };
220
260
  }
221
261
  const inquirer = deps.inquirer || require('inquirer');
222
- const { approveAll } = await inquirer.prompt([
223
- {
224
- type: 'confirm',
225
- name: 'approveAll',
226
- message: 'Auto-approve all Claude Code and Codex tool permissions in this workspace?',
227
- default: true
228
- }
229
- ]);
230
- return { ...options, approveAll: Boolean(approveAll) };
262
+ const approveAll = await askApproveAllChoice(inquirer, eligible);
263
+ return { ...options, approveAll };
231
264
  }
232
265
 
233
266
  module.exports = {
@@ -46,7 +46,7 @@ function runManagedCommand(options, mode) {
46
46
  const approvals = writeApproveAllFiles({
47
47
  repoRoot,
48
48
  hosts,
49
- approveAll: Boolean(options.approveAll),
49
+ approveAll: options.approveAll,
50
50
  dryRun: Boolean(options.dryRun),
51
51
  force: Boolean(options.force)
52
52
  });
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Start Claude Code or Codex in a named tmux session.
3
+ * @fileoverview Does not install Agent Kit skills
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const os = require('os');
9
+ const path = require('path');
10
+ const { execFileSync, spawnSync } = require('child_process');
11
+ const logger = require('../utils/logger');
12
+ const { resolveGitRoot } = require('./repo-guard');
13
+ const { writeApproveAllFiles } = require('./host-approve');
14
+
15
+ const START_APPS = {
16
+ claude: { bin: 'claude' },
17
+ codex: { bin: 'codex' }
18
+ };
19
+
20
+ /**
21
+ * @param {unknown} value
22
+ * @returns {string}
23
+ */
24
+ function normalizeStartApp(value) {
25
+ const app = String(value || '').trim();
26
+ if (!START_APPS[app]) {
27
+ throw new Error('Invalid app. Use claude or codex.');
28
+ }
29
+ return app;
30
+ }
31
+
32
+ /**
33
+ * Session names may not contain colons or periods.
34
+ * @param {unknown} value
35
+ * @returns {string}
36
+ */
37
+ function sanitizeSessionName(value) {
38
+ const name = String(value || '')
39
+ .trim()
40
+ .replace(/[.:\s]+/g, '-')
41
+ .replace(/-+/g, '-')
42
+ .replace(/^-|-$/g, '');
43
+ if (!name) {
44
+ throw new Error(
45
+ 'Invalid --name. Use the last folder of this git workspace, for example aifabrix-docs.'
46
+ );
47
+ }
48
+ return name;
49
+ }
50
+
51
+ /**
52
+ * Default display name is the last folder of the git workspace.
53
+ * @param {unknown} explicit
54
+ * @param {string} cwd
55
+ * @returns {string}
56
+ */
57
+ function resolveSessionName(explicit, cwd) {
58
+ const raw = explicit === undefined || explicit === null || explicit === ''
59
+ ? path.basename(cwd)
60
+ : explicit;
61
+ return sanitizeSessionName(raw);
62
+ }
63
+
64
+ /**
65
+ * @param {object} options
66
+ * @returns {string}
67
+ */
68
+ function currentUsername(options) {
69
+ if (options && options.username) {
70
+ return String(options.username);
71
+ }
72
+ try {
73
+ return os.userInfo().username;
74
+ } catch {
75
+ return process.env.USER || process.env.LOGNAME || 'user';
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Hostname + login, e.g. builder01-dev03.
81
+ * @param {object} [options]
82
+ * @returns {string}
83
+ */
84
+ function hostUserPrefix(options) {
85
+ const hostRaw = options && options.hostname ? options.hostname : os.hostname();
86
+ const host = String(hostRaw).split('.')[0];
87
+ return sanitizeSessionName(`${host}-${currentUsername(options || {})}`);
88
+ }
89
+
90
+ /**
91
+ * Remote Control session: builder01-dev03-aifabrix-docs.
92
+ * @param {string} prefix
93
+ * @param {string} session
94
+ * @returns {string}
95
+ */
96
+ function resolveRemoteControlName(prefix, session) {
97
+ return sanitizeSessionName(`${prefix}-${session}`);
98
+ }
99
+
100
+ /**
101
+ * @param {string} bin
102
+ * @param {Function} exec
103
+ * @returns {boolean}
104
+ */
105
+ function hasOnPath(bin, exec) {
106
+ try {
107
+ exec('which', [bin], {
108
+ encoding: 'utf8',
109
+ stdio: ['ignore', 'pipe', 'pipe']
110
+ });
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * @param {object} options
119
+ * @returns {void}
120
+ */
121
+ function validateStartOptions(options) {
122
+ const app = String(options.app || '').trim();
123
+ if (options.remoteControl && app !== 'claude') {
124
+ throw new Error('--remote-control is only valid with claude.');
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Claude argv after the binary (Remote Control names, not Claude --tmux).
130
+ * @param {object} p
131
+ * @returns {string[]}
132
+ */
133
+ function claudeInnerArgs(p) {
134
+ const args = ['--name', p.session];
135
+ if (!p.remoteControl) {
136
+ return args;
137
+ }
138
+ return args.concat([
139
+ '--remote-control',
140
+ p.remoteControlName,
141
+ '--remote-control-session-name-prefix',
142
+ p.remoteControlPrefix
143
+ ]);
144
+ }
145
+
146
+ /**
147
+ * @param {object} p
148
+ * @returns {object}
149
+ */
150
+ function buildTmuxSpec(p) {
151
+ const spec = START_APPS[p.app];
152
+ const extra = p.app === 'claude' ? claudeInnerArgs(p) : [];
153
+ const mode = p.detach
154
+ ? ['new-session', '-d', '-s', p.session, '-c', p.cwd]
155
+ : ['new-session', '-A', '-s', p.session, '-c', p.cwd];
156
+ const remote = p.app === 'claude' && p.remoteControl;
157
+ return {
158
+ bin: 'tmux',
159
+ args: [...mode, spec.bin, ...extra],
160
+ session: p.session,
161
+ appBin: spec.bin,
162
+ needTmux: true,
163
+ remoteControlName: remote ? p.remoteControlName : undefined,
164
+ remoteControlPrefix: remote ? p.remoteControlPrefix : undefined
165
+ };
166
+ }
167
+
168
+ /**
169
+ * @param {object} p
170
+ * @returns {object}
171
+ */
172
+ function buildLaunchSpec(p) {
173
+ return buildTmuxSpec(p);
174
+ }
175
+
176
+ /**
177
+ * @param {{ appBin: string, needTmux?: boolean }} spec
178
+ * @param {Function} exec
179
+ * @returns {void}
180
+ */
181
+ function requireBinaries(spec, exec) {
182
+ if (spec.needTmux && !hasOnPath('tmux', exec)) {
183
+ throw new Error('tmux is not on PATH. Install tmux, then retry.');
184
+ }
185
+ if (hasOnPath(spec.appBin, exec)) {
186
+ return;
187
+ }
188
+ if (spec.appBin === 'claude') {
189
+ throw new Error(
190
+ 'claude is not on PATH. Install with: curl -fsSL https://claude.ai/install.sh | bash'
191
+ );
192
+ }
193
+ throw new Error('codex is not on PATH. Install with: npm install -g @openai/codex');
194
+ }
195
+
196
+ /**
197
+ * @param {object} options
198
+ * @returns {boolean}
199
+ */
200
+ function shouldExecute(options) {
201
+ if (options.dryRun || options.json) {
202
+ return false;
203
+ }
204
+ if (options.detach) {
205
+ return true;
206
+ }
207
+ if (!process.stdout.isTTY) {
208
+ throw new Error('agent-kit start needs a TTY to attach. Pass --detach or --dry-run.');
209
+ }
210
+ return true;
211
+ }
212
+
213
+ /**
214
+ * @param {object} options
215
+ * @param {{ bin: string, args: string[], cwd: string }} spec
216
+ * @returns {unknown}
217
+ */
218
+ function spawnLaunch(options, spec) {
219
+ const spawn = options.spawnSync || spawnSync;
220
+ const result = spawn(spec.bin, spec.args, { stdio: 'inherit', cwd: spec.cwd });
221
+ const status = result && typeof result.status === 'number' ? result.status : 0;
222
+ if (status !== 0) {
223
+ throw new Error(`${spec.bin} exited with status ${status}`);
224
+ }
225
+ return status;
226
+ }
227
+
228
+ /**
229
+ * @param {object} spec
230
+ * @param {object} extra
231
+ * @returns {object}
232
+ */
233
+ function toStartResult(spec, extra) {
234
+ const launch = [spec.bin, ...spec.args];
235
+ return {
236
+ command: 'start',
237
+ app: extra.app,
238
+ session: spec.session,
239
+ remoteControlName: spec.remoteControlName,
240
+ remoteControlPrefix: spec.remoteControlPrefix,
241
+ path: extra.cwd,
242
+ launch,
243
+ tmux: spec.needTmux ? launch : undefined,
244
+ approvals: extra.approvals,
245
+ dryRun: extra.dryRun,
246
+ detach: extra.detach,
247
+ executed: extra.executed
248
+ };
249
+ }
250
+
251
+ /**
252
+ * @param {object} options
253
+ * @returns {object}
254
+ */
255
+ function runStart(options) {
256
+ validateStartOptions(options);
257
+ const app = normalizeStartApp(options.app);
258
+ const cwd = resolveGitRoot(options.cwd || process.cwd());
259
+ const session = resolveSessionName(options.name, cwd);
260
+ const remoteControlPrefix = hostUserPrefix(options);
261
+ const remoteControlName = resolveRemoteControlName(remoteControlPrefix, session);
262
+ const exec = options.execFileSync || execFileSync;
263
+ const spec = buildLaunchSpec({
264
+ app,
265
+ cwd,
266
+ session,
267
+ remoteControlPrefix,
268
+ remoteControlName,
269
+ detach: Boolean(options.detach),
270
+ remoteControl: Boolean(options.remoteControl)
271
+ });
272
+ spec.cwd = cwd;
273
+ const approveAll = !options.skipApproveAll && Boolean(options.approveAll || options.yes);
274
+ const approvals = writeApproveAllFiles({
275
+ repoRoot: cwd,
276
+ hosts: [app],
277
+ approveAll,
278
+ dryRun: Boolean(options.dryRun)
279
+ });
280
+ if (!options.dryRun) {
281
+ requireBinaries(spec, exec);
282
+ }
283
+ const execute = shouldExecute(options);
284
+ if (execute && !options.json) {
285
+ logger.log(`Starting ${app} session ${spec.session}`);
286
+ }
287
+ if (execute) {
288
+ spawnLaunch(options, spec);
289
+ }
290
+ return toStartResult(spec, {
291
+ app,
292
+ cwd,
293
+ approvals,
294
+ dryRun: Boolean(options.dryRun),
295
+ detach: Boolean(options.detach),
296
+ executed: execute
297
+ });
298
+ }
299
+
300
+ module.exports = {
301
+ START_APPS,
302
+ normalizeStartApp,
303
+ sanitizeSessionName,
304
+ resolveSessionName,
305
+ hostUserPrefix,
306
+ resolveRemoteControlName,
307
+ claudeInnerArgs,
308
+ buildTmuxSpec,
309
+ runStart
310
+ };
@@ -278,7 +278,10 @@ function resolvePipelineUploadTimeoutMs(raw) {
278
278
  * @param {Object} authConfig
279
279
  * @param {Object} payload
280
280
  * @param {Object} [options]
281
- * @param {number|string} [options.timeoutMs] - HTTP + dataplane publish budget (default 300000)
281
+ * @param {number|string} [options.timeoutMs] - HTTP + dataplane publish budget (default 300000); ignored when options.async is true.
282
+ * @param {boolean} [options.async=false] - When true, durably accept the upload for qualification (202 jobId/candidateFingerprint) instead of blocking for the ladder (plan 191.0 task 1).
283
+ * @param {string} [options.gate] - Optional comma-separated checks: operations, trust, governance, full.
284
+ * @param {string} [options.subjectEmail] - Optional comma-separated governance subject emails.
282
285
  */
283
286
  async function uploadApplicationViaPipeline(
284
287
  dataplaneUrl,
@@ -287,8 +290,17 @@ async function uploadApplicationViaPipeline(
287
290
  options = {}
288
291
  ) {
289
292
  const body = { ...payload, status: payload.status ?? 'draft' };
290
- const timeoutMs = resolvePipelineUploadTimeoutMs(options.timeoutMs);
291
293
  const client = createDataplaneApiClient(dataplaneUrl, authConfig);
294
+ if (options.async === true) {
295
+ const params = { async: 'true' };
296
+ if (options.gate) params.gate = options.gate;
297
+ if (options.subjectEmail) params.subjectEmail = options.subjectEmail;
298
+ return await client.post('/api/v1/pipeline/upload', {
299
+ body,
300
+ params
301
+ });
302
+ }
303
+ const timeoutMs = resolvePipelineUploadTimeoutMs(options.timeoutMs);
292
304
  return await client.post('/api/v1/pipeline/upload', {
293
305
  body,
294
306
  timeoutMs,
@@ -297,6 +309,23 @@ async function uploadApplicationViaPipeline(
297
309
  });
298
310
  }
299
311
 
312
+ /**
313
+ * Get durable publish/qualification job status.
314
+ * GET /api/v1/publish-jobs/{jobId}
315
+ * @requiresPermission {Dataplane} external-system:publish or external-system:read
316
+ * @async
317
+ * @function getPublishJobStatus
318
+ * @param {string} dataplaneUrl - Dataplane base URL
319
+ * @param {Object} authConfig - Authentication configuration
320
+ * @param {string} jobId - Publish/qualification job id returned by uploadApplicationViaPipeline({ async: true })
321
+ * @returns {Promise<Object>} API envelope `{ success, data }` where `data` is `{ jobId, state, targetType, targetKey, steps, error, result }`
322
+ * @throws {Error} If request fails
323
+ */
324
+ async function getPublishJobStatus(dataplaneUrl, authConfig, jobId) {
325
+ const client = createDataplaneApiClient(dataplaneUrl, authConfig);
326
+ return await client.get(`/api/v1/publish-jobs/${encodeURIComponent(jobId)}`);
327
+ }
328
+
300
329
  module.exports = {
301
330
  validatePipeline,
302
331
  deployPipeline,
@@ -307,7 +336,7 @@ module.exports = {
307
336
  testSystemViaPipeline,
308
337
  testDatasourceViaPipeline,
309
338
  uploadApplicationViaPipeline,
339
+ getPublishJobStatus,
310
340
  resolvePipelineUploadTimeoutMs,
311
341
  PIPELINE_UPLOAD_TIMEOUT_MS
312
342
  };
313
-
package/lib/app/deploy.js CHANGED
@@ -23,6 +23,7 @@ const pushUtils = require('../deployment/push');
23
23
  const SEP = chalk.gray('────────────────────────────────────────');
24
24
  const logger = require('../utils/logger');
25
25
  const { detectAppType, getBuilderPath, getIntegrationPath } = require('../utils/paths');
26
+ const { offerCreateFromTemplateIfMissing } = require('./offer-create-from-template');
26
27
  const { checkApplicationExists } = require('../utils/app-existence');
27
28
  const { loadDeploymentConfig } = require('./deploy-config');
28
29
  const { logOfflinePathWhenType } = require('../utils/cli-utils');
@@ -307,10 +308,12 @@ function validateImageIsPullable(imageRef, appName) {
307
308
  */
308
309
  function throwIfDeploymentFailed(status, statusObj) {
309
310
  if (status !== 'failed' && status !== 'cancelled') return;
310
- const msg =
311
- statusObj.message ||
312
- statusObj.error ||
313
- (status === 'cancelled' ? 'Deployment cancelled' : 'Deployment failed');
311
+ const reportedErrors = Array.isArray(statusObj.errors) ? statusObj.errors.filter(Boolean).join('; ') : '';
312
+ // A failed deployment can retain its last progress message (for example
313
+ // "Database-only mode"). Prefer the terminal error so the CLI reports the cause.
314
+ const msg = status === 'failed'
315
+ ? statusObj.error || reportedErrors || statusObj.message || 'Deployment failed'
316
+ : statusObj.message || statusObj.error || reportedErrors || 'Deployment cancelled';
314
317
  const err = new Error(`Deployment ${status}: ${msg}`);
315
318
  err.formatted = `Deployment ${status}.\n\n${msg}`;
316
319
  err.deploymentStatus = statusObj;
@@ -462,7 +465,7 @@ async function executeDeployPipeline(appName, options) {
462
465
  * @param {boolean} [options.poll] - Poll for deployment status
463
466
  * @param {number} [options.pollInterval] - Polling interval in milliseconds
464
467
  * @param {number} [options.pollMaxAttempts] - Max polling attempts
465
- * @returns {Promise<{ result: Object, usedExternalDeploy: boolean }>} Deployment result and whether external deploy was used
468
+ * @returns {Promise<{ result: Object, usedExternalDeploy: boolean, appKey: string }>} Deployment result and whether external deploy was used
466
469
  * @throws {Error} If deployment fails
467
470
  *
468
471
  * Controller and environment come from config.yaml (set via aifabrix login or aifabrix auth config).
@@ -473,16 +476,18 @@ async function executeDeployPipeline(appName, options) {
473
476
  async function deployApp(appName, options = {}) {
474
477
  let controllerUrl = null;
475
478
  let usedExternalDeploy = false;
479
+ let resolvedName = appName;
476
480
 
477
481
  try {
478
482
  assertDeployNotProtectionScope(appName);
479
483
  validateAppName(appName);
480
- const pipeline = await executeDeployPipeline(appName, options);
484
+ resolvedName = await offerCreateFromTemplateIfMissing(appName, options);
485
+ const pipeline = await executeDeployPipeline(resolvedName, options);
481
486
  controllerUrl = pipeline.controllerUrl ?? null;
482
487
  usedExternalDeploy = pipeline.usedExternalDeploy;
483
- return { result: pipeline.result, usedExternalDeploy };
488
+ return { result: pipeline.result, usedExternalDeploy, appKey: resolvedName };
484
489
  } catch (error) {
485
- await handleDeploymentError(error, appName, controllerUrl, usedExternalDeploy);
490
+ await handleDeploymentError(error, resolvedName, controllerUrl, usedExternalDeploy);
486
491
  }
487
492
  }
488
493
 
@@ -492,4 +497,3 @@ module.exports = {
492
497
  validateAppName,
493
498
  validateImageIsPullable
494
499
  };
495
-