@aifabrix/builder 2.57.0 → 2.59.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/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 +84 -0
- package/lib/agent-kit/start.js +4 -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/types/dev.types.js +3 -3
- package/lib/app/deploy.js +0 -102
- package/lib/app/push.js +98 -41
- package/lib/build/standard-docker-build.js +13 -11
- package/lib/cli/index.js +4 -0
- package/lib/cli/setup-app.js +16 -5
- package/lib/cli/setup-dev-path-commands.js +53 -2
- package/lib/cli/setup-dev.js +7 -4
- package/lib/cli/setup-environment.js +17 -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/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/dev-registry.js +155 -0
- package/lib/commands/dev-show-display.js +10 -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/setup-image-refresh.js +26 -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/config-attach-extensions.js +4 -1
- 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 +50 -9
- package/lib/deployment/installation/azure-preflight.js +63 -7
- package/lib/deployment/installation/azure-readiness.js +59 -7
- 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 +57 -5
- package/lib/deployment/installation/infra-manifest.js +22 -2
- package/lib/deployment/installation/infra-projection.js +14 -3
- package/lib/deployment/installation/registry-auth-mode.js +121 -0
- package/lib/deployment/installation/stage-input.js +21 -6
- package/lib/deployment/marketplace-environment.js +26 -1
- package/lib/generator/builders.js +8 -9
- package/lib/generator/index.js +23 -12
- 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/schema/environment-deploy-request.schema.json +1 -1
- package/lib/schema/external-datasource.schema.json +20 -5
- package/lib/schema/infra-parameter.schema.json +1 -1
- package/lib/schema/infra.parameter.yaml +2 -1
- package/lib/schema/infrastructure-schema.json +200 -3
- package/lib/schema/installation-environment.schema.json +1 -1
- package/lib/schema/wizard-config.schema.json +1 -1
- package/lib/utils/app-register-config.js +2 -4
- package/lib/utils/build-resolve-image.js +2 -21
- package/lib/utils/cli-utils.js +8 -1
- package/lib/utils/config-paths.js +24 -29
- package/lib/utils/config-registry-preference.js +113 -0
- 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 +53 -19
- package/lib/utils/docker-daemon-tls-ca.js +2 -2
- package/lib/utils/env-map.js +23 -6
- package/lib/utils/error-formatters/validation-errors.js +25 -5
- package/lib/utils/help-builder.js +4 -3
- package/lib/utils/image-tags.js +88 -0
- package/lib/utils/promote-dev-docker-image.js +6 -0
- package/lib/utils/registry-credentials.js +236 -0
- package/lib/utils/resolve-docker-image-ref.js +78 -9
- package/lib/utils/secrets-helpers.js +9 -1
- package/lib/utils/token-manager.js +29 -36
- package/lib/utils/variable-transformer.js +2 -4
- package/package.json +4 -2
- package/templates/agent-kit/instructions/AGENTKIT.md +39 -2
- package/templates/agent-kit/instructions/root.AGENTS.md +40 -1
- package/templates/agent-kit/skills/shared/hosts.md +23 -5
- package/templates/agent-kit/workspace/README.md +14 -1
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +1 -1
- package/templates/applications/miso-controller/application.yaml +1 -1
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/external-system/external-datasource.yaml.hbs +7 -6
- package/templates/marketplace/main.json +440 -82
|
@@ -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 };
|
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 };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Read the provisioned user config for zero-argument managed onboarding. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('../internal/fs-real-sync');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const yaml = require('js-yaml');
|
|
7
|
+
const config = require('../core/config');
|
|
8
|
+
const { normalizeSetup } = require('./setup-context');
|
|
9
|
+
|
|
10
|
+
function assertConfigFile(file) {
|
|
11
|
+
let stat;
|
|
12
|
+
try {
|
|
13
|
+
stat = fs.lstatSync(file);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (error.code === 'ENOENT') {
|
|
16
|
+
throw new Error('Managed config.yaml is missing. Ask your administrator to provision this workspace.');
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
21
|
+
throw new Error('Managed config.yaml must be a regular file. Ask your administrator to repair it.');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function assertManagedKeys(file) {
|
|
26
|
+
let raw;
|
|
27
|
+
try {
|
|
28
|
+
raw = yaml.load(fs.readFileSync(file, 'utf8'));
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error('Managed config.yaml is invalid. Ask your administrator to repair it.');
|
|
31
|
+
}
|
|
32
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw) ||
|
|
33
|
+
!['controller', 'environment', 'codingPlatform'].every(key => Object.hasOwn(raw, key))) {
|
|
34
|
+
throw new Error('Managed config.yaml needs controller, environment, and codingPlatform. Ask your administrator to repair it.');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function assertManagedOverrides(options) {
|
|
39
|
+
if (options.code !== undefined || options.controllerUrl !== undefined) {
|
|
40
|
+
throw new Error('Managed onboarding uses the provisioned Controller; --code and --controller overrides are not allowed.');
|
|
41
|
+
}
|
|
42
|
+
if (options.repo !== undefined && options.name !== undefined) {
|
|
43
|
+
throw new Error('Use either --repo owner/name or --name folder for managed onboarding.');
|
|
44
|
+
}
|
|
45
|
+
if (options.coding !== undefined && !['claude', 'codex', 'cursor'].includes(options.coding)) {
|
|
46
|
+
throw new Error('Use --coding claude, codex, or cursor.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function assertManagedValues(values) {
|
|
51
|
+
if (!values || typeof values !== 'object' || typeof values.controller !== 'string' ||
|
|
52
|
+
typeof values.environment !== 'string' || !values.environment.trim() ||
|
|
53
|
+
!['codex', 'claude', 'cursor'].includes(values.codingPlatform)) {
|
|
54
|
+
throw new Error('Managed config.yaml needs controller, environment, and codingPlatform. Ask your administrator to repair it.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function managedSetupOptions(options = {}) {
|
|
59
|
+
assertManagedOverrides(options);
|
|
60
|
+
const file = options.configFile || config.CONFIG_FILE;
|
|
61
|
+
assertConfigFile(file);
|
|
62
|
+
assertManagedKeys(file);
|
|
63
|
+
const values = await (options.getConfig || config.getConfig)();
|
|
64
|
+
assertManagedValues(values);
|
|
65
|
+
let context;
|
|
66
|
+
try {
|
|
67
|
+
context = normalizeSetup({ controllerUrl: values.controller,
|
|
68
|
+
code: options.coding || values.codingPlatform, managed: true });
|
|
69
|
+
} catch {
|
|
70
|
+
throw new Error('Managed config.yaml has an invalid Controller URL or coding platform. Ask your administrator to repair it.');
|
|
71
|
+
}
|
|
72
|
+
const sshHost = values['sync-ssh-host'];
|
|
73
|
+
const remoteHost = typeof sshHost === 'string' && /^[a-z0-9.-]+$/i.test(sshHost)
|
|
74
|
+
? sshHost : os.hostname();
|
|
75
|
+
return { controllerUrl: context.controller, code: context.code,
|
|
76
|
+
remoteHost, managed: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { managedSetupOptions, assertConfigFile, assertManagedKeys };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Process boundary for onboarding: argv only and no credential-bearing errors. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
function capture(bin, args, options = {}) {
|
|
7
|
+
return (options.execFileSync || execFileSync)(bin, args, {
|
|
8
|
+
cwd: options.cwd,
|
|
9
|
+
env: options.env || process.env,
|
|
10
|
+
encoding: 'utf8',
|
|
11
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function checked(bin, args, message, options = {}) {
|
|
16
|
+
try {
|
|
17
|
+
return capture(bin, args, options);
|
|
18
|
+
} catch {
|
|
19
|
+
throw new Error(message);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function interactive(bin, args, message, options = {}) {
|
|
24
|
+
const result = (options.spawnSync || spawnSync)(bin, args, {
|
|
25
|
+
cwd: options.cwd,
|
|
26
|
+
env: options.env || process.env,
|
|
27
|
+
stdio: 'inherit'
|
|
28
|
+
});
|
|
29
|
+
if (!result || result.error || result.signal || result.status !== 0) {
|
|
30
|
+
throw new Error(message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function available(bin, options = {}) {
|
|
35
|
+
try {
|
|
36
|
+
return String(capture('which', [bin], options)).trim();
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { capture, checked, interactive, available };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Start or reattach only to a session belonging to this workspace and selected agent. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { createHash } = require('crypto');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
7
|
+
const { runStart, hostUserPrefix, resolveRemoteControlName, resolveSessionName } = require('./start');
|
|
8
|
+
const { tmuxSessionExists } = require('./tmux-sessions');
|
|
9
|
+
const { checked, interactive } = require('./setup-process');
|
|
10
|
+
|
|
11
|
+
function sessionIdentity(context, options = {}) {
|
|
12
|
+
const key = `${context.target}\n${context.controller}\n${context.code}`;
|
|
13
|
+
const hash = createHash('sha256').update(key).digest('hex').slice(0, 16);
|
|
14
|
+
const legacyName = `aifabrix-${context.code}-${hash}`;
|
|
15
|
+
const name = options.managed && context.code === 'claude'
|
|
16
|
+
? resolveSessionName(undefined, context.target) : legacyName;
|
|
17
|
+
return { name, key, legacyName };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function verifySession(name, key, options) {
|
|
21
|
+
const saved = String(checked('tmux', ['show-options', '-v', '-t', name, '@aifabrix-setup'],
|
|
22
|
+
'Cannot verify existing agent session ownership.', options)).trim();
|
|
23
|
+
if (saved !== key) throw new Error('Existing session ownership does not match. No session was replaced.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function prepareSession(context, provider, options = {}) {
|
|
27
|
+
const { name, key, legacyName } = sessionIdentity(context, options);
|
|
28
|
+
const exec = (bin, args, extra) => (options.execFileSync || execFileSync)(bin, args, { ...extra, env: provider.env });
|
|
29
|
+
const spawn = (bin, args, extra) => (options.spawnSync || spawnSync)(bin, args, { ...extra, env: provider.env });
|
|
30
|
+
const opts = { ...options, env: provider.env };
|
|
31
|
+
if (tmuxSessionExists(name, exec)) {
|
|
32
|
+
verifySession(name, key, opts);
|
|
33
|
+
} else if (name !== legacyName && tmuxSessionExists(legacyName, exec)) {
|
|
34
|
+
verifySession(legacyName, key, opts);
|
|
35
|
+
checked('tmux', ['rename-session', '-t', legacyName, name],
|
|
36
|
+
'Could not rename the existing agent session. Inspect agent-kit list before retrying.', opts);
|
|
37
|
+
} else {
|
|
38
|
+
const remoteControlName = context.code === 'claude'
|
|
39
|
+
? resolveRemoteControlName(hostUserPrefix(options), path.basename(context.target)) : undefined;
|
|
40
|
+
runStart({ app: context.code, cwd: context.target, name, detach: true, skipApproveAll: true,
|
|
41
|
+
binary: provider.binary, remoteControlName, execFileSync: exec, spawnSync: spawn });
|
|
42
|
+
checked('tmux', ['set-option', '-t', name, '@aifabrix-setup', key],
|
|
43
|
+
'Agent started but its session could not be marked. Inspect agent-kit list before retrying.', opts);
|
|
44
|
+
}
|
|
45
|
+
return name;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function attachSession(name, provider, options = {}) {
|
|
49
|
+
interactive('tmux', ['attach-session', '-t', `=${name}`],
|
|
50
|
+
'Could not attach the agent session. Inspect aifabrix agent-kit list.', { ...options, env: provider.env });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { prepareSession, attachSession, sessionIdentity };
|