@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.
Files changed (120) hide show
  1. package/lib/agent-kit/codex-app-server.js +44 -0
  2. package/lib/agent-kit/git-identity.js +180 -0
  3. package/lib/agent-kit/init.js +4 -17
  4. package/lib/agent-kit/session-report.js +15 -10
  5. package/lib/agent-kit/setup-auth.js +131 -0
  6. package/lib/agent-kit/setup-context.js +118 -0
  7. package/lib/agent-kit/setup-github.js +70 -0
  8. package/lib/agent-kit/setup-managed.js +79 -0
  9. package/lib/agent-kit/setup-process.js +42 -0
  10. package/lib/agent-kit/setup-session.js +53 -0
  11. package/lib/agent-kit/setup-state.js +52 -0
  12. package/lib/agent-kit/setup-workspace.js +113 -0
  13. package/lib/agent-kit/setup.js +87 -0
  14. package/lib/agent-kit/start.js +48 -3
  15. package/lib/agent-kit/tmux-sessions.js +20 -3
  16. package/lib/api/dev.api.js +1 -1
  17. package/lib/api/miso-health.api.js +26 -0
  18. package/lib/api/role-assistant-test-job.api.js +60 -0
  19. package/lib/api/types/dev.types.js +3 -3
  20. package/lib/api/work-search.api.js +13 -6
  21. package/lib/app/deploy.js +8 -103
  22. package/lib/app/show-display.js +1 -0
  23. package/lib/app/show-online.js +15 -0
  24. package/lib/build/standard-docker-build.js +4 -1
  25. package/lib/cli/index.js +4 -0
  26. package/lib/cli/setup-app.js +14 -1
  27. package/lib/cli/setup-dev-path-commands.js +4 -0
  28. package/lib/cli/setup-dev.js +7 -4
  29. package/lib/cli/setup-help.js +86 -0
  30. package/lib/cli/setup-onboarding.js +37 -0
  31. package/lib/cli/setup-utility-repair.js +7 -5
  32. package/lib/cli/setup-utility.js +24 -1
  33. package/lib/commands/agent-kit-help.js +21 -7
  34. package/lib/commands/agent-kit-sessions.js +126 -0
  35. package/lib/commands/agent-kit-setup.js +44 -0
  36. package/lib/commands/agent-kit.js +5 -79
  37. package/lib/commands/dev-init.js +21 -23
  38. package/lib/commands/dev-refresh.js +2 -2
  39. package/lib/commands/governance-verify-external.js +12 -5
  40. package/lib/commands/repair-datasource.js +9 -9
  41. package/lib/commands/repair.js +45 -8
  42. package/lib/commands/role-assistant.js +7 -0
  43. package/lib/commands/upload-qualification-client.js +56 -11
  44. package/lib/commands/upload-qualification-poll.js +51 -22
  45. package/lib/commands/wizard-dataplane.js +16 -52
  46. package/lib/core/env-platform-expand.js +93 -0
  47. package/lib/core/secrets-env-content.js +37 -3
  48. package/lib/datasource/datasource-validate-summary.js +8 -5
  49. package/lib/datasource/sync-defaults.js +53 -0
  50. package/lib/deployment/installation/azure-infra-stage.js +13 -1
  51. package/lib/deployment/installation/azure-preflight.js +13 -2
  52. package/lib/deployment/installation/azure-readiness.js +24 -2
  53. package/lib/deployment/installation/environment-auth.js +60 -20
  54. package/lib/deployment/installation/environment-stage.js +9 -5
  55. package/lib/deployment/installation/infra-catalog.js +10 -7
  56. package/lib/deployment/installation/infra-manifest.js +5 -0
  57. package/lib/deployment/installation/registry-auth-mode.js +33 -10
  58. package/lib/deployment/marketplace-environment.js +26 -1
  59. package/lib/generator/builders.js +17 -0
  60. package/lib/generator/helpers.js +23 -2
  61. package/lib/generator/index.js +33 -18
  62. package/lib/integration-definition/apply.js +2 -2
  63. package/lib/internal/fs-real-sync.js +4 -1
  64. package/lib/programmatic/bearer-auth.js +3 -62
  65. package/lib/programmatic/builder-api-startup.js +51 -0
  66. package/lib/programmatic/builder-miso-runtime.js +167 -0
  67. package/lib/programmatic/openapi-descriptions.js +3 -3
  68. package/lib/programmatic/openapi-schema-descriptions-core.js +3 -3
  69. package/lib/programmatic/openapi-spec.js +2 -1
  70. package/lib/programmatic/route-handler-map.js +3 -2
  71. package/lib/role-assistant/test-cases-search.js +3 -0
  72. package/lib/role-assistant/test-job-runner.js +125 -0
  73. package/lib/role-assistant/test-runner-search.js +44 -2
  74. package/lib/schema/application-schema.json +193 -2
  75. package/lib/schema/environment-deploy-request.schema.json +1 -1
  76. package/lib/schema/external-datasource.schema.json +20 -5
  77. package/lib/schema/infra-parameter.schema.json +140 -34
  78. package/lib/schema/infra.parameter.yaml +30 -29
  79. package/lib/schema/infrastructure-schema.json +133 -14
  80. package/lib/schema/installation-environment.schema.json +1 -1
  81. package/lib/schema/wizard-config.schema.json +1 -1
  82. package/lib/utils/compose-generate-docker-compose.js +12 -0
  83. package/lib/utils/config-paths.js +24 -29
  84. package/lib/utils/config-registry-preference.js +13 -6
  85. package/lib/utils/dev-init-ssh-merge.js +5 -4
  86. package/lib/utils/dev-user-groups.js +2 -1
  87. package/lib/utils/docker-build.js +29 -8
  88. package/lib/utils/docker-daemon-tls-ca.js +2 -2
  89. package/lib/utils/docker-manifest-public-port.js +36 -0
  90. package/lib/utils/error-formatters/validation-errors.js +25 -5
  91. package/lib/utils/help-builder.js +4 -3
  92. package/lib/utils/platform-resolution.js +226 -0
  93. package/lib/utils/resolve-docker-image-ref.js +8 -3
  94. package/lib/utils/token-manager.js +29 -36
  95. package/package.json +4 -3
  96. package/templates/README.md +2 -1
  97. package/templates/agent-kit/agent-kit.yaml +4 -1
  98. package/templates/agent-kit/instructions/AGENTKIT.md +40 -3
  99. package/templates/agent-kit/instructions/root.AGENTS.md +42 -1
  100. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +33 -1
  101. package/templates/agent-kit/skills/aifabrix-connected-system/scripts/delivery-verdict.js +176 -0
  102. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -0
  103. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +10 -3
  104. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +22 -0
  105. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +21 -0
  106. package/templates/agent-kit/skills/shared/feedback.md +31 -0
  107. package/templates/agent-kit/skills/shared/hosts.md +35 -7
  108. package/templates/agent-kit/skills/shared/status.md +85 -0
  109. package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
  110. package/templates/agent-kit/workspace/README.md +14 -1
  111. package/templates/applications/builder-api/application.yaml +1 -1
  112. package/templates/applications/dataplane/application.yaml +30 -2
  113. package/templates/applications/dataplane/env.template +37 -4
  114. package/templates/applications/miso-controller/application.yaml +24 -1
  115. package/templates/applications/miso-controller/env.template +22 -35
  116. package/templates/external-system/external-datasource.yaml.hbs +7 -6
  117. package/templates/marketplace/main.json +281 -75
  118. package/templates/python/Dockerfile.hbs +2 -0
  119. package/templates/typescript/Dockerfile.hbs +2 -0
  120. 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
+ };
@@ -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
- return lines.join('\n');
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 };