@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.
- package/lib/agent-kit/host-approve.js +48 -6
- package/lib/agent-kit/init.js +2 -2
- package/lib/agent-kit/remote-prompts.js +46 -13
- package/lib/agent-kit/run.js +1 -1
- package/lib/agent-kit/start.js +310 -0
- package/lib/api/pipeline.api.js +32 -3
- package/lib/app/deploy.js +13 -9
- package/lib/app/offer-create-from-template.js +184 -0
- package/lib/cli/setup-app.js +15 -7
- package/lib/cli/setup-auth.js +11 -2
- package/lib/cli/setup-external-system-help.js +15 -3
- package/lib/cli/setup-external-system.js +21 -3
- package/lib/commands/agent-kit-help.js +74 -4
- package/lib/commands/agent-kit.js +83 -12
- package/lib/commands/auth-status-user.js +99 -0
- package/lib/commands/auth-status.js +14 -4
- package/lib/commands/datasource-load-export-cli.js +7 -2
- package/lib/commands/datasource-test-trust-cli.js +5 -1
- package/lib/commands/governance-command-action.js +5 -4
- package/lib/commands/identity-membership-role-cache.js +6 -1
- package/lib/commands/lifecycle-command-action.js +16 -15
- package/lib/commands/login-credentials.js +8 -3
- package/lib/commands/login-device.js +14 -5
- package/lib/commands/login.js +14 -1
- package/lib/commands/logout.js +50 -15
- package/lib/commands/repair-datasource-auto-rbac.js +4 -9
- package/lib/commands/role-assistant.js +24 -12
- package/lib/commands/upload-openapi-sync-followup.js +59 -0
- package/lib/commands/upload-qualification-client.js +131 -0
- package/lib/commands/upload-qualification-followups.js +104 -0
- package/lib/commands/upload-qualification-poll.js +143 -0
- package/lib/commands/upload.js +37 -156
- package/lib/commands/verify-governance-command-action.js +9 -7
- package/lib/commands/verify-operations-command-action.js +9 -8
- package/lib/commands/verify-trust-command-action.js +6 -5
- package/lib/deployment/deploy-poll-ui.js +14 -10
- package/lib/deployment/deployer-status.js +4 -4
- package/lib/external-system/deploy.js +46 -51
- package/lib/external-system/download.js +1 -2
- package/lib/generator/helpers.js +14 -46
- package/lib/generator/index.js +0 -4
- package/lib/integration-definition/apply-static-enterprise.js +5 -2
- package/lib/programmatic/builder-api-request.js +8 -4
- package/lib/programmatic/capability-registry.js +2 -0
- package/lib/programmatic/get-deploy-status.js +45 -0
- package/lib/programmatic/get-upload-status.js +45 -0
- package/lib/programmatic/http-accepted-response.js +31 -0
- package/lib/programmatic/openapi-descriptions.js +14 -4
- package/lib/programmatic/request-timeout.js +21 -15
- package/lib/programmatic/route-handler-map.js +6 -0
- package/lib/programmatic/run-deploy.js +7 -3
- package/lib/programmatic/run-upload.js +11 -7
- package/lib/role-assistant/knowledge-sync-settings-push.js +172 -0
- package/lib/role-assistant/knowledge-sync.js +9 -126
- package/lib/role-assistant/test-cases-evaluate.js +12 -1
- package/lib/role-assistant/test-cases-evidence.js +44 -5
- package/lib/role-assistant/test-cases-lifecycle.js +9 -3
- package/lib/role-assistant/test-runner.js +3 -0
- package/lib/schema/infra-parameter.schema.json +51 -3
- package/lib/schema/infra.parameter.yaml +482 -47
- package/lib/schema/type/document-storage.json +1 -1
- package/lib/utils/api-required-permissions.js +271 -0
- package/lib/utils/api.js +16 -13
- package/lib/utils/cli-json-exit.js +41 -0
- package/lib/utils/cli-utils.js +26 -7
- package/lib/utils/controller-url.js +78 -4
- package/lib/utils/deployment-errors.js +15 -0
- package/lib/utils/deployment-validation-helpers.js +4 -1
- package/lib/utils/device-code.js +31 -10
- package/lib/utils/error-formatters/error-parser.js +16 -1
- package/lib/utils/error-formatters/permission-errors.js +19 -5
- package/lib/utils/keycloak-realm-ready.js +5 -2
- package/lib/utils/logger.js +1 -0
- package/lib/utils/paths.js +26 -5
- package/lib/utils/qualification-poll-ui.js +136 -0
- package/lib/utils/secrets-helpers.js +2 -2
- package/lib/utils/write-stream-and-wait.js +63 -0
- package/package.json +22 -7
- package/templates/agent-kit/agent-kit.yaml +1 -1
- package/templates/agent-kit/instructions/AGENTKIT.md +25 -4
- package/templates/agent-kit/instructions/root.AGENTS.md +1 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +1 -1
- package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +2 -1
- package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +1 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +1 -0
- package/templates/agent-kit/skills/shared/hosts.md +13 -26
- package/templates/agent-kit/skills/shared/login.md +16 -0
- package/templates/agent-kit/workspace/README.md +1 -1
- package/templates/applications/builder-api/env.template +4 -0
- package/templates/applications/dataplane/application.yaml +1 -2
- package/templates/applications/dataplane/env.template +5 -5
- package/templates/applications/keycloak/Dockerfile +12 -5
- package/templates/applications/keycloak/application.yaml +2 -2
- package/templates/applications/keycloak/cimd-lenient-parse/AbstractClientIdMetadataDocumentExecutor.java +3 -3
- package/templates/applications/keycloak/cimd-lenient-parse/SOURCE.md +6 -6
- package/templates/applications/keycloak/env.template +13 -1
- package/templates/applications/miso-controller/application.yaml +21 -2
- package/templates/applications/miso-controller/env.template +97 -28
- package/templates/applications/miso-controller/rbac.yaml +5 -0
- 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
|
-
|
|
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
|
package/lib/agent-kit/init.js
CHANGED
|
@@ -152,7 +152,7 @@ function runInit(options) {
|
|
|
152
152
|
dryRun,
|
|
153
153
|
targetAbs,
|
|
154
154
|
host: options.host || 'all',
|
|
155
|
-
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:
|
|
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
|
-
*
|
|
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
|
|
209
|
-
if (!
|
|
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
|
|
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
|
|
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 = {
|
package/lib/agent-kit/run.js
CHANGED
|
@@ -46,7 +46,7 @@ function runManagedCommand(options, mode) {
|
|
|
46
46
|
const approvals = writeApproveAllFiles({
|
|
47
47
|
repoRoot,
|
|
48
48
|
hosts,
|
|
49
|
-
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
|
+
};
|
package/lib/api/pipeline.api.js
CHANGED
|
@@ -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
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|