@aifabrix/builder 2.58.0 → 2.60.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/codex-app-server.js +44 -0
- package/lib/agent-kit/git-identity.js +180 -0
- package/lib/agent-kit/init.js +4 -17
- package/lib/agent-kit/session-report.js +15 -10
- package/lib/agent-kit/setup-auth.js +131 -0
- package/lib/agent-kit/setup-context.js +118 -0
- package/lib/agent-kit/setup-github.js +70 -0
- package/lib/agent-kit/setup-managed.js +79 -0
- package/lib/agent-kit/setup-process.js +42 -0
- package/lib/agent-kit/setup-session.js +53 -0
- package/lib/agent-kit/setup-state.js +52 -0
- package/lib/agent-kit/setup-workspace.js +113 -0
- package/lib/agent-kit/setup.js +87 -0
- package/lib/agent-kit/start.js +48 -3
- package/lib/agent-kit/tmux-sessions.js +20 -3
- package/lib/api/dev.api.js +1 -1
- package/lib/api/miso-health.api.js +26 -0
- package/lib/api/role-assistant-test-job.api.js +60 -0
- package/lib/api/types/dev.types.js +3 -3
- package/lib/api/work-search.api.js +13 -6
- package/lib/app/deploy.js +8 -103
- package/lib/app/show-display.js +1 -0
- package/lib/app/show-online.js +15 -0
- package/lib/build/standard-docker-build.js +4 -1
- package/lib/cli/index.js +4 -0
- package/lib/cli/setup-app.js +14 -1
- package/lib/cli/setup-dev-path-commands.js +4 -0
- package/lib/cli/setup-dev.js +7 -4
- package/lib/cli/setup-help.js +86 -0
- package/lib/cli/setup-onboarding.js +37 -0
- package/lib/cli/setup-utility-repair.js +7 -5
- package/lib/cli/setup-utility.js +24 -1
- package/lib/commands/agent-kit-help.js +21 -7
- package/lib/commands/agent-kit-sessions.js +126 -0
- package/lib/commands/agent-kit-setup.js +44 -0
- package/lib/commands/agent-kit.js +5 -79
- package/lib/commands/dev-init.js +21 -23
- package/lib/commands/dev-refresh.js +2 -2
- package/lib/commands/governance-verify-external.js +12 -5
- package/lib/commands/repair-datasource.js +9 -9
- package/lib/commands/repair.js +45 -8
- package/lib/commands/role-assistant.js +7 -0
- package/lib/commands/upload-qualification-client.js +56 -11
- package/lib/commands/upload-qualification-poll.js +51 -22
- package/lib/commands/wizard-dataplane.js +16 -52
- package/lib/core/env-platform-expand.js +93 -0
- package/lib/core/secrets-env-content.js +37 -3
- package/lib/datasource/datasource-validate-summary.js +8 -5
- package/lib/datasource/sync-defaults.js +53 -0
- package/lib/deployment/installation/azure-infra-stage.js +13 -1
- package/lib/deployment/installation/azure-preflight.js +13 -2
- package/lib/deployment/installation/azure-readiness.js +24 -2
- package/lib/deployment/installation/environment-auth.js +60 -20
- package/lib/deployment/installation/environment-stage.js +9 -5
- package/lib/deployment/installation/infra-catalog.js +10 -7
- package/lib/deployment/installation/infra-manifest.js +5 -0
- package/lib/deployment/installation/registry-auth-mode.js +33 -10
- package/lib/deployment/marketplace-environment.js +26 -1
- package/lib/generator/builders.js +17 -0
- package/lib/generator/helpers.js +23 -2
- package/lib/generator/index.js +33 -18
- package/lib/integration-definition/apply.js +2 -2
- package/lib/internal/fs-real-sync.js +4 -1
- package/lib/programmatic/bearer-auth.js +3 -62
- package/lib/programmatic/builder-api-startup.js +51 -0
- package/lib/programmatic/builder-miso-runtime.js +167 -0
- package/lib/programmatic/openapi-descriptions.js +3 -3
- package/lib/programmatic/openapi-schema-descriptions-core.js +3 -3
- package/lib/programmatic/openapi-spec.js +2 -1
- package/lib/programmatic/route-handler-map.js +3 -2
- package/lib/role-assistant/test-cases-search.js +3 -0
- package/lib/role-assistant/test-job-runner.js +125 -0
- package/lib/role-assistant/test-runner-search.js +44 -2
- package/lib/schema/application-schema.json +193 -2
- package/lib/schema/environment-deploy-request.schema.json +1 -1
- package/lib/schema/external-datasource.schema.json +20 -5
- package/lib/schema/infra-parameter.schema.json +140 -34
- package/lib/schema/infra.parameter.yaml +30 -29
- package/lib/schema/infrastructure-schema.json +133 -14
- package/lib/schema/installation-environment.schema.json +1 -1
- package/lib/schema/wizard-config.schema.json +1 -1
- package/lib/utils/compose-generate-docker-compose.js +12 -0
- package/lib/utils/config-paths.js +24 -29
- package/lib/utils/config-registry-preference.js +13 -6
- package/lib/utils/dev-init-ssh-merge.js +5 -4
- package/lib/utils/dev-user-groups.js +2 -1
- package/lib/utils/docker-build.js +29 -8
- package/lib/utils/docker-daemon-tls-ca.js +2 -2
- package/lib/utils/docker-manifest-public-port.js +36 -0
- package/lib/utils/error-formatters/validation-errors.js +25 -5
- package/lib/utils/help-builder.js +4 -3
- package/lib/utils/platform-resolution.js +226 -0
- package/lib/utils/resolve-docker-image-ref.js +8 -3
- package/lib/utils/token-manager.js +29 -36
- package/package.json +4 -3
- package/templates/README.md +2 -1
- package/templates/agent-kit/agent-kit.yaml +4 -1
- package/templates/agent-kit/instructions/AGENTKIT.md +40 -3
- package/templates/agent-kit/instructions/root.AGENTS.md +42 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +33 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/scripts/delivery-verdict.js +176 -0
- package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -0
- package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +10 -3
- package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +22 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +21 -0
- package/templates/agent-kit/skills/shared/feedback.md +31 -0
- package/templates/agent-kit/skills/shared/hosts.md +35 -7
- package/templates/agent-kit/skills/shared/status.md +85 -0
- package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
- package/templates/agent-kit/workspace/README.md +14 -1
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/dataplane/application.yaml +30 -2
- package/templates/applications/dataplane/env.template +37 -4
- package/templates/applications/miso-controller/application.yaml +24 -1
- package/templates/applications/miso-controller/env.template +22 -35
- package/templates/external-system/external-datasource.yaml.hbs +7 -6
- package/templates/marketplace/main.json +281 -75
- package/templates/python/Dockerfile.hbs +2 -0
- package/templates/typescript/Dockerfile.hbs +2 -0
- package/templates/typescript/docker-compose.hbs +12 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Discover and stop direct Codex app servers owned by the current user. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
function parseServerLine(line) {
|
|
7
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
|
|
8
|
+
if (!match) return null;
|
|
9
|
+
const args = match[3];
|
|
10
|
+
if (!/(?:^|\s)(?:\S*\/)?codex(?:\s|$)/.test(args) ||
|
|
11
|
+
!/(?:^|\s)app-server\s+--listen(?:\s|$)/.test(args)) return null;
|
|
12
|
+
return { pid: Number(match[1]), parentPid: Number(match[2]) };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function listCodexAppServers(options = {}) {
|
|
16
|
+
if (typeof process.getuid !== 'function') return [];
|
|
17
|
+
const exec = options.execFileSync || execFileSync;
|
|
18
|
+
const output = exec('ps', ['-u', String(process.getuid()), '-ww', '-o', 'pid=,ppid=,args='], {
|
|
19
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
|
|
20
|
+
});
|
|
21
|
+
const candidates = String(output).split('\n').map(parseServerLine).filter(Boolean);
|
|
22
|
+
const pids = new Set(candidates.map(server => server.pid));
|
|
23
|
+
return candidates.filter(server => !pids.has(server.parentPid)).map((server) => {
|
|
24
|
+
try {
|
|
25
|
+
const startTime = String(exec('ps', ['-p', String(server.pid), '-o', 'lstart='], {
|
|
26
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']
|
|
27
|
+
})).trim();
|
|
28
|
+
return startTime ? { pid: server.pid, startTime } : null;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error.status === 1) return null;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}).filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function stopCodexAppServer(server, options = {}) {
|
|
37
|
+
const current = listCodexAppServers(options)
|
|
38
|
+
.find(item => item.pid === server.pid && item.startTime === server.startTime);
|
|
39
|
+
if (!current) throw new Error('Codex app server changed or exited. Run agent-kit list and retry.');
|
|
40
|
+
(options.kill || process.kill)(current.pid, 'SIGTERM');
|
|
41
|
+
return { command: 'kill', kind: 'codex-app-server', pid: current.pid, stopped: true };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { parseServerLine, listCodexAppServers, stopCodexAppServer };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git commit identity for onboarding and Agent Kit GitHub work.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order:
|
|
5
|
+
* 1. Existing `git config` — never rewritten, because it is the user's own setting.
|
|
6
|
+
* 2. The AI Fabrix login token's claims, offered as editable defaults.
|
|
7
|
+
* 3. The prompt answer, written to git config.
|
|
8
|
+
*
|
|
9
|
+
* The GitHub token is deliberately not used for the address: `gh api user` returns null whenever
|
|
10
|
+
* the account keeps its email private, and reading `user/emails` would demand the broader `user`
|
|
11
|
+
* scope for a value the login token already holds.
|
|
12
|
+
*
|
|
13
|
+
* @fileoverview Resolve and persist git user.name / user.email
|
|
14
|
+
* @author AI Fabrix Team
|
|
15
|
+
* @version 1.0.0
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const { execFileSync } = require('child_process');
|
|
21
|
+
const logger = require('../utils/logger');
|
|
22
|
+
|
|
23
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {Function} exec - execFileSync
|
|
27
|
+
* @param {string} key - Config key
|
|
28
|
+
* @param {boolean} global - Read the global scope only
|
|
29
|
+
* @returns {string} Trimmed value or ''
|
|
30
|
+
*/
|
|
31
|
+
function readConfig(exec, key, global) {
|
|
32
|
+
try {
|
|
33
|
+
const args = global ? ['config', '--global', '--get', key] : ['config', '--get', key];
|
|
34
|
+
return String(exec('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })).trim();
|
|
35
|
+
} catch {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Current identity, preferring any effective value (local or global) over none.
|
|
42
|
+
* @param {Object} [options] - `execFileSync`
|
|
43
|
+
* @returns {{ name: string, email: string, complete: boolean }}
|
|
44
|
+
*/
|
|
45
|
+
function readGitIdentity(options = {}) {
|
|
46
|
+
const exec = options.execFileSync || execFileSync;
|
|
47
|
+
const name = readConfig(exec, 'user.name', false);
|
|
48
|
+
const email = readConfig(exec, 'user.email', false);
|
|
49
|
+
return { name, email, complete: Boolean(name && email) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function decodeJwtClaims(token) {
|
|
53
|
+
const raw = String(token || '');
|
|
54
|
+
const parts = raw.split('.');
|
|
55
|
+
if (parts.length < 2) return null;
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Name and email from the AI Fabrix login token, used as prompt defaults.
|
|
65
|
+
* @param {Object} [options] - `config` and `controller` overrides for tests
|
|
66
|
+
* @returns {Promise<{ name: string, email: string }>} Empty strings when unavailable
|
|
67
|
+
*/
|
|
68
|
+
async function identityFromLoginToken(options = {}) {
|
|
69
|
+
const config = options.config || require('../core/config');
|
|
70
|
+
try {
|
|
71
|
+
const controller = options.controller || (await config.getControllerUrl());
|
|
72
|
+
if (!controller) return { name: '', email: '' };
|
|
73
|
+
const entry = await config.getDeviceToken(controller);
|
|
74
|
+
const claims = decodeJwtClaims(entry && (entry.token || entry.accessToken));
|
|
75
|
+
if (!claims) return { name: '', email: '' };
|
|
76
|
+
return {
|
|
77
|
+
name: String(claims.name || '').trim(),
|
|
78
|
+
email: String(claims.email || claims.preferred_username || '').trim()
|
|
79
|
+
};
|
|
80
|
+
} catch {
|
|
81
|
+
return { name: '', email: '' };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {Object} [options] - `config`
|
|
87
|
+
* @returns {Promise<string>} adminEmail from config.yaml, or ''
|
|
88
|
+
*/
|
|
89
|
+
async function emailFromConfig(options = {}) {
|
|
90
|
+
const config = options.config || require('../core/config');
|
|
91
|
+
try {
|
|
92
|
+
const cfg = await config.getConfig();
|
|
93
|
+
return String((cfg && cfg.adminEmail) || '').trim();
|
|
94
|
+
} catch {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} value - Candidate address
|
|
101
|
+
* @returns {boolean|string} true, or the reason it is rejected
|
|
102
|
+
*/
|
|
103
|
+
function validateEmail(value) {
|
|
104
|
+
return EMAIL_RE.test(String(value || '').trim()) || 'Enter an email address, for example name@example.com';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validateName(value) {
|
|
108
|
+
return String(value || '').trim().length > 0 || 'Enter the name to record on commits';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function promptIdentity(defaults, deps) {
|
|
112
|
+
const inquirer = deps.inquirer || require('inquirer');
|
|
113
|
+
return inquirer.prompt([
|
|
114
|
+
{
|
|
115
|
+
type: 'input',
|
|
116
|
+
name: 'name',
|
|
117
|
+
message: 'Name for git commits',
|
|
118
|
+
default: defaults.name || undefined,
|
|
119
|
+
validate: validateName
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
type: 'input',
|
|
123
|
+
name: 'email',
|
|
124
|
+
message: 'Email for git commits',
|
|
125
|
+
default: defaults.email || undefined,
|
|
126
|
+
validate: validateEmail
|
|
127
|
+
}
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function writeGitIdentity(identity, options) {
|
|
132
|
+
const exec = options.execFileSync || execFileSync;
|
|
133
|
+
exec('git', ['config', '--global', 'user.name', identity.name], { stdio: 'ignore' });
|
|
134
|
+
exec('git', ['config', '--global', 'user.email', identity.email], { stdio: 'ignore' });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function missingIdentityError(defaults) {
|
|
138
|
+
const name = defaults.name || 'Your Name';
|
|
139
|
+
const email = defaults.email || 'you@example.com';
|
|
140
|
+
return new Error(
|
|
141
|
+
'Git commit identity is not configured. Run:\n' +
|
|
142
|
+
` git config --global user.name ${JSON.stringify(name)}\n` +
|
|
143
|
+
` git config --global user.email ${email}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Ensures git has a commit identity, asking only when one is missing.
|
|
149
|
+
* @param {Object} [options] - `interactive`, `execFileSync`, `inquirer`, `config`
|
|
150
|
+
* @returns {Promise<{ name: string, email: string, source: 'git'|'prompt' }>}
|
|
151
|
+
*/
|
|
152
|
+
async function ensureGitIdentity(options = {}) {
|
|
153
|
+
const current = readGitIdentity(options);
|
|
154
|
+
if (current.complete) {
|
|
155
|
+
return { name: current.name, email: current.email, source: 'git' };
|
|
156
|
+
}
|
|
157
|
+
const token = await identityFromLoginToken(options);
|
|
158
|
+
const defaults = {
|
|
159
|
+
name: current.name || token.name,
|
|
160
|
+
email: current.email || token.email || (await emailFromConfig(options))
|
|
161
|
+
};
|
|
162
|
+
if (!options.interactive) {
|
|
163
|
+
throw missingIdentityError(defaults);
|
|
164
|
+
}
|
|
165
|
+
logger.log('Git commit identity is not set. Defaults come from your AI Fabrix login; edit if needed.');
|
|
166
|
+
const answers = await promptIdentity(defaults, options);
|
|
167
|
+
const identity = { name: String(answers.name).trim(), email: String(answers.email).trim() };
|
|
168
|
+
writeGitIdentity(identity, options);
|
|
169
|
+
logger.log(`Git identity set to ${identity.name} <${identity.email}>`);
|
|
170
|
+
return { ...identity, source: 'prompt' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
readGitIdentity,
|
|
175
|
+
identityFromLoginToken,
|
|
176
|
+
emailFromConfig,
|
|
177
|
+
ensureGitIdentity,
|
|
178
|
+
validateEmail,
|
|
179
|
+
validateName
|
|
180
|
+
};
|
package/lib/agent-kit/init.js
CHANGED
|
@@ -122,8 +122,7 @@ function runInit(options) {
|
|
|
122
122
|
const cwd = options.cwd || process.cwd();
|
|
123
123
|
const dryRun = Boolean(options.dryRun);
|
|
124
124
|
const scm = normalizeScm(options.scm);
|
|
125
|
-
const interactive =
|
|
126
|
-
typeof options.interactive === 'boolean' ? options.interactive : Boolean(process.stdout.isTTY);
|
|
125
|
+
const interactive = typeof options.interactive === 'boolean' ? options.interactive : Boolean(process.stdout.isTTY);
|
|
127
126
|
const { targetAbs, workspaceName } = resolveWorkspaceTarget(options.nameOrPath, cwd);
|
|
128
127
|
assertSafeTarget(targetAbs, Boolean(options.force));
|
|
129
128
|
assertRemoteFlags(options, interactive);
|
|
@@ -160,15 +159,7 @@ function runInit(options) {
|
|
|
160
159
|
|
|
161
160
|
const remote = remotePhase({ options, targetAbs, workspaceName, dryRun, phases });
|
|
162
161
|
|
|
163
|
-
return buildInitReport({
|
|
164
|
-
dryRun,
|
|
165
|
-
targetAbs,
|
|
166
|
-
workspaceName,
|
|
167
|
-
scm,
|
|
168
|
-
kit,
|
|
169
|
-
remote,
|
|
170
|
-
phases
|
|
171
|
-
});
|
|
162
|
+
return buildInitReport({ dryRun, targetAbs, workspaceName, scm, kit, remote, phases });
|
|
172
163
|
}
|
|
173
164
|
|
|
174
165
|
/**
|
|
@@ -258,8 +249,7 @@ function buildInitReport(p) {
|
|
|
258
249
|
function runRemoteCreate(options) {
|
|
259
250
|
const cwd = options.cwd || process.cwd();
|
|
260
251
|
const dryRun = Boolean(options.dryRun);
|
|
261
|
-
const interactive =
|
|
262
|
-
typeof options.interactive === 'boolean' ? options.interactive : Boolean(process.stdout.isTTY);
|
|
252
|
+
const interactive = typeof options.interactive === 'boolean' ? options.interactive : Boolean(process.stdout.isTTY);
|
|
263
253
|
const targetAbs = path.resolve(cwd);
|
|
264
254
|
if (isUnsafeRoot(targetAbs)) {
|
|
265
255
|
throw new Error(
|
|
@@ -285,10 +275,7 @@ function runRemoteCreate(options) {
|
|
|
285
275
|
throw new Error('Non-interactive remote create requires --org <owner>.');
|
|
286
276
|
}
|
|
287
277
|
checkRemoteAuth(options);
|
|
288
|
-
const commit = ensureInitialCommit(targetAbs, {
|
|
289
|
-
dryRun,
|
|
290
|
-
execFileSync: options.execFileSync
|
|
291
|
-
});
|
|
278
|
+
const commit = ensureInitialCommit(targetAbs, { dryRun, execFileSync: options.execFileSync });
|
|
292
279
|
const name = options.name || path.basename(targetAbs);
|
|
293
280
|
const remote = createGithubRemote({
|
|
294
281
|
name,
|
|
@@ -41,17 +41,22 @@ function formatSessionList(report) {
|
|
|
41
41
|
const lines = [sectionTitle(`Tmux sessions — ${sessions.length} active:`), ''];
|
|
42
42
|
if (sessions.length === 0) {
|
|
43
43
|
lines.push(metadata(' No active tmux sessions.'));
|
|
44
|
-
|
|
44
|
+
} else {
|
|
45
|
+
const widths = columnWidths(sessions);
|
|
46
|
+
const header = `${pad('Name', widths.name)}${pad('Status', widths.status)}Windows`;
|
|
47
|
+
lines.push(metadata(header));
|
|
48
|
+
lines.push(metadata('-'.repeat(widths.name + widths.status + widths.windows)));
|
|
49
|
+
sessions.forEach((session) => {
|
|
50
|
+
const status = session.attached ? 'attached' : 'detached';
|
|
51
|
+
lines.push(`${pad(session.name, widths.name)}${pad(status, widths.status)}${session.windows}`);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const servers = Array.isArray(report.codexServers) ? report.codexServers : [];
|
|
55
|
+
if (servers.length) {
|
|
56
|
+
lines.push('', sectionTitle(`Codex app servers — ${servers.length} active:`), '');
|
|
57
|
+
lines.push(metadata(' Started by Codex clients, independently of af onboarding.'));
|
|
58
|
+
servers.forEach(server => lines.push(` PID ${server.pid} ${server.startTime}`));
|
|
45
59
|
}
|
|
46
|
-
|
|
47
|
-
const widths = columnWidths(sessions);
|
|
48
|
-
const header = `${pad('Name', widths.name)}${pad('Status', widths.status)}Windows`;
|
|
49
|
-
lines.push(metadata(header));
|
|
50
|
-
lines.push(metadata('-'.repeat(widths.name + widths.status + widths.windows)));
|
|
51
|
-
sessions.forEach((session) => {
|
|
52
|
-
const status = session.attached ? 'attached' : 'detached';
|
|
53
|
-
lines.push(`${pad(session.name, widths.name)}${pad(status, widths.status)}${session.windows}`);
|
|
54
|
-
});
|
|
55
60
|
return lines.join('\n');
|
|
56
61
|
}
|
|
57
62
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/** Reuse Controller device login and delegate provider login to official CLIs. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
const config = require('../core/config');
|
|
9
|
+
const paths = require('../utils/paths');
|
|
10
|
+
const { checkDeviceToken } = require('../commands/auth-status');
|
|
11
|
+
const { handleDeviceCodeLogin } = require('../commands/login-device');
|
|
12
|
+
const { capture, checked, interactive, available } = require('./setup-process');
|
|
13
|
+
const { assertDirectory } = require('./setup-context');
|
|
14
|
+
const logger = require('../utils/logger');
|
|
15
|
+
|
|
16
|
+
const PROVIDERS = {
|
|
17
|
+
codex: { package: '@openai/codex', status: ['login', 'status'], login: ['login', '--device-auth'] },
|
|
18
|
+
claude: { package: '@anthropic-ai/claude-code', status: ['auth', 'status'], login: ['auth', 'login'] }
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
async function ensureController(context) {
|
|
22
|
+
const environment = await config.getCurrentEnvironment();
|
|
23
|
+
let auth;
|
|
24
|
+
try {
|
|
25
|
+
auth = await checkDeviceToken(context.controller);
|
|
26
|
+
} catch {
|
|
27
|
+
auth = null;
|
|
28
|
+
}
|
|
29
|
+
if (!auth || !auth.authenticated) {
|
|
30
|
+
await handleDeviceCodeLogin(context.controller, environment, false, undefined, {
|
|
31
|
+
compact: true, exitOnFailure: false
|
|
32
|
+
});
|
|
33
|
+
auth = await checkDeviceToken(context.controller);
|
|
34
|
+
}
|
|
35
|
+
if (!auth || !auth.authenticated) {
|
|
36
|
+
throw new Error('Controller authentication could not be verified. Run aifabrix login, then retry.');
|
|
37
|
+
}
|
|
38
|
+
await config.setControllerUrl(context.controller);
|
|
39
|
+
return environment;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ensureGithubLogin(options) {
|
|
43
|
+
try {
|
|
44
|
+
capture('gh', ['auth', 'status', '--hostname', 'github.com'], options);
|
|
45
|
+
return;
|
|
46
|
+
} catch {
|
|
47
|
+
const loginOptions = options.managed
|
|
48
|
+
? { ...options, env: { ...process.env, ...options.env, GH_BROWSER: 'true' } } : options;
|
|
49
|
+
if (options.managed) {
|
|
50
|
+
logger.log('GitHub sign-in: open https://github.com/login/device on your computer and enter the code shown below.');
|
|
51
|
+
}
|
|
52
|
+
interactive('gh', ['auth', 'login', '--hostname', 'github.com', '--git-protocol', 'https', '--web'],
|
|
53
|
+
'GitHub login did not complete. Open https://github.com/login/device on your computer, then retry.', loginOptions);
|
|
54
|
+
}
|
|
55
|
+
checked('gh', ['auth', 'status', '--hostname', 'github.com'], 'GitHub authentication could not be verified.', options);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function providerEnvironment(options) {
|
|
59
|
+
const prefix = path.join(paths.getAifabrixSystemDir(), 'agent-tools');
|
|
60
|
+
const env = { ...process.env, ...options.env };
|
|
61
|
+
const localBin = path.join(options.homedir || os.homedir(), '.local', 'bin');
|
|
62
|
+
env.PATH = [path.join(prefix, 'bin'), localBin, env.PATH || ''].join(path.delimiter);
|
|
63
|
+
return { ...options, env, prefix };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function installClaudeNative(runtime) {
|
|
67
|
+
if (!available('curl', runtime) || !available('bash', runtime)) {
|
|
68
|
+
throw new Error('Install curl and bash to install Claude Code, then retry.');
|
|
69
|
+
}
|
|
70
|
+
logger.log('Installing Claude Code for the current user...');
|
|
71
|
+
const script = checked('curl', ['-fsSL', 'https://claude.ai/install.sh'],
|
|
72
|
+
'Could not download the Claude Code installer. Check network access, then retry.', runtime);
|
|
73
|
+
const result = (runtime.spawnSync || spawnSync)('bash', ['-s', 'stable'], {
|
|
74
|
+
cwd: runtime.cwd, env: runtime.env, input: script, encoding: 'utf8',
|
|
75
|
+
stdio: ['pipe', 'inherit', 'inherit']
|
|
76
|
+
});
|
|
77
|
+
if (!result || result.error || result.signal || result.status !== 0) {
|
|
78
|
+
throw new Error('Could not install Claude Code. Check user-directory permissions, then retry.');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function exposeManagedProvider(binary, code, options = {}) {
|
|
83
|
+
const localDir = path.join(options.homedir || os.homedir(), '.local');
|
|
84
|
+
const binDir = path.join(localDir, 'bin');
|
|
85
|
+
assertDirectory(localDir);
|
|
86
|
+
fs.mkdirSync(localDir, { recursive: true });
|
|
87
|
+
assertDirectory(binDir);
|
|
88
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
89
|
+
const link = path.join(binDir, code);
|
|
90
|
+
let existing;
|
|
91
|
+
try {
|
|
92
|
+
existing = fs.lstatSync(link);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error.code !== 'ENOENT') throw error;
|
|
95
|
+
}
|
|
96
|
+
if (existing) {
|
|
97
|
+
if (path.resolve(binary) === path.resolve(link)) return link;
|
|
98
|
+
if (existing.isSymbolicLink() && fs.readlinkSync(link) === binary) return link;
|
|
99
|
+
throw new Error(`Cannot expose ${code} on PATH because ${link} already exists. Preserve it and resolve the conflict.`);
|
|
100
|
+
}
|
|
101
|
+
fs.symlinkSync(binary, link);
|
|
102
|
+
return link;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function ensureProvider(context, options = {}) {
|
|
106
|
+
const provider = PROVIDERS[context.code];
|
|
107
|
+
const runtime = providerEnvironment(options);
|
|
108
|
+
if (!available(context.code, runtime)) {
|
|
109
|
+
if (context.code === 'claude') {
|
|
110
|
+
installClaudeNative(runtime);
|
|
111
|
+
} else {
|
|
112
|
+
if (!available('npm', runtime)) throw new Error('Install npm to install the selected coding CLI, then retry.');
|
|
113
|
+
logger.log(`Installing ${context.code} for the current user...`);
|
|
114
|
+
checked('npm', ['install', '--global', '--prefix', runtime.prefix, provider.package],
|
|
115
|
+
`Could not install ${context.code}. Check npm access and user-directory permissions, then retry.`, runtime);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const binary = available(context.code, runtime);
|
|
119
|
+
if (!binary) throw new Error(`The ${context.code} executable was not found after installation.`);
|
|
120
|
+
checked(binary, ['--version'], `The ${context.code} executable is not usable. Reinstall it, then retry.`, runtime);
|
|
121
|
+
try {
|
|
122
|
+
capture(binary, provider.status, runtime);
|
|
123
|
+
} catch {
|
|
124
|
+
interactive(binary, provider.login, `${context.code} login did not complete. Sign in and retry.`, runtime);
|
|
125
|
+
}
|
|
126
|
+
checked(binary, provider.status, `${context.code} authentication could not be verified.`, runtime);
|
|
127
|
+
if (options.managed) exposeManagedProvider(binary, context.code, options);
|
|
128
|
+
return { binary, env: runtime.env };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { ensureController, ensureGithubLogin, ensureProvider, providerEnvironment, exposeManagedProvider };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** Validate the minimal onboarding inputs and resolve the configured workspace root. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const inquirer = require('inquirer');
|
|
7
|
+
const fs = require('../internal/fs-real-sync');
|
|
8
|
+
const config = require('../core/config');
|
|
9
|
+
const paths = require('../utils/paths');
|
|
10
|
+
const { isUnsafeRoot } = require('./repo-guard');
|
|
11
|
+
const { available } = require('./setup-process');
|
|
12
|
+
|
|
13
|
+
function controllerUrl(value) {
|
|
14
|
+
let url;
|
|
15
|
+
try {
|
|
16
|
+
url = new URL(value);
|
|
17
|
+
} catch {
|
|
18
|
+
throw new Error('A valid Controller URL is required.');
|
|
19
|
+
}
|
|
20
|
+
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
|
|
21
|
+
throw new Error('Controller URL must use HTTP(S) without credentials, query parameters, or a fragment.');
|
|
22
|
+
}
|
|
23
|
+
return url.href.replace(/\/+$/, '');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function setupName(options, repository) {
|
|
27
|
+
if (options.managed && options.name !== undefined) return options.name;
|
|
28
|
+
return repository === undefined ? 'workspace' : repository;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function validFolderName(name) {
|
|
32
|
+
return typeof name === 'string' && /^[a-z0-9][a-z0-9._-]{0,99}$/i.test(name) && !name.endsWith('.git');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeSetup(options) {
|
|
36
|
+
const controller = controllerUrl(options.controllerUrl);
|
|
37
|
+
const code = options.code || 'codex';
|
|
38
|
+
const hosts = options.managed ? ['codex', 'claude', 'cursor'] : ['codex', 'claude'];
|
|
39
|
+
if (!hosts.includes(code)) throw new Error(options.managed
|
|
40
|
+
? 'Managed codingPlatform must be codex, claude, or cursor.' : 'Use codex or claude for --code.');
|
|
41
|
+
if (options.repo !== undefined && (typeof options.repo !== 'string' || options.repo.split('/').length !== 2)) {
|
|
42
|
+
throw new Error('Use --repo owner/name for a GitHub repository.');
|
|
43
|
+
}
|
|
44
|
+
const [org, repository] = options.repo === undefined ? [] : options.repo.split('/');
|
|
45
|
+
const name = setupName(options, repository);
|
|
46
|
+
if (org !== undefined && !/^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?$/i.test(org)) {
|
|
47
|
+
throw new Error('Invalid GitHub organization or user name.');
|
|
48
|
+
}
|
|
49
|
+
if (!validFolderName(name)) {
|
|
50
|
+
throw new Error(options.managed
|
|
51
|
+
? 'Invalid --name. Use one folder name without slashes or a .git suffix.'
|
|
52
|
+
: 'Invalid repository name. Use a simple folder name without a .git suffix.');
|
|
53
|
+
}
|
|
54
|
+
return { controller, code, name, fullName: org ? `${org}/${name}` : null };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function preflight(context, options = {}) {
|
|
58
|
+
const platform = options.platform || process.platform;
|
|
59
|
+
const arch = options.arch || process.arch;
|
|
60
|
+
if (!['linux', 'darwin'].includes(platform) || !['x64', 'arm64'].includes(arch)) {
|
|
61
|
+
throw new Error('Agent onboarding supports Linux/macOS on x64 or arm64.');
|
|
62
|
+
}
|
|
63
|
+
const tty = options.isTTY === undefined ? process.stdin.isTTY && process.stdout.isTTY : options.isTTY;
|
|
64
|
+
if (!tty) throw new Error(options.managed
|
|
65
|
+
? 'Run onboarding in an interactive terminal for account login.'
|
|
66
|
+
: 'Run agent-kit setup in an interactive terminal for login and agent attachment.');
|
|
67
|
+
for (const bin of ['git', ...(!options.managed || context.code === 'claude' ? ['tmux'] : []),
|
|
68
|
+
...(context.fullName ? ['gh'] : [])]) {
|
|
69
|
+
if (!available(bin, options)) throw new Error(`${bin} is required on PATH. Install it, then retry onboarding.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertDirectory(file) {
|
|
74
|
+
let stat;
|
|
75
|
+
try {
|
|
76
|
+
stat = fs.lstatSync(file);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error.code === 'ENOENT') return;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
82
|
+
throw new Error(`Expected a directory, not a link or file: ${file}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function workspaceName(context, options, root, home) {
|
|
87
|
+
if (options.managed && root === home && options.name === undefined && !context.fullName) {
|
|
88
|
+
return 'agent-kit-workspace';
|
|
89
|
+
}
|
|
90
|
+
return context.name;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function resolveWork(context, options = {}) {
|
|
94
|
+
let root = (options.getWork || paths.getAifabrixWork)();
|
|
95
|
+
if (!root && options.managed) root = path.join(options.homedir || os.homedir(), 'aifabrix-work');
|
|
96
|
+
if (!root) {
|
|
97
|
+
const answer = await (options.prompt || inquirer.prompt)([{
|
|
98
|
+
type: 'input', name: 'work', message: 'AI Fabrix work directory:',
|
|
99
|
+
default: path.join(os.homedir(), 'aifabrix-work'),
|
|
100
|
+
validate: value => path.isAbsolute(String(value).trim()) || 'Enter an absolute directory path.'
|
|
101
|
+
}]);
|
|
102
|
+
root = String(answer.work || '').trim();
|
|
103
|
+
if (!path.isAbsolute(root)) throw new Error('Work directory must be an absolute path.');
|
|
104
|
+
await (options.setWork || config.setAifabrixWorkOverride)(root);
|
|
105
|
+
}
|
|
106
|
+
root = path.resolve(root);
|
|
107
|
+
assertDirectory(root);
|
|
108
|
+
fs.mkdirSync(root, { recursive: true });
|
|
109
|
+
root = fs.realpathSync(root);
|
|
110
|
+
const home = fs.realpathSync(options.homedir || os.homedir());
|
|
111
|
+
const name = workspaceName(context, options, root, home);
|
|
112
|
+
const target = path.join(root, name);
|
|
113
|
+
if (isUnsafeRoot(target)) throw new Error('Refusing to use home or the filesystem root as a workspace.');
|
|
114
|
+
assertDirectory(target);
|
|
115
|
+
return { ...context, name, root, target };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { normalizeSetup, preflight, resolveWork, assertDirectory };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** Optional GitHub onboarding; API creation arbitrates missing/private repository names. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { capture, checked } = require('./setup-process');
|
|
5
|
+
|
|
6
|
+
function githubOptions(options = {}) {
|
|
7
|
+
return { ...options, env: { ...process.env, ...options.env, GH_HOST: 'github.com' } };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function lookupRepository(fullName, options) {
|
|
11
|
+
options = githubOptions(options);
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(capture('gh', ['api', `repos/${fullName}`], options));
|
|
14
|
+
} catch (error) {
|
|
15
|
+
// gh's included HTTP response allows status checking without exposing its body/errors.
|
|
16
|
+
try {
|
|
17
|
+
capture('gh', ['api', '--include', `repos/${fullName}`], options);
|
|
18
|
+
} catch (probe) {
|
|
19
|
+
if (/^HTTP\/\S+ 404(?:\s|$)/m.test(String(probe.stdout || ''))) return null;
|
|
20
|
+
}
|
|
21
|
+
throw new Error('Cannot inspect the GitHub repository. Check account access and connectivity, then retry.');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createRepository(fullName, options) {
|
|
26
|
+
checked('gh', ['repo', 'create', fullName, '--private'],
|
|
27
|
+
'GitHub could not create the private repository. It may already exist without access, or creation is denied. No push was attempted.', githubOptions(options));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cloneRepository(context, options) {
|
|
31
|
+
checked('gh', ['repo', 'clone', context.fullName, context.target, '--', '--origin', 'origin'],
|
|
32
|
+
'Repository clone failed. Preserve any partial folder and inspect GitHub access before retrying.', githubOptions(options));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function originIdentity(target, options) {
|
|
36
|
+
let origin;
|
|
37
|
+
try {
|
|
38
|
+
origin = String(capture('git', ['remote', 'get-url', 'origin'], { ...options, cwd: target })).trim();
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const match = /^(?:https:\/\/github\.com\/|git@github\.com:)([a-z0-9-]+\/[a-z0-9._-]+?)(?:\.git)?$/i.exec(origin);
|
|
43
|
+
return match ? match[1].toLowerCase() : 'unrecognized';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function assertOrigin(context, options) {
|
|
47
|
+
if (originIdentity(context.target, options) !== context.fullName.toLowerCase()) {
|
|
48
|
+
throw new Error('The existing origin does not match the requested GitHub repository. No remote was changed.');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function pushInitialCommit(context, progress, save, options) {
|
|
53
|
+
const opts = { ...githubOptions(options), cwd: context.target };
|
|
54
|
+
assertOrigin(context, options);
|
|
55
|
+
const expected = progress.state.initialCommit;
|
|
56
|
+
if (!/^[0-9a-f]{40,64}$/.test(expected || '')) throw new Error('Missing initial commit identity; refusing to push.');
|
|
57
|
+
const credentials = ['-c', 'credential.helper=', '-c', 'credential.https://github.com.helper=!gh auth git-credential'];
|
|
58
|
+
const remote = String(checked('git', [...credentials, 'ls-remote', 'origin', 'refs/heads/main'],
|
|
59
|
+
'Cannot verify the remote branch before initial push.', opts)).trim();
|
|
60
|
+
if (remote && remote.split(/\s/)[0] !== expected) {
|
|
61
|
+
throw new Error('Remote branch changed during onboarding. Reconcile it with Git before retrying.');
|
|
62
|
+
}
|
|
63
|
+
if (!remote) {
|
|
64
|
+
checked('git', [...credentials, 'push', 'origin', `${expected}:refs/heads/main`],
|
|
65
|
+
'Initial push failed. Local files and the private repository are preserved; retry setup.', opts);
|
|
66
|
+
}
|
|
67
|
+
save(progress, { pushed: true });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { lookupRepository, createRepository, cloneRepository, originIdentity, assertOrigin, pushInitialCommit };
|