@virtengine/openfleet 0.25.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/.env.example +914 -0
- package/LICENSE +190 -0
- package/README.md +500 -0
- package/agent-endpoint.mjs +918 -0
- package/agent-hook-bridge.mjs +230 -0
- package/agent-hooks.mjs +1188 -0
- package/agent-pool.mjs +2403 -0
- package/agent-prompts.mjs +689 -0
- package/agent-sdk.mjs +141 -0
- package/anomaly-detector.mjs +1195 -0
- package/autofix.mjs +1294 -0
- package/claude-shell.mjs +708 -0
- package/cli.mjs +906 -0
- package/codex-config.mjs +1274 -0
- package/codex-model-profiles.mjs +135 -0
- package/codex-shell.mjs +762 -0
- package/config-doctor.mjs +613 -0
- package/config.mjs +1720 -0
- package/conflict-resolver.mjs +248 -0
- package/container-runner.mjs +450 -0
- package/copilot-shell.mjs +827 -0
- package/daemon-restart-policy.mjs +56 -0
- package/diff-stats.mjs +282 -0
- package/error-detector.mjs +829 -0
- package/fetch-runtime.mjs +34 -0
- package/fleet-coordinator.mjs +838 -0
- package/get-telegram-chat-id.mjs +71 -0
- package/git-safety.mjs +170 -0
- package/github-reconciler.mjs +403 -0
- package/hook-profiles.mjs +651 -0
- package/kanban-adapter.mjs +4491 -0
- package/lib/logger.mjs +645 -0
- package/maintenance.mjs +828 -0
- package/merge-strategy.mjs +1171 -0
- package/monitor.mjs +12207 -0
- package/openfleet.config.example.json +115 -0
- package/openfleet.schema.json +465 -0
- package/package.json +203 -0
- package/postinstall.mjs +187 -0
- package/pr-cleanup-daemon.mjs +978 -0
- package/preflight.mjs +408 -0
- package/prepublish-check.mjs +90 -0
- package/presence.mjs +328 -0
- package/primary-agent.mjs +282 -0
- package/publish.mjs +151 -0
- package/repo-root.mjs +29 -0
- package/restart-controller.mjs +100 -0
- package/review-agent.mjs +557 -0
- package/rotate-agent-logs.sh +133 -0
- package/sdk-conflict-resolver.mjs +973 -0
- package/session-tracker.mjs +880 -0
- package/setup.mjs +3937 -0
- package/shared-knowledge.mjs +410 -0
- package/shared-state-manager.mjs +841 -0
- package/shared-workspace-cli.mjs +199 -0
- package/shared-workspace-registry.mjs +537 -0
- package/shared-workspaces.json +18 -0
- package/startup-service.mjs +1070 -0
- package/sync-engine.mjs +1063 -0
- package/task-archiver.mjs +801 -0
- package/task-assessment.mjs +550 -0
- package/task-claims.mjs +924 -0
- package/task-complexity.mjs +581 -0
- package/task-executor.mjs +5111 -0
- package/task-store.mjs +753 -0
- package/telegram-bot.mjs +9281 -0
- package/telegram-sentinel.mjs +2010 -0
- package/ui/app.js +867 -0
- package/ui/app.legacy.js +1464 -0
- package/ui/app.monolith.js +2488 -0
- package/ui/components/charts.js +226 -0
- package/ui/components/chat-view.js +567 -0
- package/ui/components/command-palette.js +587 -0
- package/ui/components/diff-viewer.js +190 -0
- package/ui/components/forms.js +327 -0
- package/ui/components/kanban-board.js +451 -0
- package/ui/components/session-list.js +305 -0
- package/ui/components/shared.js +473 -0
- package/ui/index.html +70 -0
- package/ui/modules/api.js +297 -0
- package/ui/modules/icons.js +461 -0
- package/ui/modules/router.js +81 -0
- package/ui/modules/settings-schema.js +261 -0
- package/ui/modules/state.js +679 -0
- package/ui/modules/telegram.js +331 -0
- package/ui/modules/utils.js +270 -0
- package/ui/styles/animations.css +140 -0
- package/ui/styles/base.css +98 -0
- package/ui/styles/components.css +1915 -0
- package/ui/styles/kanban.css +286 -0
- package/ui/styles/layout.css +809 -0
- package/ui/styles/sessions.css +827 -0
- package/ui/styles/variables.css +188 -0
- package/ui/styles.css +141 -0
- package/ui/styles.monolith.css +1046 -0
- package/ui/tabs/agents.js +1417 -0
- package/ui/tabs/chat.js +74 -0
- package/ui/tabs/control.js +887 -0
- package/ui/tabs/dashboard.js +515 -0
- package/ui/tabs/infra.js +537 -0
- package/ui/tabs/logs.js +783 -0
- package/ui/tabs/settings.js +1487 -0
- package/ui/tabs/tasks.js +1385 -0
- package/ui-server.mjs +4073 -0
- package/update-check.mjs +465 -0
- package/utils.mjs +172 -0
- package/ve-kanban.mjs +654 -0
- package/ve-kanban.ps1 +1365 -0
- package/ve-kanban.sh +18 -0
- package/ve-orchestrator.mjs +340 -0
- package/ve-orchestrator.ps1 +6546 -0
- package/ve-orchestrator.sh +18 -0
- package/vibe-kanban-wrapper.mjs +41 -0
- package/vk-error-resolver.mjs +470 -0
- package/vk-log-stream.mjs +914 -0
- package/whatsapp-channel.mjs +520 -0
- package/workspace-monitor.mjs +581 -0
- package/workspace-reaper.mjs +405 -0
- package/workspace-registry.mjs +238 -0
- package/worktree-manager.mjs +1266 -0
package/setup.mjs
ADDED
|
@@ -0,0 +1,3937 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* openfleet — Setup Wizard
|
|
5
|
+
*
|
|
6
|
+
* Interactive CLI that configures openfleet for a new or existing repository.
|
|
7
|
+
* Handles:
|
|
8
|
+
* - Prerequisites validation
|
|
9
|
+
* - Environment file generation (.env + openfleet.config.json)
|
|
10
|
+
* - Executor/model configuration (N executors with weights & failover)
|
|
11
|
+
* - Multi-repo setup (separate backend/frontend repos)
|
|
12
|
+
* - Vibe-Kanban auto-wiring (project, repos, executor profiles, agent appends)
|
|
13
|
+
* - Prompt template scaffolding (.openfleet/agents/*.md)
|
|
14
|
+
* - First-run auto-detection (launches automatically on virgin installs)
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* openfleet --setup # interactive wizard
|
|
18
|
+
* openfleet-setup # same (bin alias)
|
|
19
|
+
* npx @virtengine/openfleet setup
|
|
20
|
+
* node setup.mjs --non-interactive # use env vars, skip prompts
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { createInterface } from "node:readline";
|
|
24
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
25
|
+
import { resolve, dirname, basename, relative, isAbsolute } from "node:path";
|
|
26
|
+
import { execSync } from "node:child_process";
|
|
27
|
+
import { execFileSync } from "node:child_process";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import {
|
|
30
|
+
readCodexConfig,
|
|
31
|
+
getConfigPath,
|
|
32
|
+
hasVibeKanbanMcp,
|
|
33
|
+
auditStreamTimeouts,
|
|
34
|
+
ensureCodexConfig,
|
|
35
|
+
printConfigSummary,
|
|
36
|
+
} from "./codex-config.mjs";
|
|
37
|
+
import {
|
|
38
|
+
ensureAgentPromptWorkspace,
|
|
39
|
+
getAgentPromptDefinitions,
|
|
40
|
+
PROMPT_WORKSPACE_DIR,
|
|
41
|
+
} from "./agent-prompts.mjs";
|
|
42
|
+
import {
|
|
43
|
+
buildHookScaffoldOptionsFromEnv,
|
|
44
|
+
normalizeHookTargets,
|
|
45
|
+
scaffoldAgentHookFiles,
|
|
46
|
+
} from "./hook-profiles.mjs";
|
|
47
|
+
|
|
48
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
49
|
+
|
|
50
|
+
const isNonInteractive =
|
|
51
|
+
process.argv.includes("--non-interactive") || process.argv.includes("-y");
|
|
52
|
+
|
|
53
|
+
// ── Zero-dependency terminal styling (replaces chalk) ────────────────────────
|
|
54
|
+
const isTTY = process.stdout.isTTY;
|
|
55
|
+
const chalk = {
|
|
56
|
+
bold: (s) => (isTTY ? `\x1b[1m${s}\x1b[22m` : s),
|
|
57
|
+
dim: (s) => (isTTY ? `\x1b[2m${s}\x1b[22m` : s),
|
|
58
|
+
cyan: (s) => (isTTY ? `\x1b[36m${s}\x1b[39m` : s),
|
|
59
|
+
green: (s) => (isTTY ? `\x1b[32m${s}\x1b[39m` : s),
|
|
60
|
+
yellow: (s) => (isTTY ? `\x1b[33m${s}\x1b[39m` : s),
|
|
61
|
+
red: (s) => (isTTY ? `\x1b[31m${s}\x1b[39m` : s),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
function getVersion() {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(readFileSync(resolve(__dirname, "package.json"), "utf8"))
|
|
69
|
+
.version;
|
|
70
|
+
} catch {
|
|
71
|
+
return "0.0.0";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function hasSetupMarkers(dir) {
|
|
76
|
+
const markers = [
|
|
77
|
+
".env",
|
|
78
|
+
"openfleet.config.json",
|
|
79
|
+
".openfleet.json",
|
|
80
|
+
"openfleet.json",
|
|
81
|
+
];
|
|
82
|
+
return markers.some((name) => existsSync(resolve(dir, name)));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function hasConfigFiles(dir) {
|
|
86
|
+
const markers = [
|
|
87
|
+
"openfleet.config.json",
|
|
88
|
+
".openfleet.json",
|
|
89
|
+
"openfleet.json",
|
|
90
|
+
];
|
|
91
|
+
return markers.some((name) => existsSync(resolve(dir, name)));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isPathInside(parent, child) {
|
|
95
|
+
const rel = relative(parent, child);
|
|
96
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function resolveConfigDir(repoRoot) {
|
|
100
|
+
const explicit = process.env.CODEX_MONITOR_DIR;
|
|
101
|
+
if (explicit) return resolve(explicit);
|
|
102
|
+
const repoPath = resolve(repoRoot || process.cwd());
|
|
103
|
+
const packageDir = resolve(__dirname);
|
|
104
|
+
if (isPathInside(repoPath, packageDir) || hasConfigFiles(packageDir)) {
|
|
105
|
+
return packageDir;
|
|
106
|
+
}
|
|
107
|
+
const baseDir =
|
|
108
|
+
process.env.APPDATA ||
|
|
109
|
+
process.env.LOCALAPPDATA ||
|
|
110
|
+
process.env.HOME ||
|
|
111
|
+
process.env.USERPROFILE ||
|
|
112
|
+
process.cwd();
|
|
113
|
+
return resolve(baseDir, "openfleet");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function printBanner() {
|
|
117
|
+
const ver = getVersion();
|
|
118
|
+
const title = `Codex Monitor — Setup Wizard v${ver}`;
|
|
119
|
+
const pad = Math.max(0, 57 - title.length);
|
|
120
|
+
const left = Math.floor(pad / 2);
|
|
121
|
+
const right = pad - left;
|
|
122
|
+
console.log("");
|
|
123
|
+
console.log(
|
|
124
|
+
" ╔═══════════════════════════════════════════════════════════════╗",
|
|
125
|
+
);
|
|
126
|
+
console.log(` ║${" ".repeat(left + 3)}${title}${" ".repeat(right + 3)}║`);
|
|
127
|
+
console.log(
|
|
128
|
+
" ╚═══════════════════════════════════════════════════════════════╝",
|
|
129
|
+
);
|
|
130
|
+
console.log("");
|
|
131
|
+
console.log(
|
|
132
|
+
chalk.dim(" This wizard will configure openfleet for your project."),
|
|
133
|
+
);
|
|
134
|
+
console.log(
|
|
135
|
+
chalk.dim(" Press Enter to accept defaults shown in [brackets]."),
|
|
136
|
+
);
|
|
137
|
+
console.log("");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function heading(text) {
|
|
141
|
+
const line = "\u2500".repeat(Math.max(0, 59 - text.length));
|
|
142
|
+
console.log(`\n ${chalk.bold(text)} ${chalk.dim(line)}\n`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function check(label, ok, hint) {
|
|
146
|
+
const icon = ok ? "✅" : "❌";
|
|
147
|
+
console.log(` ${icon} ${label}`);
|
|
148
|
+
if (!ok && hint) console.log(` → ${hint}`);
|
|
149
|
+
return ok;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function info(msg) {
|
|
153
|
+
console.log(` ℹ️ ${msg}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function success(msg) {
|
|
157
|
+
console.log(` ✅ ${msg}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function warn(msg) {
|
|
161
|
+
console.log(` ⚠️ ${msg}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function commandExists(cmd) {
|
|
165
|
+
try {
|
|
166
|
+
execSync(`${process.platform === "win32" ? "where" : "which"} ${cmd}`, {
|
|
167
|
+
stdio: "ignore",
|
|
168
|
+
});
|
|
169
|
+
return true;
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeBaseUrl(raw) {
|
|
176
|
+
const trimmed = String(raw || "").trim();
|
|
177
|
+
if (!trimmed) return "";
|
|
178
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
179
|
+
return trimmed.replace(/\/+$/, "");
|
|
180
|
+
}
|
|
181
|
+
return `https://${trimmed.replace(/\/+$/, "")}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function openUrlInBrowser(url) {
|
|
185
|
+
const target = String(url || "").trim();
|
|
186
|
+
if (!target) return false;
|
|
187
|
+
const escaped = target.replace(/"/g, '\\"');
|
|
188
|
+
try {
|
|
189
|
+
if (process.platform === "darwin") {
|
|
190
|
+
execSync(`open "${escaped}"`);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
if (process.platform === "win32") {
|
|
194
|
+
execSync(`cmd /c start "" "${escaped}"`);
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
if (commandExists("xdg-open")) {
|
|
198
|
+
execSync(`xdg-open "${escaped}"`);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
if (commandExists("gio")) {
|
|
202
|
+
execSync(`gio open "${escaped}"`);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function buildJiraAuthHeaders(email, token) {
|
|
212
|
+
const credentials = Buffer.from(`${email}:${token}`).toString("base64");
|
|
213
|
+
return {
|
|
214
|
+
Authorization: `Basic ${credentials}`,
|
|
215
|
+
Accept: "application/json",
|
|
216
|
+
"Content-Type": "application/json",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function jiraRequest({ baseUrl, email, token, path, method = "GET", body }) {
|
|
221
|
+
if (!baseUrl || !email || !token) {
|
|
222
|
+
throw new Error("Jira credentials are missing");
|
|
223
|
+
}
|
|
224
|
+
const url = `${baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
225
|
+
const response = await fetch(url, {
|
|
226
|
+
method,
|
|
227
|
+
headers: buildJiraAuthHeaders(email, token),
|
|
228
|
+
body: body == null ? undefined : JSON.stringify(body),
|
|
229
|
+
});
|
|
230
|
+
if (!response || typeof response.status !== "number") {
|
|
231
|
+
throw new Error(`Jira API ${method} ${path} failed: no HTTP response`);
|
|
232
|
+
}
|
|
233
|
+
if (response.status === 204) return null;
|
|
234
|
+
const contentType = String(response.headers.get("content-type") || "");
|
|
235
|
+
let payload = null;
|
|
236
|
+
if (contentType.includes("application/json")) {
|
|
237
|
+
payload = await response.json().catch(() => null);
|
|
238
|
+
} else {
|
|
239
|
+
payload = await response.text().catch(() => "");
|
|
240
|
+
}
|
|
241
|
+
if (!response.ok) {
|
|
242
|
+
const message =
|
|
243
|
+
payload?.errorMessages?.join("; ") ||
|
|
244
|
+
(payload?.errors ? Object.values(payload.errors || {}).join("; ") : "");
|
|
245
|
+
throw new Error(
|
|
246
|
+
`Jira API ${method} ${path} failed (${response.status}): ${message || response.statusText || "Unknown error"}`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return payload;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function listJiraProjects({ baseUrl, email, token }) {
|
|
253
|
+
const projects = [];
|
|
254
|
+
let startAt = 0;
|
|
255
|
+
while (true) {
|
|
256
|
+
const page = await jiraRequest({
|
|
257
|
+
baseUrl,
|
|
258
|
+
email,
|
|
259
|
+
token,
|
|
260
|
+
path: `/rest/api/3/project/search?startAt=${startAt}&maxResults=50&orderBy=name`,
|
|
261
|
+
});
|
|
262
|
+
const values = Array.isArray(page?.values) ? page.values : [];
|
|
263
|
+
projects.push(...values);
|
|
264
|
+
if (values.length === 0 || page?.isLast) break;
|
|
265
|
+
startAt += values.length;
|
|
266
|
+
}
|
|
267
|
+
return projects.map((project) => ({
|
|
268
|
+
key: String(project.key || project.id || "").trim(),
|
|
269
|
+
name: project.name || project.key || "Unnamed Jira Project",
|
|
270
|
+
id: String(project.id || project.key || ""),
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function listJiraIssueTypes({ baseUrl, email, token }) {
|
|
275
|
+
const data = await jiraRequest({
|
|
276
|
+
baseUrl,
|
|
277
|
+
email,
|
|
278
|
+
token,
|
|
279
|
+
path: "/rest/api/3/issuetype",
|
|
280
|
+
});
|
|
281
|
+
return (Array.isArray(data) ? data : [])
|
|
282
|
+
.map((entry) => ({
|
|
283
|
+
id: String(entry?.id || ""),
|
|
284
|
+
name: String(entry?.name || "").trim(),
|
|
285
|
+
subtask: Boolean(entry?.subtask),
|
|
286
|
+
}))
|
|
287
|
+
.filter((entry) => entry.name);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function listJiraFields({ baseUrl, email, token }) {
|
|
291
|
+
const data = await jiraRequest({
|
|
292
|
+
baseUrl,
|
|
293
|
+
email,
|
|
294
|
+
token,
|
|
295
|
+
path: "/rest/api/3/field",
|
|
296
|
+
});
|
|
297
|
+
return (Array.isArray(data) ? data : [])
|
|
298
|
+
.map((field) => ({
|
|
299
|
+
id: String(field?.id || "").trim(),
|
|
300
|
+
name: String(field?.name || "").trim(),
|
|
301
|
+
custom: String(field?.id || "").startsWith("customfield_"),
|
|
302
|
+
}))
|
|
303
|
+
.filter((field) => field.id && field.name);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function searchJiraUsers({ baseUrl, email, token, query }) {
|
|
307
|
+
const data = await jiraRequest({
|
|
308
|
+
baseUrl,
|
|
309
|
+
email,
|
|
310
|
+
token,
|
|
311
|
+
path: `/rest/api/3/user/search?maxResults=20&query=${encodeURIComponent(
|
|
312
|
+
String(query || "").trim(),
|
|
313
|
+
)}`,
|
|
314
|
+
});
|
|
315
|
+
return (Array.isArray(data) ? data : []).map((user) => ({
|
|
316
|
+
accountId: String(user?.accountId || ""),
|
|
317
|
+
displayName: user?.displayName || "",
|
|
318
|
+
emailAddress: user?.emailAddress || "",
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function isSubtaskIssueType(issueType) {
|
|
323
|
+
const name = String(issueType || "")
|
|
324
|
+
.trim()
|
|
325
|
+
.toLowerCase();
|
|
326
|
+
return name.includes("subtask") || name.includes("sub-task");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function getScriptRuntimePrerequisiteStatus(
|
|
330
|
+
platform = process.platform,
|
|
331
|
+
checker = commandExists,
|
|
332
|
+
) {
|
|
333
|
+
if (platform === "win32") {
|
|
334
|
+
return {
|
|
335
|
+
required: {
|
|
336
|
+
label: "PowerShell (pwsh)",
|
|
337
|
+
command: "pwsh",
|
|
338
|
+
ok: checker("pwsh"),
|
|
339
|
+
hint: "Install: https://github.com/PowerShell/PowerShell",
|
|
340
|
+
},
|
|
341
|
+
optionalPwsh: null,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
required: {
|
|
347
|
+
label: "bash",
|
|
348
|
+
command: "bash",
|
|
349
|
+
ok: checker("bash"),
|
|
350
|
+
hint: "Install bash via your system package manager",
|
|
351
|
+
},
|
|
352
|
+
optionalPwsh: {
|
|
353
|
+
label: "PowerShell (pwsh)",
|
|
354
|
+
command: "pwsh",
|
|
355
|
+
ok: checker("pwsh"),
|
|
356
|
+
hint: "Optional on macOS/Linux (needed only for .ps1 scripts)",
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function getDefaultOrchestratorScripts(
|
|
362
|
+
platform = process.platform,
|
|
363
|
+
baseDir = __dirname,
|
|
364
|
+
) {
|
|
365
|
+
const variants = ["ps1", "sh"]
|
|
366
|
+
.map((ext) => {
|
|
367
|
+
const orchestratorPath = resolve(baseDir, `ve-orchestrator.${ext}`);
|
|
368
|
+
const kanbanPath = resolve(baseDir, `ve-kanban.${ext}`);
|
|
369
|
+
return {
|
|
370
|
+
ext,
|
|
371
|
+
orchestratorPath,
|
|
372
|
+
kanbanPath,
|
|
373
|
+
available: existsSync(orchestratorPath) && existsSync(kanbanPath),
|
|
374
|
+
};
|
|
375
|
+
})
|
|
376
|
+
.filter((variant) => variant.available);
|
|
377
|
+
|
|
378
|
+
const preferredExt = platform === "win32" ? "ps1" : "sh";
|
|
379
|
+
const selectedDefault =
|
|
380
|
+
variants.find((variant) => variant.ext === preferredExt) || variants[0] || null;
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
preferredExt,
|
|
384
|
+
variants,
|
|
385
|
+
selectedDefault,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function formatOrchestratorScriptForEnv(
|
|
390
|
+
scriptPath,
|
|
391
|
+
configDir = __dirname,
|
|
392
|
+
) {
|
|
393
|
+
const raw = String(scriptPath || "").trim();
|
|
394
|
+
if (!raw) return "";
|
|
395
|
+
|
|
396
|
+
const absolutePath = isAbsolute(raw) ? raw : resolve(configDir, raw);
|
|
397
|
+
const relativePath = relative(configDir, absolutePath);
|
|
398
|
+
if (!relativePath || relativePath === ".") {
|
|
399
|
+
return `./${basename(absolutePath)}`.replace(/\\/g, "/");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (isAbsolute(relativePath)) {
|
|
403
|
+
return absolutePath.replace(/\\/g, "/");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
407
|
+
if (normalized.startsWith(".") || normalized.startsWith("..")) {
|
|
408
|
+
return normalized;
|
|
409
|
+
}
|
|
410
|
+
return `./${normalized}`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function resolveSetupOrchestratorDefaults({
|
|
414
|
+
platform = process.platform,
|
|
415
|
+
repoRoot = process.cwd(),
|
|
416
|
+
configDir = __dirname,
|
|
417
|
+
packageDir = __dirname,
|
|
418
|
+
} = {}) {
|
|
419
|
+
const repoScriptDefaults = getDefaultOrchestratorScripts(
|
|
420
|
+
platform,
|
|
421
|
+
resolve(repoRoot, "scripts", "openfleet"),
|
|
422
|
+
);
|
|
423
|
+
const packageScriptDefaults = getDefaultOrchestratorScripts(
|
|
424
|
+
platform,
|
|
425
|
+
packageDir,
|
|
426
|
+
);
|
|
427
|
+
const orchestratorDefaults =
|
|
428
|
+
[repoScriptDefaults, packageScriptDefaults].find((defaults) =>
|
|
429
|
+
defaults.variants.some(
|
|
430
|
+
(variant) => variant.ext === defaults.preferredExt,
|
|
431
|
+
),
|
|
432
|
+
) ||
|
|
433
|
+
[repoScriptDefaults, packageScriptDefaults].find(
|
|
434
|
+
(defaults) => defaults.variants.length > 0,
|
|
435
|
+
) ||
|
|
436
|
+
packageScriptDefaults;
|
|
437
|
+
const selectedDefault = orchestratorDefaults.selectedDefault;
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
repoScriptDefaults,
|
|
441
|
+
packageScriptDefaults,
|
|
442
|
+
orchestratorDefaults,
|
|
443
|
+
selectedDefault,
|
|
444
|
+
orchestratorScriptEnvValue: selectedDefault
|
|
445
|
+
? formatOrchestratorScriptForEnv(selectedDefault.orchestratorPath, configDir)
|
|
446
|
+
: "",
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function parseEnvAssignmentLine(line) {
|
|
451
|
+
const raw = String(line || "").trim();
|
|
452
|
+
if (!raw || raw.startsWith("#")) return null;
|
|
453
|
+
const normalized = raw.startsWith("export ") ? raw.slice(7).trim() : raw;
|
|
454
|
+
const match = normalized.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
455
|
+
if (!match) return null;
|
|
456
|
+
|
|
457
|
+
const key = match[1];
|
|
458
|
+
let value = match[2] ?? "";
|
|
459
|
+
if (
|
|
460
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
461
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
462
|
+
) {
|
|
463
|
+
const quote = value[0];
|
|
464
|
+
value = value.slice(1, -1);
|
|
465
|
+
if (quote === '"') {
|
|
466
|
+
value = value
|
|
467
|
+
.replace(/\\n/g, "\n")
|
|
468
|
+
.replace(/\\r/g, "\r")
|
|
469
|
+
.replace(/\\t/g, "\t")
|
|
470
|
+
.replace(/\\"/g, '"')
|
|
471
|
+
.replace(/\\\\/g, "\\");
|
|
472
|
+
}
|
|
473
|
+
} else {
|
|
474
|
+
const hashIdx = value.indexOf("#");
|
|
475
|
+
if (hashIdx >= 0) {
|
|
476
|
+
value = value.slice(0, hashIdx).trimEnd();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return { key, value };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function applyEnvFileToProcess(envPath, options = {}) {
|
|
484
|
+
const override = Boolean(options.override);
|
|
485
|
+
const result = {
|
|
486
|
+
path: envPath,
|
|
487
|
+
found: false,
|
|
488
|
+
loaded: 0,
|
|
489
|
+
skipped: 0,
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
if (!envPath || !existsSync(envPath)) {
|
|
493
|
+
return result;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
result.found = true;
|
|
497
|
+
const content = readFileSync(envPath, "utf8");
|
|
498
|
+
for (const line of content.split(/\r?\n/)) {
|
|
499
|
+
const parsed = parseEnvAssignmentLine(line);
|
|
500
|
+
if (!parsed) continue;
|
|
501
|
+
if (!override && process.env[parsed.key] !== undefined) {
|
|
502
|
+
result.skipped += 1;
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
process.env[parsed.key] = parsed.value;
|
|
506
|
+
result.loaded += 1;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Check if a binary exists in the package's own node_modules/.bin/.
|
|
514
|
+
* When installed globally, npm only symlinks the top-level package's bin
|
|
515
|
+
* entries to the global path — transitive dependency binaries (like
|
|
516
|
+
* vibe-kanban) live here instead.
|
|
517
|
+
*/
|
|
518
|
+
function bundledBinExists(cmd) {
|
|
519
|
+
const base = resolve(__dirname, "node_modules", ".bin", cmd);
|
|
520
|
+
return existsSync(base) || existsSync(base + ".cmd");
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function detectRepoSlug(cwd) {
|
|
524
|
+
try {
|
|
525
|
+
const remote = execSync("git remote get-url origin", {
|
|
526
|
+
encoding: "utf8",
|
|
527
|
+
cwd: cwd || process.cwd(),
|
|
528
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
529
|
+
}).trim();
|
|
530
|
+
const match = remote.match(/github\.com[/:]([^/]+\/[^/.]+)/);
|
|
531
|
+
return match ? match[1] : null;
|
|
532
|
+
} catch {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function detectRepoRoot(cwd) {
|
|
538
|
+
try {
|
|
539
|
+
return execSync("git rev-parse --show-toplevel", {
|
|
540
|
+
encoding: "utf8",
|
|
541
|
+
cwd: cwd || process.cwd(),
|
|
542
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
543
|
+
}).trim();
|
|
544
|
+
} catch {
|
|
545
|
+
return cwd || process.cwd();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function detectProjectName(repoRoot) {
|
|
550
|
+
const pkgPath = resolve(repoRoot, "package.json");
|
|
551
|
+
if (existsSync(pkgPath)) {
|
|
552
|
+
try {
|
|
553
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
554
|
+
if (pkg.name) return pkg.name.replace(/^@[^/]+\//, "");
|
|
555
|
+
} catch {
|
|
556
|
+
/* skip */
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return basename(repoRoot);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function runGhCommand(args, cwd) {
|
|
563
|
+
const normalizedArgs = Array.isArray(args)
|
|
564
|
+
? args.map((entry) => String(entry))
|
|
565
|
+
: [];
|
|
566
|
+
const output = execFileSync("gh", normalizedArgs, {
|
|
567
|
+
encoding: "utf8",
|
|
568
|
+
cwd: cwd || process.cwd(),
|
|
569
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
570
|
+
});
|
|
571
|
+
return String(output || "").trim();
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function formatGhErrorReason(err) {
|
|
575
|
+
if (!err) return "";
|
|
576
|
+
const stderr = String(err.stderr || "").trim();
|
|
577
|
+
const stdout = String(err.stdout || "").trim();
|
|
578
|
+
const message = String(err.message || "").trim();
|
|
579
|
+
return stderr || stdout || message;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function detectGitHubUserLogin(cwd) {
|
|
583
|
+
try {
|
|
584
|
+
return runGhCommand(["api", "user", "--jq", ".login"], cwd);
|
|
585
|
+
} catch {
|
|
586
|
+
return "";
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function collectProjectCandidates(node, out) {
|
|
591
|
+
if (node === null || node === undefined) return;
|
|
592
|
+
if (Array.isArray(node)) {
|
|
593
|
+
for (const item of node) collectProjectCandidates(item, out);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (typeof node !== "object") return;
|
|
597
|
+
|
|
598
|
+
if (
|
|
599
|
+
Object.prototype.hasOwnProperty.call(node, "title") ||
|
|
600
|
+
Object.prototype.hasOwnProperty.call(node, "number") ||
|
|
601
|
+
Object.prototype.hasOwnProperty.call(node, "url") ||
|
|
602
|
+
Object.prototype.hasOwnProperty.call(node, "projectNumber")
|
|
603
|
+
) {
|
|
604
|
+
out.push(node);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
for (const value of Object.values(node)) {
|
|
608
|
+
if (value && (Array.isArray(value) || typeof value === "object")) {
|
|
609
|
+
collectProjectCandidates(value, out);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function parseGitHubProjectList(rawOutput) {
|
|
615
|
+
const rawText = String(rawOutput || "").trim();
|
|
616
|
+
if (!rawText) return [];
|
|
617
|
+
|
|
618
|
+
let parsed;
|
|
619
|
+
try {
|
|
620
|
+
parsed = JSON.parse(rawText);
|
|
621
|
+
} catch {
|
|
622
|
+
return [];
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const candidates = [];
|
|
626
|
+
collectProjectCandidates(parsed, candidates);
|
|
627
|
+
return candidates;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function extractProjectNumberFromText(value) {
|
|
631
|
+
const text = String(value || "").trim();
|
|
632
|
+
if (!text) return "";
|
|
633
|
+
if (/^\d+$/.test(text)) return text;
|
|
634
|
+
|
|
635
|
+
const patterns = [
|
|
636
|
+
/\/projects\/(\d+)(?:\b|$)/i,
|
|
637
|
+
/\/projects\/v2\/(\d+)(?:\b|$)/i,
|
|
638
|
+
/\bproject\s*(?:number|id)?\s*[:#=-]?\s*(\d+)\b/i,
|
|
639
|
+
/\bnumber\s*[:#=-]\s*(\d+)\b/i,
|
|
640
|
+
];
|
|
641
|
+
|
|
642
|
+
for (const pattern of patterns) {
|
|
643
|
+
const match = text.match(pattern);
|
|
644
|
+
if (match && match[1]) return match[1];
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (/project/i.test(text)) {
|
|
648
|
+
const fallback = text.match(/\b(\d+)\b/);
|
|
649
|
+
if (fallback && fallback[1]) return fallback[1];
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return "";
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function extractProjectNumber(value) {
|
|
656
|
+
if (value === null || value === undefined) return "";
|
|
657
|
+
|
|
658
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
659
|
+
const normalized = Math.trunc(value);
|
|
660
|
+
return normalized > 0 ? String(normalized) : "";
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (typeof value === "string") {
|
|
664
|
+
return extractProjectNumberFromText(value);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (typeof value === "object") {
|
|
668
|
+
const keys = [
|
|
669
|
+
"number",
|
|
670
|
+
"projectNumber",
|
|
671
|
+
"project_number",
|
|
672
|
+
"url",
|
|
673
|
+
"resourcePath",
|
|
674
|
+
"html_url",
|
|
675
|
+
"id",
|
|
676
|
+
"text",
|
|
677
|
+
"message",
|
|
678
|
+
];
|
|
679
|
+
for (const key of keys) {
|
|
680
|
+
const nested = extractProjectNumber(value?.[key]);
|
|
681
|
+
if (nested) return nested;
|
|
682
|
+
}
|
|
683
|
+
return extractProjectNumberFromText(JSON.stringify(value));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return "";
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function resolveOrCreateGitHubProject({
|
|
690
|
+
owner,
|
|
691
|
+
title,
|
|
692
|
+
cwd,
|
|
693
|
+
repoOwner,
|
|
694
|
+
githubLogin,
|
|
695
|
+
runCommand = runGhCommand,
|
|
696
|
+
}) {
|
|
697
|
+
const normalizedOwner = String(owner || "").trim();
|
|
698
|
+
const normalizedRepoOwner = String(repoOwner || "").trim();
|
|
699
|
+
const normalizedGithubLogin = String(githubLogin || "").trim();
|
|
700
|
+
const normalizedTitle = String(title || "").trim();
|
|
701
|
+
if (!normalizedTitle) {
|
|
702
|
+
return {
|
|
703
|
+
number: "",
|
|
704
|
+
owner: "",
|
|
705
|
+
reason: "missing GitHub Project title",
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const ownerCandidates = [];
|
|
710
|
+
for (const candidate of [
|
|
711
|
+
normalizedOwner,
|
|
712
|
+
normalizedGithubLogin,
|
|
713
|
+
normalizedRepoOwner,
|
|
714
|
+
]) {
|
|
715
|
+
if (!candidate) continue;
|
|
716
|
+
if (!ownerCandidates.includes(candidate)) ownerCandidates.push(candidate);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (ownerCandidates.length === 0) {
|
|
720
|
+
return {
|
|
721
|
+
number: "",
|
|
722
|
+
owner: "",
|
|
723
|
+
reason: "missing GitHub Project owner",
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const reasons = [];
|
|
728
|
+
const normalizedTitleLower = normalizedTitle.toLowerCase();
|
|
729
|
+
|
|
730
|
+
for (const candidateOwner of ownerCandidates) {
|
|
731
|
+
let listFailed = false;
|
|
732
|
+
let hadListProjects = false;
|
|
733
|
+
|
|
734
|
+
try {
|
|
735
|
+
const listRaw = runCommand(
|
|
736
|
+
["project", "list", "--owner", candidateOwner, "--format", "json"],
|
|
737
|
+
cwd,
|
|
738
|
+
);
|
|
739
|
+
const projects = parseGitHubProjectList(listRaw);
|
|
740
|
+
hadListProjects = projects.length > 0;
|
|
741
|
+
|
|
742
|
+
const existing = projects.find(
|
|
743
|
+
(project) =>
|
|
744
|
+
String(project?.title || "")
|
|
745
|
+
.trim()
|
|
746
|
+
.toLowerCase() === normalizedTitleLower,
|
|
747
|
+
);
|
|
748
|
+
const existingNumber = extractProjectNumber(existing);
|
|
749
|
+
if (existingNumber) {
|
|
750
|
+
return {
|
|
751
|
+
number: existingNumber,
|
|
752
|
+
owner: candidateOwner,
|
|
753
|
+
reason: "",
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
} catch (err) {
|
|
757
|
+
listFailed = true;
|
|
758
|
+
const reason = formatGhErrorReason(err);
|
|
759
|
+
reasons.push(
|
|
760
|
+
reason
|
|
761
|
+
? `list failed for owner '${candidateOwner}': ${reason}`
|
|
762
|
+
: `list failed for owner '${candidateOwner}'`,
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
try {
|
|
767
|
+
const createRaw = runCommand(
|
|
768
|
+
[
|
|
769
|
+
"project",
|
|
770
|
+
"create",
|
|
771
|
+
"--owner",
|
|
772
|
+
candidateOwner,
|
|
773
|
+
"--title",
|
|
774
|
+
normalizedTitle,
|
|
775
|
+
],
|
|
776
|
+
cwd,
|
|
777
|
+
);
|
|
778
|
+
const createdNumber = extractProjectNumber(createRaw);
|
|
779
|
+
if (createdNumber) {
|
|
780
|
+
return {
|
|
781
|
+
number: createdNumber,
|
|
782
|
+
owner: candidateOwner,
|
|
783
|
+
reason: "",
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
reasons.push(
|
|
788
|
+
`create returned no project number for owner '${candidateOwner}'`,
|
|
789
|
+
);
|
|
790
|
+
} catch (err) {
|
|
791
|
+
const reason = formatGhErrorReason(err);
|
|
792
|
+
const context = listFailed
|
|
793
|
+
? "list+create"
|
|
794
|
+
: hadListProjects
|
|
795
|
+
? "create"
|
|
796
|
+
: "create";
|
|
797
|
+
reasons.push(
|
|
798
|
+
reason
|
|
799
|
+
? `${context} failed for owner '${candidateOwner}': ${reason}`
|
|
800
|
+
: `${context} failed for owner '${candidateOwner}'`,
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return {
|
|
806
|
+
number: "",
|
|
807
|
+
owner: ownerCandidates[0] || "",
|
|
808
|
+
reason:
|
|
809
|
+
reasons.find(Boolean) ||
|
|
810
|
+
"no matching project found and project creation failed",
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function resolveOrCreateGitHubProjectNumber(options) {
|
|
815
|
+
return resolveOrCreateGitHubProject(options).number;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function getDefaultPromptOverrides() {
|
|
819
|
+
const entries = getAgentPromptDefinitions().map((def) => [
|
|
820
|
+
def.key,
|
|
821
|
+
`${PROMPT_WORKSPACE_DIR}/${def.filename}`,
|
|
822
|
+
]);
|
|
823
|
+
return Object.fromEntries(entries);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function ensureRepoGitIgnoreEntry(repoRoot, entry) {
|
|
827
|
+
const gitignorePath = resolve(repoRoot, ".gitignore");
|
|
828
|
+
const normalizedEntry = String(entry || "").trim();
|
|
829
|
+
if (!normalizedEntry) return false;
|
|
830
|
+
|
|
831
|
+
let existing = "";
|
|
832
|
+
if (existsSync(gitignorePath)) {
|
|
833
|
+
existing = readFileSync(gitignorePath, "utf8");
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const hasEntry = existing
|
|
837
|
+
.split(/\r?\n/)
|
|
838
|
+
.map((line) => line.trim())
|
|
839
|
+
.includes(normalizedEntry);
|
|
840
|
+
if (hasEntry) return false;
|
|
841
|
+
|
|
842
|
+
const next =
|
|
843
|
+
existing.endsWith("\n") || !existing ? existing : `${existing}\n`;
|
|
844
|
+
writeFileSync(gitignorePath, `${next}${normalizedEntry}\n`, "utf8");
|
|
845
|
+
return true;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function buildRecommendedVsCodeSettings(env = {}) {
|
|
849
|
+
const maxRequests = Math.max(
|
|
850
|
+
50,
|
|
851
|
+
Number(env.COPILOT_AGENT_MAX_REQUESTS || process.env.COPILOT_AGENT_MAX_REQUESTS || 500),
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
return {
|
|
855
|
+
"github.copilot.chat.searchSubagent.enabled": true,
|
|
856
|
+
"github.copilot.chat.switchAgent.enabled": true,
|
|
857
|
+
"github.copilot.chat.cli.customAgents.enabled": true,
|
|
858
|
+
"github.copilot.chat.cli.mcp.enabled": true,
|
|
859
|
+
"github.copilot.chat.agent.enabled": true,
|
|
860
|
+
"github.copilot.chat.agent.maxRequests": maxRequests,
|
|
861
|
+
"github.copilot.chat.thinking.collapsedTools": "withThinking",
|
|
862
|
+
"github.copilot.chat.thinking.generateTitles": true,
|
|
863
|
+
"github.copilot.chat.confirmEditRequestRemoval": false,
|
|
864
|
+
"github.copilot.chat.confirmRetryRequestRemoval": false,
|
|
865
|
+
"github.copilot.chat.terminal.enableAutoApprove": true,
|
|
866
|
+
"github.copilot.chat.terminal.autoReplyToPrompts": true,
|
|
867
|
+
"github.copilot.chat.tools.autoApprove": true,
|
|
868
|
+
"github.copilot.chat.tools.runSubagent.enabled": true,
|
|
869
|
+
"github.copilot.chat.tools.searchSubagent.enabled": true,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function mergePlainObjects(base, updates) {
|
|
874
|
+
const out = { ...(base || {}) };
|
|
875
|
+
for (const [key, value] of Object.entries(updates || {})) {
|
|
876
|
+
if (
|
|
877
|
+
value &&
|
|
878
|
+
typeof value === "object" &&
|
|
879
|
+
!Array.isArray(value) &&
|
|
880
|
+
out[key] &&
|
|
881
|
+
typeof out[key] === "object" &&
|
|
882
|
+
!Array.isArray(out[key])
|
|
883
|
+
) {
|
|
884
|
+
out[key] = mergePlainObjects(out[key], value);
|
|
885
|
+
} else {
|
|
886
|
+
out[key] = value;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return out;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function writeWorkspaceVsCodeSettings(repoRoot, env) {
|
|
893
|
+
try {
|
|
894
|
+
const vscodeDir = resolve(repoRoot, ".vscode");
|
|
895
|
+
const settingsPath = resolve(vscodeDir, "settings.json");
|
|
896
|
+
mkdirSync(vscodeDir, { recursive: true });
|
|
897
|
+
|
|
898
|
+
let existing = {};
|
|
899
|
+
if (existsSync(settingsPath)) {
|
|
900
|
+
try {
|
|
901
|
+
existing = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
902
|
+
} catch {
|
|
903
|
+
existing = {};
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const recommended = buildRecommendedVsCodeSettings(env);
|
|
908
|
+
const merged = mergePlainObjects(existing, recommended);
|
|
909
|
+
writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
910
|
+
return { path: settingsPath, updated: true };
|
|
911
|
+
} catch (err) {
|
|
912
|
+
return { path: null, updated: false, error: err.message };
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function buildRecommendedCopilotMcpServers() {
|
|
917
|
+
return {
|
|
918
|
+
context7: {
|
|
919
|
+
command: "npx",
|
|
920
|
+
args: ["-y", "@upstash/context7-mcp"],
|
|
921
|
+
},
|
|
922
|
+
"sequential-thinking": {
|
|
923
|
+
command: "npx",
|
|
924
|
+
args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
|
|
925
|
+
},
|
|
926
|
+
playwright: {
|
|
927
|
+
command: "npx",
|
|
928
|
+
args: ["-y", "@playwright/mcp@latest"],
|
|
929
|
+
},
|
|
930
|
+
"microsoft-docs": {
|
|
931
|
+
url: "https://learn.microsoft.com/api/mcp",
|
|
932
|
+
},
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function writeWorkspaceCopilotMcpConfig(repoRoot) {
|
|
937
|
+
try {
|
|
938
|
+
const vscodeDir = resolve(repoRoot, ".vscode");
|
|
939
|
+
const mcpPath = resolve(vscodeDir, "mcp.json");
|
|
940
|
+
mkdirSync(vscodeDir, { recursive: true });
|
|
941
|
+
|
|
942
|
+
let existing = {};
|
|
943
|
+
if (existsSync(mcpPath)) {
|
|
944
|
+
try {
|
|
945
|
+
existing = JSON.parse(readFileSync(mcpPath, "utf8"));
|
|
946
|
+
} catch {
|
|
947
|
+
existing = {};
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const existingServers =
|
|
952
|
+
existing.mcpServers ||
|
|
953
|
+
existing["github.copilot.mcpServers"] ||
|
|
954
|
+
existing;
|
|
955
|
+
|
|
956
|
+
const recommended = buildRecommendedCopilotMcpServers();
|
|
957
|
+
const mergedServers = {
|
|
958
|
+
...recommended,
|
|
959
|
+
...(typeof existingServers === "object" ? existingServers : {}),
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
const next = { mcpServers: mergedServers };
|
|
963
|
+
writeFileSync(mcpPath, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
964
|
+
return { path: mcpPath, updated: true };
|
|
965
|
+
} catch (err) {
|
|
966
|
+
return { path: null, updated: false, error: err.message };
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function parseHookCommandInput(rawValue) {
|
|
971
|
+
const raw = String(rawValue || "").trim();
|
|
972
|
+
if (!raw) return null;
|
|
973
|
+
const lowered = raw.toLowerCase();
|
|
974
|
+
if (["none", "off", "disable", "disabled"].includes(lowered)) {
|
|
975
|
+
return [];
|
|
976
|
+
}
|
|
977
|
+
return raw
|
|
978
|
+
.split(/\s*;;\s*|\r?\n/)
|
|
979
|
+
.map((part) => part.trim())
|
|
980
|
+
.filter(Boolean);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function printHookScaffoldSummary(result) {
|
|
984
|
+
if (!result || !result.enabled) {
|
|
985
|
+
info("Agent hook scaffolding disabled.");
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const totalChanged = result.written.length + result.updated.length;
|
|
990
|
+
if (totalChanged > 0) {
|
|
991
|
+
success(`Configured ${totalChanged} agent hook file(s).`);
|
|
992
|
+
} else {
|
|
993
|
+
info("Agent hook files already existed — no file changes needed.");
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (result.written.length > 0) {
|
|
997
|
+
for (const path of result.written) {
|
|
998
|
+
console.log(` + ${path}`);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
if (result.updated.length > 0) {
|
|
1002
|
+
for (const path of result.updated) {
|
|
1003
|
+
console.log(` ~ ${path}`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
if (result.skipped.length > 0) {
|
|
1007
|
+
for (const path of result.skipped) {
|
|
1008
|
+
console.log(` = ${path} (kept existing)`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
if (result.warnings.length > 0) {
|
|
1012
|
+
for (const warning of result.warnings) {
|
|
1013
|
+
warn(warning);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// ── Prompt System ────────────────────────────────────────────────────────────
|
|
1019
|
+
|
|
1020
|
+
function createPrompt() {
|
|
1021
|
+
// Fix for Windows PowerShell readline issues
|
|
1022
|
+
// Only use terminal mode if stdin is actually a TTY
|
|
1023
|
+
// This prevents both double-echo and output duplication
|
|
1024
|
+
const rl = createInterface({
|
|
1025
|
+
input: process.stdin,
|
|
1026
|
+
output: process.stdout,
|
|
1027
|
+
terminal: process.stdin.isTTY && process.stdout.isTTY,
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
return {
|
|
1031
|
+
ask(question, defaultValue) {
|
|
1032
|
+
return new Promise((res) => {
|
|
1033
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
1034
|
+
rl.question(` ${question}${suffix}: `, (answer) => {
|
|
1035
|
+
res(answer.trim() || defaultValue || "");
|
|
1036
|
+
});
|
|
1037
|
+
});
|
|
1038
|
+
},
|
|
1039
|
+
confirm(question, defaultYes = true) {
|
|
1040
|
+
return new Promise((res) => {
|
|
1041
|
+
const hint = defaultYes ? "[Y/n]" : "[y/N]";
|
|
1042
|
+
rl.question(` ${question} ${hint}: `, (answer) => {
|
|
1043
|
+
const a = answer.trim().toLowerCase();
|
|
1044
|
+
if (!a) res(defaultYes);
|
|
1045
|
+
else res(a === "y" || a === "yes");
|
|
1046
|
+
});
|
|
1047
|
+
});
|
|
1048
|
+
},
|
|
1049
|
+
choose(question, options, defaultIdx = 0) {
|
|
1050
|
+
return new Promise((res) => {
|
|
1051
|
+
console.log(` ${question}`);
|
|
1052
|
+
options.forEach((opt, i) => {
|
|
1053
|
+
const marker = i === defaultIdx ? "→" : " ";
|
|
1054
|
+
console.log(` ${marker} ${i + 1}) ${opt}`);
|
|
1055
|
+
});
|
|
1056
|
+
rl.question(` Choice [${defaultIdx + 1}]: `, (answer) => {
|
|
1057
|
+
const idx = answer.trim() ? Number(answer.trim()) - 1 : defaultIdx;
|
|
1058
|
+
res(Math.max(0, Math.min(idx, options.length - 1)));
|
|
1059
|
+
});
|
|
1060
|
+
});
|
|
1061
|
+
},
|
|
1062
|
+
close() {
|
|
1063
|
+
rl.close();
|
|
1064
|
+
},
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// ── Executor Templates ───────────────────────────────────────────────────────
|
|
1069
|
+
|
|
1070
|
+
const EXECUTOR_PRESETS = {
|
|
1071
|
+
"copilot-codex": [
|
|
1072
|
+
{
|
|
1073
|
+
name: "copilot-claude",
|
|
1074
|
+
executor: "COPILOT",
|
|
1075
|
+
variant: "CLAUDE_OPUS_4_6",
|
|
1076
|
+
weight: 50,
|
|
1077
|
+
role: "primary",
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
name: "codex-default",
|
|
1081
|
+
executor: "CODEX",
|
|
1082
|
+
variant: "DEFAULT",
|
|
1083
|
+
weight: 50,
|
|
1084
|
+
role: "backup",
|
|
1085
|
+
},
|
|
1086
|
+
],
|
|
1087
|
+
"copilot-only": [
|
|
1088
|
+
{
|
|
1089
|
+
name: "copilot-claude",
|
|
1090
|
+
executor: "COPILOT",
|
|
1091
|
+
variant: "CLAUDE_OPUS_4_6",
|
|
1092
|
+
weight: 100,
|
|
1093
|
+
role: "primary",
|
|
1094
|
+
},
|
|
1095
|
+
],
|
|
1096
|
+
"codex-only": [
|
|
1097
|
+
{
|
|
1098
|
+
name: "codex-default",
|
|
1099
|
+
executor: "CODEX",
|
|
1100
|
+
variant: "DEFAULT",
|
|
1101
|
+
weight: 100,
|
|
1102
|
+
role: "primary",
|
|
1103
|
+
},
|
|
1104
|
+
],
|
|
1105
|
+
triple: [
|
|
1106
|
+
{
|
|
1107
|
+
name: "copilot-claude",
|
|
1108
|
+
executor: "COPILOT",
|
|
1109
|
+
variant: "CLAUDE_OPUS_4_6",
|
|
1110
|
+
weight: 40,
|
|
1111
|
+
role: "primary",
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
name: "codex-default",
|
|
1115
|
+
executor: "CODEX",
|
|
1116
|
+
variant: "DEFAULT",
|
|
1117
|
+
weight: 35,
|
|
1118
|
+
role: "backup",
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
name: "copilot-gpt",
|
|
1122
|
+
executor: "COPILOT",
|
|
1123
|
+
variant: "GPT_4_1",
|
|
1124
|
+
weight: 25,
|
|
1125
|
+
role: "tertiary",
|
|
1126
|
+
},
|
|
1127
|
+
],
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
const FAILOVER_STRATEGIES = [
|
|
1131
|
+
{
|
|
1132
|
+
name: "next-in-line",
|
|
1133
|
+
desc: "Use the next executor by role priority (primary → backup → tertiary)",
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
name: "weighted-random",
|
|
1137
|
+
desc: "Randomly select from remaining executors by weight",
|
|
1138
|
+
},
|
|
1139
|
+
{ name: "round-robin", desc: "Cycle through remaining executors evenly" },
|
|
1140
|
+
];
|
|
1141
|
+
|
|
1142
|
+
const DISTRIBUTION_MODES = [
|
|
1143
|
+
{
|
|
1144
|
+
name: "weighted",
|
|
1145
|
+
desc: "Distribute tasks by configured weight percentages",
|
|
1146
|
+
},
|
|
1147
|
+
{ name: "round-robin", desc: "Alternate between executors equally" },
|
|
1148
|
+
{
|
|
1149
|
+
name: "primary-only",
|
|
1150
|
+
desc: "Always use primary; others only for failover",
|
|
1151
|
+
},
|
|
1152
|
+
];
|
|
1153
|
+
|
|
1154
|
+
const SETUP_PROFILES = [
|
|
1155
|
+
{
|
|
1156
|
+
key: "recommended",
|
|
1157
|
+
label: "Recommended — configure important choices, keep safe defaults",
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
key: "advanced",
|
|
1161
|
+
label: "Advanced — full control over all setup options",
|
|
1162
|
+
},
|
|
1163
|
+
];
|
|
1164
|
+
|
|
1165
|
+
function toPositiveInt(value, fallback) {
|
|
1166
|
+
const n = Number(value);
|
|
1167
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
1168
|
+
return Math.round(n);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function normalizeEnum(value, allowed, fallback) {
|
|
1172
|
+
const normalized = String(value || "")
|
|
1173
|
+
.trim()
|
|
1174
|
+
.toLowerCase();
|
|
1175
|
+
return allowed.includes(normalized) ? normalized : fallback;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function parseBooleanEnvValue(value, fallback = false) {
|
|
1179
|
+
if (value === undefined || value === null || value === "") {
|
|
1180
|
+
return fallback;
|
|
1181
|
+
}
|
|
1182
|
+
const normalized = String(value).trim().toLowerCase();
|
|
1183
|
+
if (["1", "true", "yes", "on", "y"].includes(normalized)) {
|
|
1184
|
+
return true;
|
|
1185
|
+
}
|
|
1186
|
+
if (["0", "false", "no", "off", "n"].includes(normalized)) {
|
|
1187
|
+
return false;
|
|
1188
|
+
}
|
|
1189
|
+
return fallback;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function toBooleanEnvString(value, fallback = false) {
|
|
1193
|
+
return parseBooleanEnvValue(value, fallback) ? "true" : "false";
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function readProcValue(path) {
|
|
1197
|
+
try {
|
|
1198
|
+
return readFileSync(path, "utf8").trim();
|
|
1199
|
+
} catch {
|
|
1200
|
+
return "";
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function hasBwrapBinary() {
|
|
1205
|
+
if (process.platform !== "linux") return false;
|
|
1206
|
+
try {
|
|
1207
|
+
execSync("bwrap --version", { stdio: "ignore" });
|
|
1208
|
+
return true;
|
|
1209
|
+
} catch {
|
|
1210
|
+
return false;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function detectBwrapSupport() {
|
|
1215
|
+
if (process.platform !== "linux") return false;
|
|
1216
|
+
const unpriv = readProcValue("/proc/sys/kernel/unprivileged_userns_clone");
|
|
1217
|
+
if (unpriv === "0") return false;
|
|
1218
|
+
const maxUserNs = readProcValue("/proc/sys/user/max_user_namespaces");
|
|
1219
|
+
if (maxUserNs && Number(maxUserNs) === 0) return false;
|
|
1220
|
+
return hasBwrapBinary();
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function buildDefaultWritableRoots(repoRoot) {
|
|
1224
|
+
if (!repoRoot) return "";
|
|
1225
|
+
const roots = new Set();
|
|
1226
|
+
const repo = String(repoRoot);
|
|
1227
|
+
if (repo) {
|
|
1228
|
+
const parent = dirname(repo);
|
|
1229
|
+
if (parent && parent !== repo) roots.add(parent);
|
|
1230
|
+
roots.add(repo);
|
|
1231
|
+
roots.add(resolve(repo, ".git"));
|
|
1232
|
+
}
|
|
1233
|
+
return Array.from(roots).join(",");
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function normalizeSetupConfiguration({
|
|
1237
|
+
env,
|
|
1238
|
+
configJson,
|
|
1239
|
+
repoRoot,
|
|
1240
|
+
slug,
|
|
1241
|
+
configDir,
|
|
1242
|
+
}) {
|
|
1243
|
+
env.PROJECT_NAME =
|
|
1244
|
+
env.PROJECT_NAME || configJson.projectName || basename(repoRoot);
|
|
1245
|
+
env.REPO_ROOT = env.REPO_ROOT || repoRoot;
|
|
1246
|
+
env.GITHUB_REPO = env.GITHUB_REPO || slug || "";
|
|
1247
|
+
|
|
1248
|
+
env.MAX_PARALLEL = String(toPositiveInt(env.MAX_PARALLEL || "6", 6));
|
|
1249
|
+
env.TELEGRAM_INTERVAL_MIN = String(
|
|
1250
|
+
toPositiveInt(env.TELEGRAM_INTERVAL_MIN || "10", 10),
|
|
1251
|
+
);
|
|
1252
|
+
|
|
1253
|
+
env.KANBAN_BACKEND = normalizeEnum(
|
|
1254
|
+
env.KANBAN_BACKEND,
|
|
1255
|
+
["internal", "vk", "github", "jira"],
|
|
1256
|
+
"internal",
|
|
1257
|
+
);
|
|
1258
|
+
env.KANBAN_SYNC_POLICY = normalizeEnum(
|
|
1259
|
+
env.KANBAN_SYNC_POLICY,
|
|
1260
|
+
["internal-primary", "bidirectional"],
|
|
1261
|
+
"internal-primary",
|
|
1262
|
+
);
|
|
1263
|
+
env.PROJECT_REQUIREMENTS_PROFILE = normalizeEnum(
|
|
1264
|
+
env.PROJECT_REQUIREMENTS_PROFILE,
|
|
1265
|
+
[
|
|
1266
|
+
"simple-feature",
|
|
1267
|
+
"feature",
|
|
1268
|
+
"large-feature",
|
|
1269
|
+
"system",
|
|
1270
|
+
"multi-system",
|
|
1271
|
+
],
|
|
1272
|
+
"feature",
|
|
1273
|
+
);
|
|
1274
|
+
env.INTERNAL_EXECUTOR_REPLENISH_ENABLED = toBooleanEnvString(
|
|
1275
|
+
env.INTERNAL_EXECUTOR_REPLENISH_ENABLED,
|
|
1276
|
+
false,
|
|
1277
|
+
);
|
|
1278
|
+
env.INTERNAL_EXECUTOR_REPLENISH_MIN_NEW_TASKS = String(
|
|
1279
|
+
toPositiveInt(env.INTERNAL_EXECUTOR_REPLENISH_MIN_NEW_TASKS, 1),
|
|
1280
|
+
);
|
|
1281
|
+
env.INTERNAL_EXECUTOR_REPLENISH_MAX_NEW_TASKS = String(
|
|
1282
|
+
toPositiveInt(env.INTERNAL_EXECUTOR_REPLENISH_MAX_NEW_TASKS, 2),
|
|
1283
|
+
);
|
|
1284
|
+
env.COPILOT_NO_EXPERIMENTAL = toBooleanEnvString(
|
|
1285
|
+
env.COPILOT_NO_EXPERIMENTAL,
|
|
1286
|
+
false,
|
|
1287
|
+
);
|
|
1288
|
+
env.COPILOT_NO_ALLOW_ALL = toBooleanEnvString(
|
|
1289
|
+
env.COPILOT_NO_ALLOW_ALL,
|
|
1290
|
+
false,
|
|
1291
|
+
);
|
|
1292
|
+
env.COPILOT_ENABLE_ASK_USER = toBooleanEnvString(
|
|
1293
|
+
env.COPILOT_ENABLE_ASK_USER,
|
|
1294
|
+
false,
|
|
1295
|
+
);
|
|
1296
|
+
env.COPILOT_ENABLE_ALL_GITHUB_MCP_TOOLS = toBooleanEnvString(
|
|
1297
|
+
env.COPILOT_ENABLE_ALL_GITHUB_MCP_TOOLS,
|
|
1298
|
+
true,
|
|
1299
|
+
);
|
|
1300
|
+
env.COPILOT_AGENT_MAX_REQUESTS = String(
|
|
1301
|
+
toPositiveInt(env.COPILOT_AGENT_MAX_REQUESTS || 500, 500),
|
|
1302
|
+
);
|
|
1303
|
+
env.EXECUTOR_MODE = normalizeEnum(
|
|
1304
|
+
env.EXECUTOR_MODE,
|
|
1305
|
+
["internal", "vk", "hybrid"],
|
|
1306
|
+
"internal",
|
|
1307
|
+
);
|
|
1308
|
+
|
|
1309
|
+
env.CODEX_MODEL_PROFILE = normalizeEnum(
|
|
1310
|
+
env.CODEX_MODEL_PROFILE,
|
|
1311
|
+
["xl", "m"],
|
|
1312
|
+
"xl",
|
|
1313
|
+
);
|
|
1314
|
+
env.CODEX_MODEL_PROFILE_SUBAGENT = normalizeEnum(
|
|
1315
|
+
env.CODEX_MODEL_PROFILE_SUBAGENT || env.CODEX_SUBAGENT_PROFILE,
|
|
1316
|
+
["xl", "m"],
|
|
1317
|
+
"m",
|
|
1318
|
+
);
|
|
1319
|
+
env.CODEX_MODEL_PROFILE_XL_PROVIDER = normalizeEnum(
|
|
1320
|
+
env.CODEX_MODEL_PROFILE_XL_PROVIDER,
|
|
1321
|
+
["openai", "azure", "compatible"],
|
|
1322
|
+
"openai",
|
|
1323
|
+
);
|
|
1324
|
+
env.CODEX_MODEL_PROFILE_M_PROVIDER = normalizeEnum(
|
|
1325
|
+
env.CODEX_MODEL_PROFILE_M_PROVIDER,
|
|
1326
|
+
["openai", "azure", "compatible"],
|
|
1327
|
+
"openai",
|
|
1328
|
+
);
|
|
1329
|
+
env.CODEX_MODEL_PROFILE_XL_MODEL =
|
|
1330
|
+
env.CODEX_MODEL_PROFILE_XL_MODEL || "gpt-5.3-codex";
|
|
1331
|
+
env.CODEX_MODEL_PROFILE_M_MODEL =
|
|
1332
|
+
env.CODEX_MODEL_PROFILE_M_MODEL || "gpt-5.1-codex-mini";
|
|
1333
|
+
env.CODEX_SUBAGENT_MODEL =
|
|
1334
|
+
env.CODEX_SUBAGENT_MODEL || env.CODEX_MODEL_PROFILE_M_MODEL;
|
|
1335
|
+
env.CODEX_AGENT_MAX_THREADS = String(
|
|
1336
|
+
toPositiveInt(
|
|
1337
|
+
env.CODEX_AGENT_MAX_THREADS || env.CODEX_AGENTS_MAX_THREADS || "12",
|
|
1338
|
+
12,
|
|
1339
|
+
),
|
|
1340
|
+
);
|
|
1341
|
+
env.CODEX_SANDBOX = normalizeEnum(
|
|
1342
|
+
env.CODEX_SANDBOX,
|
|
1343
|
+
["workspace-write", "danger-full-access", "read-only"],
|
|
1344
|
+
"workspace-write",
|
|
1345
|
+
);
|
|
1346
|
+
env.CODEX_FEATURES_BWRAP = toBooleanEnvString(
|
|
1347
|
+
env.CODEX_FEATURES_BWRAP,
|
|
1348
|
+
detectBwrapSupport(),
|
|
1349
|
+
);
|
|
1350
|
+
env.CODEX_SANDBOX_PERMISSIONS =
|
|
1351
|
+
env.CODEX_SANDBOX_PERMISSIONS || "disk-full-write-access";
|
|
1352
|
+
env.CODEX_SANDBOX_WRITABLE_ROOTS =
|
|
1353
|
+
env.CODEX_SANDBOX_WRITABLE_ROOTS || buildDefaultWritableRoots(repoRoot);
|
|
1354
|
+
|
|
1355
|
+
env.VK_BASE_URL = env.VK_BASE_URL || "http://127.0.0.1:54089";
|
|
1356
|
+
env.VK_RECOVERY_PORT = String(
|
|
1357
|
+
toPositiveInt(env.VK_RECOVERY_PORT || "54089", 54089),
|
|
1358
|
+
);
|
|
1359
|
+
|
|
1360
|
+
env.CODEX_TRANSPORT = normalizeEnum(
|
|
1361
|
+
env.CODEX_TRANSPORT || process.env.CODEX_TRANSPORT,
|
|
1362
|
+
["sdk", "auto", "cli"],
|
|
1363
|
+
"sdk",
|
|
1364
|
+
);
|
|
1365
|
+
env.COPILOT_TRANSPORT = normalizeEnum(
|
|
1366
|
+
env.COPILOT_TRANSPORT || process.env.COPILOT_TRANSPORT,
|
|
1367
|
+
["sdk", "auto", "cli", "url"],
|
|
1368
|
+
"sdk",
|
|
1369
|
+
);
|
|
1370
|
+
env.COPILOT_MCP_CONFIG =
|
|
1371
|
+
env.COPILOT_MCP_CONFIG || resolve(repoRoot, ".vscode", "mcp.json");
|
|
1372
|
+
env.CLAUDE_TRANSPORT = normalizeEnum(
|
|
1373
|
+
env.CLAUDE_TRANSPORT || process.env.CLAUDE_TRANSPORT,
|
|
1374
|
+
["sdk", "auto", "cli"],
|
|
1375
|
+
"sdk",
|
|
1376
|
+
);
|
|
1377
|
+
|
|
1378
|
+
env.WHATSAPP_ENABLED = toBooleanEnvString(env.WHATSAPP_ENABLED, false);
|
|
1379
|
+
|
|
1380
|
+
env.CONTAINER_ENABLED = toBooleanEnvString(env.CONTAINER_ENABLED, false);
|
|
1381
|
+
|
|
1382
|
+
env.CONTAINER_RUNTIME = normalizeEnum(
|
|
1383
|
+
env.CONTAINER_RUNTIME,
|
|
1384
|
+
["auto", "docker", "podman", "container"],
|
|
1385
|
+
"auto",
|
|
1386
|
+
);
|
|
1387
|
+
if (env.ORCHESTRATOR_SCRIPT) {
|
|
1388
|
+
env.ORCHESTRATOR_SCRIPT = formatOrchestratorScriptForEnv(
|
|
1389
|
+
env.ORCHESTRATOR_SCRIPT,
|
|
1390
|
+
configDir || __dirname,
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
if (
|
|
1395
|
+
!Array.isArray(configJson.executors) ||
|
|
1396
|
+
configJson.executors.length === 0
|
|
1397
|
+
) {
|
|
1398
|
+
configJson.executors = EXECUTOR_PRESETS["codex-only"];
|
|
1399
|
+
}
|
|
1400
|
+
configJson.executors = configJson.executors.map((executor, index) => ({
|
|
1401
|
+
...executor,
|
|
1402
|
+
name: executor.name || `executor-${index + 1}`,
|
|
1403
|
+
executor: String(executor.executor || "CODEX").toUpperCase(),
|
|
1404
|
+
variant: executor.variant || "DEFAULT",
|
|
1405
|
+
weight: toPositiveInt(executor.weight || 1, 1),
|
|
1406
|
+
role:
|
|
1407
|
+
executor.role ||
|
|
1408
|
+
(index === 0
|
|
1409
|
+
? "primary"
|
|
1410
|
+
: index === 1
|
|
1411
|
+
? "backup"
|
|
1412
|
+
: `executor-${index + 1}`),
|
|
1413
|
+
enabled: executor.enabled !== false,
|
|
1414
|
+
}));
|
|
1415
|
+
|
|
1416
|
+
configJson.failover = {
|
|
1417
|
+
strategy: normalizeEnum(
|
|
1418
|
+
configJson.failover?.strategy || env.FAILOVER_STRATEGY || "next-in-line",
|
|
1419
|
+
["next-in-line", "weighted-random", "round-robin"],
|
|
1420
|
+
"next-in-line",
|
|
1421
|
+
),
|
|
1422
|
+
maxRetries: toPositiveInt(
|
|
1423
|
+
configJson.failover?.maxRetries || env.FAILOVER_MAX_RETRIES || 3,
|
|
1424
|
+
3,
|
|
1425
|
+
),
|
|
1426
|
+
cooldownMinutes: toPositiveInt(
|
|
1427
|
+
configJson.failover?.cooldownMinutes || env.FAILOVER_COOLDOWN_MIN || 5,
|
|
1428
|
+
5,
|
|
1429
|
+
),
|
|
1430
|
+
disableOnConsecutiveFailures: toPositiveInt(
|
|
1431
|
+
configJson.failover?.disableOnConsecutiveFailures ||
|
|
1432
|
+
env.FAILOVER_DISABLE_AFTER ||
|
|
1433
|
+
3,
|
|
1434
|
+
3,
|
|
1435
|
+
),
|
|
1436
|
+
};
|
|
1437
|
+
|
|
1438
|
+
configJson.distribution = normalizeEnum(
|
|
1439
|
+
configJson.distribution || env.EXECUTOR_DISTRIBUTION || "weighted",
|
|
1440
|
+
["weighted", "round-robin", "primary-only"],
|
|
1441
|
+
"weighted",
|
|
1442
|
+
);
|
|
1443
|
+
|
|
1444
|
+
if (
|
|
1445
|
+
!Array.isArray(configJson.repositories) ||
|
|
1446
|
+
configJson.repositories.length === 0
|
|
1447
|
+
) {
|
|
1448
|
+
configJson.repositories = [
|
|
1449
|
+
{
|
|
1450
|
+
name: basename(repoRoot),
|
|
1451
|
+
slug: env.GITHUB_REPO,
|
|
1452
|
+
primary: true,
|
|
1453
|
+
},
|
|
1454
|
+
];
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
configJson.projectName = env.PROJECT_NAME;
|
|
1458
|
+
configJson.kanban = {
|
|
1459
|
+
...(configJson.kanban || {}),
|
|
1460
|
+
backend: env.KANBAN_BACKEND,
|
|
1461
|
+
syncPolicy: env.KANBAN_SYNC_POLICY,
|
|
1462
|
+
};
|
|
1463
|
+
configJson.internalExecutor = {
|
|
1464
|
+
...(configJson.internalExecutor || {}),
|
|
1465
|
+
mode: env.EXECUTOR_MODE,
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function formatEnvValue(value) {
|
|
1470
|
+
const raw = String(value ?? "");
|
|
1471
|
+
const needsQuotes = /\s|#|=/.test(raw);
|
|
1472
|
+
if (!needsQuotes) return raw;
|
|
1473
|
+
return `"${raw.replace(/"/g, '\\"')}"`;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
export function buildStandardizedEnvFile(templateText, envEntries) {
|
|
1477
|
+
const lines = templateText.split(/\r?\n/);
|
|
1478
|
+
const entryMap = new Map(
|
|
1479
|
+
Object.entries(envEntries)
|
|
1480
|
+
.filter(([key]) => !key.startsWith("_"))
|
|
1481
|
+
.map(([key, value]) => [key, String(value ?? "")]),
|
|
1482
|
+
);
|
|
1483
|
+
|
|
1484
|
+
const consumed = new Set();
|
|
1485
|
+
const seenKeys = new Set();
|
|
1486
|
+
const updated = lines.flatMap((line) => {
|
|
1487
|
+
const match = line.match(/^\s*#?\s*([A-Z0-9_]+)=.*$/);
|
|
1488
|
+
if (!match) return [line];
|
|
1489
|
+
const key = match[1];
|
|
1490
|
+
if (seenKeys.has(key)) return [];
|
|
1491
|
+
seenKeys.add(key);
|
|
1492
|
+
if (!entryMap.has(key)) return [line];
|
|
1493
|
+
consumed.add(key);
|
|
1494
|
+
return [`${key}=${formatEnvValue(entryMap.get(key))}`];
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
const extras = [...entryMap.keys()].filter((key) => !consumed.has(key));
|
|
1498
|
+
if (extras.length > 0) {
|
|
1499
|
+
updated.push("");
|
|
1500
|
+
updated.push("# Added by setup wizard");
|
|
1501
|
+
for (const key of extras.sort()) {
|
|
1502
|
+
updated.push(`${key}=${formatEnvValue(entryMap.get(key))}`);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
const header = [
|
|
1507
|
+
"# Generated by openfleet setup wizard",
|
|
1508
|
+
`# ${new Date().toISOString()}`,
|
|
1509
|
+
"",
|
|
1510
|
+
];
|
|
1511
|
+
return [...header, ...updated].join("\n") + "\n";
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
// ── Agent Template ───────────────────────────────────────────────────────────
|
|
1515
|
+
|
|
1516
|
+
function generateAgentsMd(projectName, repoSlug) {
|
|
1517
|
+
return `# ${projectName} — Agent Guide
|
|
1518
|
+
|
|
1519
|
+
## CRITICAL
|
|
1520
|
+
|
|
1521
|
+
Always work on tasks longer than you think are needed to accommodate edge cases, testing, and quality.
|
|
1522
|
+
Ensure tests pass and build is clean with 0 warnings before deciding a task is complete.
|
|
1523
|
+
When working on a task, do not stop until it is COMPLETELY done end-to-end.
|
|
1524
|
+
|
|
1525
|
+
Before finishing a task — create a commit using conventional commits and push.
|
|
1526
|
+
|
|
1527
|
+
### PR Creation
|
|
1528
|
+
|
|
1529
|
+
After committing:
|
|
1530
|
+
- Run \`gh pr create\` to open the PR
|
|
1531
|
+
- Ensure pre-push hooks pass
|
|
1532
|
+
- Fix any lint or test errors encountered
|
|
1533
|
+
|
|
1534
|
+
## Overview
|
|
1535
|
+
|
|
1536
|
+
- Repository: \`${repoSlug}\`
|
|
1537
|
+
- Task management: Vibe-Kanban (auto-configured by openfleet)
|
|
1538
|
+
|
|
1539
|
+
## Build & Test
|
|
1540
|
+
|
|
1541
|
+
\`\`\`bash
|
|
1542
|
+
# Add your build commands here
|
|
1543
|
+
npm run build
|
|
1544
|
+
npm test
|
|
1545
|
+
\`\`\`
|
|
1546
|
+
|
|
1547
|
+
## Commit Conventions
|
|
1548
|
+
|
|
1549
|
+
Use [Conventional Commits](https://www.conventionalcommits.org/):
|
|
1550
|
+
|
|
1551
|
+
\`\`\`
|
|
1552
|
+
type(scope): description
|
|
1553
|
+
\`\`\`
|
|
1554
|
+
|
|
1555
|
+
Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
|
|
1556
|
+
|
|
1557
|
+
## Pre-commit / Pre-push
|
|
1558
|
+
|
|
1559
|
+
Linting and formatting are enforced before commit.
|
|
1560
|
+
Tests and builds are verified before push.
|
|
1561
|
+
`;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// ── VK Auto-Configuration ────────────────────────────────────────────────────
|
|
1565
|
+
|
|
1566
|
+
function generateVkSetupScript(config) {
|
|
1567
|
+
const repoRoot = config.repoRoot.replace(/\\/g, "/");
|
|
1568
|
+
const monitorDir = config.monitorDir.replace(/\\/g, "/");
|
|
1569
|
+
|
|
1570
|
+
return `#!/usr/bin/env bash
|
|
1571
|
+
# Auto-generated by openfleet setup
|
|
1572
|
+
# VK workspace setup script for: ${config.projectName}
|
|
1573
|
+
|
|
1574
|
+
set -euo pipefail
|
|
1575
|
+
|
|
1576
|
+
echo "Setting up workspace for ${config.projectName}..."
|
|
1577
|
+
|
|
1578
|
+
# ── PATH propagation ──────────────────────────────────────────────────────────
|
|
1579
|
+
# Ensure common tool directories are on PATH so agents can find gh, pwsh, node,
|
|
1580
|
+
# go, etc. without using full absolute paths. The host user's PATH may not be
|
|
1581
|
+
# inherited by the workspace shell.
|
|
1582
|
+
_add_to_path() { case ":\$PATH:" in *":\$1:"*) ;; *) export PATH="\$1:\$PATH" ;; esac; }
|
|
1583
|
+
|
|
1584
|
+
for _dir in \\
|
|
1585
|
+
/usr/local/bin \\
|
|
1586
|
+
/usr/local/sbin \\
|
|
1587
|
+
/usr/bin \\
|
|
1588
|
+
"\$HOME/.local/bin" \\
|
|
1589
|
+
"\$HOME/bin" \\
|
|
1590
|
+
"\$HOME/go/bin" \\
|
|
1591
|
+
"\$HOME/.cargo/bin" \\
|
|
1592
|
+
/snap/bin \\
|
|
1593
|
+
/opt/homebrew/bin; do
|
|
1594
|
+
[ -d "\$_dir" ] && _add_to_path "\$_dir"
|
|
1595
|
+
done
|
|
1596
|
+
|
|
1597
|
+
# Windows-specific paths (Git Bash / MSYS2 environment)
|
|
1598
|
+
case "\$(uname -s 2>/dev/null)" in
|
|
1599
|
+
MINGW*|MSYS*|CYGWIN*)
|
|
1600
|
+
for _wdir in \\
|
|
1601
|
+
"/c/Program Files/GitHub CLI" \\
|
|
1602
|
+
"/c/Program Files/PowerShell/7" \\
|
|
1603
|
+
"/c/Program Files/nodejs"; do
|
|
1604
|
+
[ -d "\$_wdir" ] && _add_to_path "\$_wdir"
|
|
1605
|
+
done
|
|
1606
|
+
;;
|
|
1607
|
+
esac
|
|
1608
|
+
|
|
1609
|
+
# ── Git credential guard ─────────────────────────────────────────────────────
|
|
1610
|
+
# NEVER run 'gh auth setup-git' inside a workspace — it writes the container's
|
|
1611
|
+
# gh path into .git/config, corrupting pushes from other environments.
|
|
1612
|
+
# Rely on GH_TOKEN/GITHUB_TOKEN env vars or the global credential helper.
|
|
1613
|
+
if git config --local credential.helper &>/dev/null; then
|
|
1614
|
+
_local_helper=\$(git config --local credential.helper)
|
|
1615
|
+
if echo "\$_local_helper" | grep -qE '/home/.*/gh(\\.exe)?|/tmp/.*/gh'; then
|
|
1616
|
+
echo " [setup] Removing stale local credential.helper: \$_local_helper"
|
|
1617
|
+
git config --local --unset credential.helper || true
|
|
1618
|
+
fi
|
|
1619
|
+
fi
|
|
1620
|
+
|
|
1621
|
+
# ── Git worktree cleanup ─────────────────────────────────────────────────────
|
|
1622
|
+
# Prune stale worktree references to prevent path corruption errors.
|
|
1623
|
+
# This happens when worktree directories are deleted but git metadata remains.
|
|
1624
|
+
if [ -f ".git" ]; then
|
|
1625
|
+
_gitdir=\$(cat .git | sed 's/^gitdir: //')
|
|
1626
|
+
_repo_root=\$(dirname "\$_gitdir" | xargs dirname | xargs dirname)
|
|
1627
|
+
if [ -d "\$_repo_root/.git/worktrees" ]; then
|
|
1628
|
+
echo " [setup] Pruning stale worktrees..."
|
|
1629
|
+
( cd "\$_repo_root" && git worktree prune -v 2>&1 | sed 's/^/ [prune] /' ) || true
|
|
1630
|
+
fi
|
|
1631
|
+
fi
|
|
1632
|
+
|
|
1633
|
+
# ── GitHub auth verification ─────────────────────────────────────────────────
|
|
1634
|
+
if command -v gh &>/dev/null; then
|
|
1635
|
+
echo " [setup] gh CLI found at: \$(command -v gh)"
|
|
1636
|
+
gh auth status 2>/dev/null || echo " [setup] gh not authenticated — ensure GH_TOKEN is set"
|
|
1637
|
+
else
|
|
1638
|
+
echo " [setup] WARNING: gh CLI not found on PATH"
|
|
1639
|
+
echo " [setup] Current PATH: \$PATH"
|
|
1640
|
+
fi
|
|
1641
|
+
|
|
1642
|
+
# Install dependencies
|
|
1643
|
+
if [ -f "package.json" ]; then
|
|
1644
|
+
if command -v pnpm &>/dev/null; then
|
|
1645
|
+
pnpm install
|
|
1646
|
+
elif command -v npm &>/dev/null; then
|
|
1647
|
+
npm install
|
|
1648
|
+
fi
|
|
1649
|
+
fi
|
|
1650
|
+
|
|
1651
|
+
# Install openfleet dependencies
|
|
1652
|
+
if [ -d "${relative(config.repoRoot, monitorDir)}" ]; then
|
|
1653
|
+
cd "${relative(config.repoRoot, monitorDir)}"
|
|
1654
|
+
if command -v pnpm &>/dev/null; then
|
|
1655
|
+
pnpm install
|
|
1656
|
+
elif command -v npm &>/dev/null; then
|
|
1657
|
+
npm install
|
|
1658
|
+
fi
|
|
1659
|
+
cd -
|
|
1660
|
+
fi
|
|
1661
|
+
|
|
1662
|
+
echo "Workspace setup complete."
|
|
1663
|
+
`;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function generateVkCleanupScript(config) {
|
|
1667
|
+
return `#!/usr/bin/env bash
|
|
1668
|
+
# Auto-generated by openfleet setup
|
|
1669
|
+
# VK workspace cleanup script for: ${config.projectName}
|
|
1670
|
+
|
|
1671
|
+
set -euo pipefail
|
|
1672
|
+
|
|
1673
|
+
echo "Cleaning up workspace for ${config.projectName}..."
|
|
1674
|
+
|
|
1675
|
+
# Create PR if branch has commits
|
|
1676
|
+
BRANCH=$(git branch --show-current 2>/dev/null || true)
|
|
1677
|
+
if [ -n "$BRANCH" ] && [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ]; then
|
|
1678
|
+
COMMITS=$(git log main.."$BRANCH" --oneline 2>/dev/null | wc -l || echo 0)
|
|
1679
|
+
if [ "$COMMITS" -gt 0 ]; then
|
|
1680
|
+
echo "Branch $BRANCH has $COMMITS commit(s) — creating PR..."
|
|
1681
|
+
gh pr create --fill 2>/dev/null || echo "PR creation skipped"
|
|
1682
|
+
fi
|
|
1683
|
+
fi
|
|
1684
|
+
|
|
1685
|
+
echo "Cleanup complete."
|
|
1686
|
+
`;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// ── Main Setup Flow ──────────────────────────────────────────────────────────
|
|
1690
|
+
|
|
1691
|
+
async function main() {
|
|
1692
|
+
printBanner();
|
|
1693
|
+
|
|
1694
|
+
// ── Step 1: Prerequisites ───────────────────────────────
|
|
1695
|
+
heading("Step 1 of 9 — Prerequisites");
|
|
1696
|
+
const hasNode = check(
|
|
1697
|
+
"Node.js ≥ 18",
|
|
1698
|
+
Number(process.versions.node.split(".")[0]) >= 18,
|
|
1699
|
+
);
|
|
1700
|
+
const hasGit = check("git", commandExists("git"));
|
|
1701
|
+
const runtimeStatus = getScriptRuntimePrerequisiteStatus();
|
|
1702
|
+
check(
|
|
1703
|
+
runtimeStatus.required.label,
|
|
1704
|
+
runtimeStatus.required.ok,
|
|
1705
|
+
runtimeStatus.required.hint,
|
|
1706
|
+
);
|
|
1707
|
+
if (runtimeStatus.optionalPwsh) {
|
|
1708
|
+
if (runtimeStatus.optionalPwsh.ok) {
|
|
1709
|
+
info(
|
|
1710
|
+
`${runtimeStatus.optionalPwsh.label} detected (${runtimeStatus.optionalPwsh.hint}).`,
|
|
1711
|
+
);
|
|
1712
|
+
} else {
|
|
1713
|
+
warn(
|
|
1714
|
+
`${runtimeStatus.optionalPwsh.label} not found (${runtimeStatus.optionalPwsh.hint}).`,
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
check(
|
|
1719
|
+
"GitHub CLI (gh)",
|
|
1720
|
+
commandExists("gh"),
|
|
1721
|
+
"Recommended: https://cli.github.com/",
|
|
1722
|
+
);
|
|
1723
|
+
const hasVk = check(
|
|
1724
|
+
"Vibe-Kanban CLI",
|
|
1725
|
+
commandExists("vibe-kanban") || bundledBinExists("vibe-kanban"),
|
|
1726
|
+
"Bundled with @virtengine/openfleet as a dependency",
|
|
1727
|
+
);
|
|
1728
|
+
|
|
1729
|
+
if (!hasVk) {
|
|
1730
|
+
warn(
|
|
1731
|
+
"vibe-kanban not found. This is bundled with openfleet, so this is unexpected.",
|
|
1732
|
+
);
|
|
1733
|
+
info("Try reinstalling:");
|
|
1734
|
+
console.log(" npm uninstall -g @virtengine/openfleet");
|
|
1735
|
+
console.log(" npm install -g @virtengine/openfleet\n");
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
if (!hasNode) {
|
|
1739
|
+
console.error("\n Node.js 18+ is required. Aborting.\n");
|
|
1740
|
+
process.exit(1);
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
const repoRoot = detectRepoRoot();
|
|
1744
|
+
const configDir = resolveConfigDir(repoRoot);
|
|
1745
|
+
const slug = detectRepoSlug();
|
|
1746
|
+
const projectName = detectProjectName(repoRoot);
|
|
1747
|
+
const envCandidates = [resolve(configDir, ".env"), resolve(repoRoot, ".env")];
|
|
1748
|
+
const seenEnvPaths = new Set();
|
|
1749
|
+
let detectedEnv = false;
|
|
1750
|
+
let loadedEnvEntries = 0;
|
|
1751
|
+
for (const envPath of envCandidates) {
|
|
1752
|
+
if (seenEnvPaths.has(envPath)) continue;
|
|
1753
|
+
seenEnvPaths.add(envPath);
|
|
1754
|
+
const applied = applyEnvFileToProcess(envPath, { override: false });
|
|
1755
|
+
if (applied.found) {
|
|
1756
|
+
detectedEnv = true;
|
|
1757
|
+
loadedEnvEntries += applied.loaded;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
if (detectedEnv) {
|
|
1761
|
+
info(
|
|
1762
|
+
"Detected .env file -> overriding default setting with existing config",
|
|
1763
|
+
);
|
|
1764
|
+
info(
|
|
1765
|
+
`Loaded ${loadedEnvEntries} value(s) from existing environment file(s).`,
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
const env = {};
|
|
1770
|
+
const configJson = {
|
|
1771
|
+
projectName,
|
|
1772
|
+
executors: [],
|
|
1773
|
+
failover: {},
|
|
1774
|
+
distribution: "weighted",
|
|
1775
|
+
repositories: [],
|
|
1776
|
+
agentPrompts: {},
|
|
1777
|
+
};
|
|
1778
|
+
|
|
1779
|
+
env.REPO_ROOT = process.env.REPO_ROOT || repoRoot;
|
|
1780
|
+
|
|
1781
|
+
if (isNonInteractive) {
|
|
1782
|
+
return runNonInteractive({
|
|
1783
|
+
env,
|
|
1784
|
+
configJson,
|
|
1785
|
+
repoRoot,
|
|
1786
|
+
slug,
|
|
1787
|
+
projectName,
|
|
1788
|
+
configDir,
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
const prompt = createPrompt();
|
|
1793
|
+
|
|
1794
|
+
try {
|
|
1795
|
+
// ── Step 2: Setup Mode + Project Identity ─────────────
|
|
1796
|
+
heading("Step 2 of 9 — Setup Mode & Project Identity");
|
|
1797
|
+
const setupProfileIdx = await prompt.choose(
|
|
1798
|
+
"How much setup detail do you want?",
|
|
1799
|
+
SETUP_PROFILES.map((profile) => profile.label),
|
|
1800
|
+
0,
|
|
1801
|
+
);
|
|
1802
|
+
const setupProfile = SETUP_PROFILES[setupProfileIdx]?.key || "recommended";
|
|
1803
|
+
const isAdvancedSetup = setupProfile === "advanced";
|
|
1804
|
+
info(
|
|
1805
|
+
isAdvancedSetup
|
|
1806
|
+
? "Advanced mode enabled — all sections will prompt for detailed overrides."
|
|
1807
|
+
: "Recommended mode enabled — only key decisions are prompted; safe defaults fill the rest.",
|
|
1808
|
+
);
|
|
1809
|
+
|
|
1810
|
+
env.PROJECT_NAME = await prompt.ask("Project name", projectName);
|
|
1811
|
+
env.GITHUB_REPO = await prompt.ask(
|
|
1812
|
+
"GitHub repo slug (org/repo)",
|
|
1813
|
+
process.env.GITHUB_REPO || slug || "",
|
|
1814
|
+
);
|
|
1815
|
+
configJson.projectName = env.PROJECT_NAME;
|
|
1816
|
+
|
|
1817
|
+
// ── Step 3: Repository ─────────────────────────────────
|
|
1818
|
+
heading("Step 3 of 9 — Repository Configuration");
|
|
1819
|
+
const multiRepo = isAdvancedSetup
|
|
1820
|
+
? await prompt.confirm(
|
|
1821
|
+
"Do you have multiple repositories (e.g. separate backend/frontend)?",
|
|
1822
|
+
false,
|
|
1823
|
+
)
|
|
1824
|
+
: false;
|
|
1825
|
+
|
|
1826
|
+
if (multiRepo) {
|
|
1827
|
+
info("Configure each repository. The first is the primary.\n");
|
|
1828
|
+
let addMore = true;
|
|
1829
|
+
let repoIdx = 0;
|
|
1830
|
+
while (addMore) {
|
|
1831
|
+
const repoName = await prompt.ask(
|
|
1832
|
+
` Repo ${repoIdx + 1} — name`,
|
|
1833
|
+
repoIdx === 0 ? basename(repoRoot) : "",
|
|
1834
|
+
);
|
|
1835
|
+
const repoPath = await prompt.ask(
|
|
1836
|
+
` Repo ${repoIdx + 1} — local path`,
|
|
1837
|
+
repoIdx === 0 ? repoRoot : "",
|
|
1838
|
+
);
|
|
1839
|
+
const repoSlug = await prompt.ask(
|
|
1840
|
+
` Repo ${repoIdx + 1} — GitHub slug`,
|
|
1841
|
+
repoIdx === 0 ? env.GITHUB_REPO : "",
|
|
1842
|
+
);
|
|
1843
|
+
configJson.repositories.push({
|
|
1844
|
+
name: repoName,
|
|
1845
|
+
path: repoPath,
|
|
1846
|
+
slug: repoSlug,
|
|
1847
|
+
primary: repoIdx === 0,
|
|
1848
|
+
});
|
|
1849
|
+
repoIdx++;
|
|
1850
|
+
addMore = await prompt.confirm("Add another repository?", false);
|
|
1851
|
+
}
|
|
1852
|
+
} else {
|
|
1853
|
+
// Single-repo: omit path — config.mjs auto-detects via git
|
|
1854
|
+
configJson.repositories.push({
|
|
1855
|
+
name: basename(repoRoot),
|
|
1856
|
+
slug: env.GITHUB_REPO,
|
|
1857
|
+
primary: true,
|
|
1858
|
+
});
|
|
1859
|
+
if (!isAdvancedSetup) {
|
|
1860
|
+
info(
|
|
1861
|
+
"Using single-repo defaults (recommended mode). Re-run setup in Advanced mode for multi-repo config.",
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// ── Step 4: Executor Configuration ─────────────────────
|
|
1867
|
+
heading("Step 4 of 9 — Executor / Agent Configuration");
|
|
1868
|
+
console.log(" Executors are the AI agents that work on tasks.\n");
|
|
1869
|
+
|
|
1870
|
+
const presetOptions = isAdvancedSetup
|
|
1871
|
+
? [
|
|
1872
|
+
"Codex only",
|
|
1873
|
+
"Copilot + Codex (50/50 split)",
|
|
1874
|
+
"Copilot only (Claude Opus 4.6)",
|
|
1875
|
+
"Triple (Copilot Claude 40%, Codex 35%, Copilot GPT 25%)",
|
|
1876
|
+
"Custom — I'll define my own executors",
|
|
1877
|
+
]
|
|
1878
|
+
: [
|
|
1879
|
+
"Codex only",
|
|
1880
|
+
"Copilot + Codex (50/50 split)",
|
|
1881
|
+
"Copilot only (Claude Opus 4.6)",
|
|
1882
|
+
"Triple (Copilot Claude 40%, Codex 35%, Copilot GPT 25%)",
|
|
1883
|
+
];
|
|
1884
|
+
|
|
1885
|
+
const presetIdx = await prompt.choose(
|
|
1886
|
+
"Select executor preset:",
|
|
1887
|
+
presetOptions,
|
|
1888
|
+
0,
|
|
1889
|
+
);
|
|
1890
|
+
|
|
1891
|
+
const presetNames = isAdvancedSetup
|
|
1892
|
+
? ["codex-only", "copilot-codex", "copilot-only", "triple", "custom"]
|
|
1893
|
+
: ["codex-only", "copilot-codex", "copilot-only", "triple"];
|
|
1894
|
+
const presetKey = presetNames[presetIdx] || "codex-only";
|
|
1895
|
+
|
|
1896
|
+
if (presetKey === "custom") {
|
|
1897
|
+
info("Define your executors. Enter empty name to finish.\n");
|
|
1898
|
+
let execIdx = 0;
|
|
1899
|
+
const roles = ["primary", "backup", "tertiary"];
|
|
1900
|
+
while (true) {
|
|
1901
|
+
const eName = await prompt.ask(
|
|
1902
|
+
` Executor ${execIdx + 1} — name (empty to finish)`,
|
|
1903
|
+
"",
|
|
1904
|
+
);
|
|
1905
|
+
if (!eName) break;
|
|
1906
|
+
const eType = await prompt.ask(" Executor type", "COPILOT");
|
|
1907
|
+
const eVariant = await prompt.ask(" Model variant", "CLAUDE_OPUS_4_6");
|
|
1908
|
+
const eWeight = Number(await prompt.ask(" Weight (1-100)", "50"));
|
|
1909
|
+
configJson.executors.push({
|
|
1910
|
+
name: eName,
|
|
1911
|
+
executor: eType.toUpperCase(),
|
|
1912
|
+
variant: eVariant,
|
|
1913
|
+
weight: eWeight,
|
|
1914
|
+
role: roles[execIdx] || `executor-${execIdx + 1}`,
|
|
1915
|
+
enabled: true,
|
|
1916
|
+
});
|
|
1917
|
+
execIdx++;
|
|
1918
|
+
}
|
|
1919
|
+
} else {
|
|
1920
|
+
configJson.executors = EXECUTOR_PRESETS[presetKey];
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// Show executor summary
|
|
1924
|
+
console.log("\n Configured executors:");
|
|
1925
|
+
const totalWeight = configJson.executors.reduce((s, e) => s + e.weight, 0);
|
|
1926
|
+
for (const e of configJson.executors) {
|
|
1927
|
+
const pct = Math.round((e.weight / totalWeight) * 100);
|
|
1928
|
+
console.log(
|
|
1929
|
+
` ${e.role.padEnd(10)} ${e.executor}:${e.variant} — ${pct}%`,
|
|
1930
|
+
);
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
if (isAdvancedSetup) {
|
|
1934
|
+
console.log();
|
|
1935
|
+
console.log(
|
|
1936
|
+
chalk.dim(" What happens when an executor fails repeatedly?"),
|
|
1937
|
+
);
|
|
1938
|
+
console.log();
|
|
1939
|
+
|
|
1940
|
+
const failoverIdx = await prompt.choose(
|
|
1941
|
+
"Select failover strategy:",
|
|
1942
|
+
FAILOVER_STRATEGIES.map((f) => `${f.name} — ${f.desc}`),
|
|
1943
|
+
0,
|
|
1944
|
+
);
|
|
1945
|
+
configJson.failover = {
|
|
1946
|
+
strategy: FAILOVER_STRATEGIES[failoverIdx].name,
|
|
1947
|
+
maxRetries: Number(
|
|
1948
|
+
await prompt.ask("Max retries before failover", "3"),
|
|
1949
|
+
),
|
|
1950
|
+
cooldownMinutes: Number(
|
|
1951
|
+
await prompt.ask("Cooldown after disabling executor (minutes)", "5"),
|
|
1952
|
+
),
|
|
1953
|
+
disableOnConsecutiveFailures: Number(
|
|
1954
|
+
await prompt.ask(
|
|
1955
|
+
"Disable executor after N consecutive failures",
|
|
1956
|
+
"3",
|
|
1957
|
+
),
|
|
1958
|
+
),
|
|
1959
|
+
};
|
|
1960
|
+
|
|
1961
|
+
const distIdx = await prompt.choose(
|
|
1962
|
+
"\n Task distribution mode:",
|
|
1963
|
+
DISTRIBUTION_MODES.map((d) => `${d.name} — ${d.desc}`),
|
|
1964
|
+
0,
|
|
1965
|
+
);
|
|
1966
|
+
configJson.distribution = DISTRIBUTION_MODES[distIdx].name;
|
|
1967
|
+
} else {
|
|
1968
|
+
configJson.failover = {
|
|
1969
|
+
strategy: "next-in-line",
|
|
1970
|
+
maxRetries: 3,
|
|
1971
|
+
cooldownMinutes: 5,
|
|
1972
|
+
disableOnConsecutiveFailures: 3,
|
|
1973
|
+
};
|
|
1974
|
+
configJson.distribution = "weighted";
|
|
1975
|
+
info(
|
|
1976
|
+
"Using recommended routing defaults: weighted distribution, next-in-line failover.",
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// ── Step 5: AI Provider ────────────────────────────────
|
|
1981
|
+
heading("Step 5 of 9 — AI / Codex Provider");
|
|
1982
|
+
console.log(
|
|
1983
|
+
" Codex Monitor uses the Codex SDK for crash analysis & autofix.\n",
|
|
1984
|
+
);
|
|
1985
|
+
|
|
1986
|
+
const providerIdx = await prompt.choose(
|
|
1987
|
+
"Select AI provider:",
|
|
1988
|
+
[
|
|
1989
|
+
"OpenAI (default)",
|
|
1990
|
+
"Azure OpenAI",
|
|
1991
|
+
"Local model (Ollama, vLLM, etc.)",
|
|
1992
|
+
"Other OpenAI-compatible endpoint",
|
|
1993
|
+
"None — disable AI features",
|
|
1994
|
+
],
|
|
1995
|
+
0,
|
|
1996
|
+
);
|
|
1997
|
+
|
|
1998
|
+
if (providerIdx < 4) {
|
|
1999
|
+
env.OPENAI_API_KEY = await prompt.ask(
|
|
2000
|
+
"API Key",
|
|
2001
|
+
process.env.OPENAI_API_KEY || "",
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
if (providerIdx === 1) {
|
|
2005
|
+
env.OPENAI_BASE_URL = await prompt.ask(
|
|
2006
|
+
"Azure endpoint URL",
|
|
2007
|
+
process.env.OPENAI_BASE_URL || "",
|
|
2008
|
+
);
|
|
2009
|
+
env.CODEX_MODEL = await prompt.ask(
|
|
2010
|
+
"Deployment/model name",
|
|
2011
|
+
process.env.CODEX_MODEL || "",
|
|
2012
|
+
);
|
|
2013
|
+
} else if (providerIdx === 2) {
|
|
2014
|
+
env.OPENAI_API_KEY = env.OPENAI_API_KEY || "ollama";
|
|
2015
|
+
env.OPENAI_BASE_URL = await prompt.ask(
|
|
2016
|
+
"Local API URL",
|
|
2017
|
+
"http://localhost:11434/v1",
|
|
2018
|
+
);
|
|
2019
|
+
env.CODEX_MODEL = await prompt.ask("Model name", "codex");
|
|
2020
|
+
} else if (providerIdx === 3) {
|
|
2021
|
+
env.OPENAI_BASE_URL = await prompt.ask("API Base URL", "");
|
|
2022
|
+
env.CODEX_MODEL = await prompt.ask("Model name", "");
|
|
2023
|
+
} else if (providerIdx === 4) {
|
|
2024
|
+
env.CODEX_SDK_DISABLED = "true";
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
if (providerIdx < 4) {
|
|
2028
|
+
const configureProfiles = await prompt.confirm(
|
|
2029
|
+
"Configure model profiles (xl/m) for one-click switching?",
|
|
2030
|
+
true,
|
|
2031
|
+
);
|
|
2032
|
+
if (configureProfiles) {
|
|
2033
|
+
const activeProfileIdx = await prompt.choose(
|
|
2034
|
+
"Default active profile:",
|
|
2035
|
+
["xl (high quality)", "m (faster/cheaper)"],
|
|
2036
|
+
0,
|
|
2037
|
+
);
|
|
2038
|
+
env.CODEX_MODEL_PROFILE = activeProfileIdx === 0 ? "xl" : "m";
|
|
2039
|
+
env.CODEX_MODEL_PROFILE_SUBAGENT = activeProfileIdx === 0 ? "m" : "xl";
|
|
2040
|
+
|
|
2041
|
+
env.CODEX_MODEL_PROFILE_XL_MODEL = await prompt.ask(
|
|
2042
|
+
"XL profile model",
|
|
2043
|
+
process.env.CODEX_MODEL_PROFILE_XL_MODEL ||
|
|
2044
|
+
process.env.CODEX_MODEL ||
|
|
2045
|
+
"gpt-5.3-codex",
|
|
2046
|
+
);
|
|
2047
|
+
env.CODEX_MODEL_PROFILE_M_MODEL = await prompt.ask(
|
|
2048
|
+
"M profile model",
|
|
2049
|
+
process.env.CODEX_MODEL_PROFILE_M_MODEL || "gpt-5.1-codex-mini",
|
|
2050
|
+
);
|
|
2051
|
+
|
|
2052
|
+
const providerName =
|
|
2053
|
+
providerIdx === 1 ? "azure" : providerIdx === 3 ? "compatible" : "openai";
|
|
2054
|
+
env.CODEX_MODEL_PROFILE_XL_PROVIDER =
|
|
2055
|
+
process.env.CODEX_MODEL_PROFILE_XL_PROVIDER || providerName;
|
|
2056
|
+
env.CODEX_MODEL_PROFILE_M_PROVIDER =
|
|
2057
|
+
process.env.CODEX_MODEL_PROFILE_M_PROVIDER || providerName;
|
|
2058
|
+
|
|
2059
|
+
if (!env.CODEX_SUBAGENT_MODEL) {
|
|
2060
|
+
env.CODEX_SUBAGENT_MODEL =
|
|
2061
|
+
env.CODEX_MODEL_PROFILE_M_MODEL || "gpt-5.1-codex-mini";
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
// ── Step 6: Telegram ──────────────────────────────────
|
|
2067
|
+
heading("Step 6 of 9 — Telegram Notifications");
|
|
2068
|
+
console.log(
|
|
2069
|
+
" The Telegram bot sends real-time notifications and lets you\n" +
|
|
2070
|
+
" control the orchestrator via /status, /tasks, /restart, etc.\n",
|
|
2071
|
+
);
|
|
2072
|
+
|
|
2073
|
+
const wantTelegram = await prompt.confirm(
|
|
2074
|
+
"Set up Telegram notifications?",
|
|
2075
|
+
true,
|
|
2076
|
+
);
|
|
2077
|
+
if (wantTelegram) {
|
|
2078
|
+
// Step 1: Create bot
|
|
2079
|
+
console.log(
|
|
2080
|
+
"\n" +
|
|
2081
|
+
chalk.bold("Step 1: Create Your Bot") +
|
|
2082
|
+
chalk.dim(" (if you haven't already)"),
|
|
2083
|
+
);
|
|
2084
|
+
console.log(
|
|
2085
|
+
" 1. Open Telegram and search for " + chalk.cyan("@BotFather"),
|
|
2086
|
+
);
|
|
2087
|
+
console.log(" 2. Send: " + chalk.cyan("/newbot"));
|
|
2088
|
+
console.log(" 3. Choose a display name (e.g., 'MyProject Monitor')");
|
|
2089
|
+
console.log(
|
|
2090
|
+
" 4. Choose a username ending in 'bot' (e.g., 'myproject_monitor_bot')",
|
|
2091
|
+
);
|
|
2092
|
+
console.log(" 5. Copy the bot token BotFather gives you");
|
|
2093
|
+
console.log();
|
|
2094
|
+
|
|
2095
|
+
const hasBotReady = await prompt.confirm(
|
|
2096
|
+
"Have you created your bot and have the token ready?",
|
|
2097
|
+
false,
|
|
2098
|
+
);
|
|
2099
|
+
|
|
2100
|
+
if (!hasBotReady) {
|
|
2101
|
+
warn("No problem! You can set up Telegram later by:");
|
|
2102
|
+
console.log(" 1. Adding TELEGRAM_BOT_TOKEN to .env");
|
|
2103
|
+
console.log(" 2. Adding TELEGRAM_CHAT_ID to .env");
|
|
2104
|
+
console.log(" 3. Or re-running: openfleet --setup");
|
|
2105
|
+
console.log();
|
|
2106
|
+
} else {
|
|
2107
|
+
// Step 2: Get bot token
|
|
2108
|
+
console.log("\n" + chalk.bold("Step 2: Enter Your Bot Token"));
|
|
2109
|
+
console.log(
|
|
2110
|
+
chalk.dim(
|
|
2111
|
+
" Looks like: 1234567890:ABCdefGHIjklMNOpqrsTUVwxyz-1234567890",
|
|
2112
|
+
),
|
|
2113
|
+
);
|
|
2114
|
+
console.log();
|
|
2115
|
+
|
|
2116
|
+
env.TELEGRAM_BOT_TOKEN = await prompt.ask(
|
|
2117
|
+
"Bot Token",
|
|
2118
|
+
process.env.TELEGRAM_BOT_TOKEN || "",
|
|
2119
|
+
);
|
|
2120
|
+
|
|
2121
|
+
if (env.TELEGRAM_BOT_TOKEN && env.TELEGRAM_BOT_TOKEN.length > 20) {
|
|
2122
|
+
// Validate token format
|
|
2123
|
+
const tokenValid = /^\d+:[A-Za-z0-9_-]+$/.test(
|
|
2124
|
+
env.TELEGRAM_BOT_TOKEN,
|
|
2125
|
+
);
|
|
2126
|
+
if (!tokenValid) {
|
|
2127
|
+
warn(
|
|
2128
|
+
"Token format looks incorrect. Make sure you copied the full token from BotFather.",
|
|
2129
|
+
);
|
|
2130
|
+
} else {
|
|
2131
|
+
info("✓ Token format looks good");
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
// Step 3: Get chat ID
|
|
2135
|
+
console.log("\n" + chalk.bold("Step 3: Get Your Chat ID"));
|
|
2136
|
+
console.log(" Your chat ID tells the bot where to send messages.");
|
|
2137
|
+
console.log();
|
|
2138
|
+
|
|
2139
|
+
const knowsChatId = await prompt.confirm(
|
|
2140
|
+
"Do you already know your chat ID?",
|
|
2141
|
+
false,
|
|
2142
|
+
);
|
|
2143
|
+
|
|
2144
|
+
if (knowsChatId) {
|
|
2145
|
+
env.TELEGRAM_CHAT_ID = await prompt.ask(
|
|
2146
|
+
"Chat ID (numeric, e.g., 123456789)",
|
|
2147
|
+
process.env.TELEGRAM_CHAT_ID || "",
|
|
2148
|
+
);
|
|
2149
|
+
} else {
|
|
2150
|
+
// Guide user to get chat ID
|
|
2151
|
+
console.log("\n" + chalk.cyan("To get your chat ID:") + "\n");
|
|
2152
|
+
console.log(
|
|
2153
|
+
" 1. Open Telegram and search for your bot's username",
|
|
2154
|
+
);
|
|
2155
|
+
console.log(
|
|
2156
|
+
" 2. Click " +
|
|
2157
|
+
chalk.cyan("START") +
|
|
2158
|
+
" or send any message (e.g., 'Hello')",
|
|
2159
|
+
);
|
|
2160
|
+
console.log(" 3. Come back here and we'll detect your chat ID");
|
|
2161
|
+
console.log();
|
|
2162
|
+
|
|
2163
|
+
const ready = await prompt.confirm(
|
|
2164
|
+
"Ready? (I've messaged my bot)",
|
|
2165
|
+
false,
|
|
2166
|
+
);
|
|
2167
|
+
|
|
2168
|
+
if (ready) {
|
|
2169
|
+
// Try to fetch chat ID from Telegram API
|
|
2170
|
+
info("Fetching your chat ID from Telegram...");
|
|
2171
|
+
try {
|
|
2172
|
+
const response = await fetch(
|
|
2173
|
+
`https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/getUpdates`,
|
|
2174
|
+
);
|
|
2175
|
+
const data = await response.json();
|
|
2176
|
+
|
|
2177
|
+
if (data.ok && data.result && data.result.length > 0) {
|
|
2178
|
+
// Find the most recent message
|
|
2179
|
+
const latestMessage = data.result[data.result.length - 1];
|
|
2180
|
+
const chatId = latestMessage?.message?.chat?.id;
|
|
2181
|
+
|
|
2182
|
+
if (chatId) {
|
|
2183
|
+
env.TELEGRAM_CHAT_ID = String(chatId);
|
|
2184
|
+
info(`✓ Found your chat ID: ${chatId}`);
|
|
2185
|
+
console.log();
|
|
2186
|
+
} else {
|
|
2187
|
+
warn(
|
|
2188
|
+
"Couldn't find a chat ID. Make sure you sent a message to your bot.",
|
|
2189
|
+
);
|
|
2190
|
+
env.TELEGRAM_CHAT_ID = await prompt.ask(
|
|
2191
|
+
"Enter chat ID manually",
|
|
2192
|
+
"",
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
} else {
|
|
2196
|
+
warn(
|
|
2197
|
+
"No messages found. Make sure you sent a message to your bot first.",
|
|
2198
|
+
);
|
|
2199
|
+
console.log(
|
|
2200
|
+
chalk.dim(
|
|
2201
|
+
" Or run: openfleet-chat-id (after starting the bot)",
|
|
2202
|
+
),
|
|
2203
|
+
);
|
|
2204
|
+
env.TELEGRAM_CHAT_ID = await prompt.ask(
|
|
2205
|
+
"Enter chat ID manually (or leave empty to set up later)",
|
|
2206
|
+
"",
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
} catch (err) {
|
|
2210
|
+
warn(`Failed to fetch chat ID: ${err.message}`);
|
|
2211
|
+
console.log(
|
|
2212
|
+
chalk.dim(
|
|
2213
|
+
" You can run: openfleet-chat-id (after starting the bot)",
|
|
2214
|
+
),
|
|
2215
|
+
);
|
|
2216
|
+
env.TELEGRAM_CHAT_ID = await prompt.ask(
|
|
2217
|
+
"Enter chat ID manually (or leave empty to set up later)",
|
|
2218
|
+
"",
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
} else {
|
|
2222
|
+
console.log();
|
|
2223
|
+
info("No problem! You can get your chat ID later by:");
|
|
2224
|
+
console.log(
|
|
2225
|
+
" • Running: " +
|
|
2226
|
+
chalk.cyan("openfleet-chat-id") +
|
|
2227
|
+
" (after starting openfleet)",
|
|
2228
|
+
);
|
|
2229
|
+
console.log(
|
|
2230
|
+
" • Or manually: " +
|
|
2231
|
+
chalk.cyan(
|
|
2232
|
+
"curl 'https://api.telegram.org/bot<TOKEN>/getUpdates'",
|
|
2233
|
+
),
|
|
2234
|
+
);
|
|
2235
|
+
console.log(" Then add TELEGRAM_CHAT_ID to .env");
|
|
2236
|
+
console.log();
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
// Step 4: Verify setup
|
|
2241
|
+
if (env.TELEGRAM_CHAT_ID) {
|
|
2242
|
+
console.log("\n" + chalk.bold("Step 4: Test Your Setup"));
|
|
2243
|
+
const testNow = await prompt.confirm(
|
|
2244
|
+
"Send a test message to verify setup?",
|
|
2245
|
+
true,
|
|
2246
|
+
);
|
|
2247
|
+
|
|
2248
|
+
if (testNow) {
|
|
2249
|
+
info("Sending test message...");
|
|
2250
|
+
try {
|
|
2251
|
+
const testMsg =
|
|
2252
|
+
"🤖 *Telegram Bot Test*\n\n" +
|
|
2253
|
+
"Your openfleet Telegram bot is configured correctly!\n\n" +
|
|
2254
|
+
`Project: ${env.PROJECT_NAME || configJson.projectName || "Unknown"}\n` +
|
|
2255
|
+
"Try: /status, /tasks, /help";
|
|
2256
|
+
|
|
2257
|
+
const response = await fetch(
|
|
2258
|
+
`https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendMessage`,
|
|
2259
|
+
{
|
|
2260
|
+
method: "POST",
|
|
2261
|
+
headers: { "Content-Type": "application/json" },
|
|
2262
|
+
body: JSON.stringify({
|
|
2263
|
+
chat_id: env.TELEGRAM_CHAT_ID,
|
|
2264
|
+
text: testMsg,
|
|
2265
|
+
parse_mode: "Markdown",
|
|
2266
|
+
}),
|
|
2267
|
+
},
|
|
2268
|
+
);
|
|
2269
|
+
|
|
2270
|
+
const result = await response.json();
|
|
2271
|
+
if (result.ok) {
|
|
2272
|
+
info("✓ Test message sent! Check your Telegram.");
|
|
2273
|
+
} else {
|
|
2274
|
+
warn(
|
|
2275
|
+
`Test message failed: ${result.description || "Unknown error"}`,
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
} catch (err) {
|
|
2279
|
+
warn(`Failed to send test message: ${err.message}`);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
} else {
|
|
2284
|
+
warn(
|
|
2285
|
+
"Bot token is required for Telegram setup. You can add it to .env later.",
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
// ── Step 7: Kanban + Execution ─────────────────────────
|
|
2292
|
+
heading("Step 7 of 9 — Kanban & Execution");
|
|
2293
|
+
const backendDefault = String(
|
|
2294
|
+
process.env.KANBAN_BACKEND || configJson.kanban?.backend || "internal",
|
|
2295
|
+
)
|
|
2296
|
+
.trim()
|
|
2297
|
+
.toLowerCase();
|
|
2298
|
+
const backendIdx = await prompt.choose(
|
|
2299
|
+
"Select task board backend:",
|
|
2300
|
+
[
|
|
2301
|
+
"Internal Store (internal, recommended primary)",
|
|
2302
|
+
"Vibe-Kanban (vk)",
|
|
2303
|
+
"GitHub Issues (github)",
|
|
2304
|
+
"Jira Issues (jira)",
|
|
2305
|
+
],
|
|
2306
|
+
backendDefault === "vk"
|
|
2307
|
+
? 1
|
|
2308
|
+
: backendDefault === "github"
|
|
2309
|
+
? 2
|
|
2310
|
+
: backendDefault === "jira"
|
|
2311
|
+
? 3
|
|
2312
|
+
: 0,
|
|
2313
|
+
);
|
|
2314
|
+
const selectedKanbanBackend =
|
|
2315
|
+
backendIdx === 1
|
|
2316
|
+
? "vk"
|
|
2317
|
+
: backendIdx === 2
|
|
2318
|
+
? "github"
|
|
2319
|
+
: backendIdx === 3
|
|
2320
|
+
? "jira"
|
|
2321
|
+
: "internal";
|
|
2322
|
+
env.KANBAN_BACKEND = selectedKanbanBackend;
|
|
2323
|
+
const syncPolicyIdx = await prompt.choose(
|
|
2324
|
+
"Select sync policy:",
|
|
2325
|
+
[
|
|
2326
|
+
"Internal primary (recommended) — external is secondary mirror",
|
|
2327
|
+
"Bidirectional (legacy) — external can drive internal status",
|
|
2328
|
+
],
|
|
2329
|
+
0,
|
|
2330
|
+
);
|
|
2331
|
+
const selectedSyncPolicy =
|
|
2332
|
+
syncPolicyIdx === 1 ? "bidirectional" : "internal-primary";
|
|
2333
|
+
env.KANBAN_SYNC_POLICY = selectedSyncPolicy;
|
|
2334
|
+
configJson.kanban = {
|
|
2335
|
+
backend: selectedKanbanBackend,
|
|
2336
|
+
syncPolicy: selectedSyncPolicy,
|
|
2337
|
+
};
|
|
2338
|
+
|
|
2339
|
+
const modeDefault = String(
|
|
2340
|
+
process.env.EXECUTOR_MODE || configJson.internalExecutor?.mode || "internal",
|
|
2341
|
+
)
|
|
2342
|
+
.trim()
|
|
2343
|
+
.toLowerCase();
|
|
2344
|
+
const execModeIdx = await prompt.choose(
|
|
2345
|
+
"Select execution mode:",
|
|
2346
|
+
[
|
|
2347
|
+
"Internal executor (recommended)",
|
|
2348
|
+
"VK executor/orchestrator",
|
|
2349
|
+
"Hybrid (internal + VK)",
|
|
2350
|
+
],
|
|
2351
|
+
selectedKanbanBackend === "internal" ||
|
|
2352
|
+
selectedKanbanBackend === "github" ||
|
|
2353
|
+
selectedKanbanBackend === "jira"
|
|
2354
|
+
? 0
|
|
2355
|
+
: modeDefault === "hybrid"
|
|
2356
|
+
? 2
|
|
2357
|
+
: modeDefault === "internal"
|
|
2358
|
+
? 0
|
|
2359
|
+
: 1,
|
|
2360
|
+
);
|
|
2361
|
+
const selectedExecutorMode =
|
|
2362
|
+
execModeIdx === 0 ? "internal" : execModeIdx === 1 ? "vk" : "hybrid";
|
|
2363
|
+
env.EXECUTOR_MODE = selectedExecutorMode;
|
|
2364
|
+
configJson.internalExecutor = {
|
|
2365
|
+
...(configJson.internalExecutor || {}),
|
|
2366
|
+
mode: selectedExecutorMode,
|
|
2367
|
+
};
|
|
2368
|
+
|
|
2369
|
+
const requirementsProfileDefault = String(
|
|
2370
|
+
process.env.PROJECT_REQUIREMENTS_PROFILE ||
|
|
2371
|
+
configJson.projectRequirements?.profile ||
|
|
2372
|
+
"feature",
|
|
2373
|
+
)
|
|
2374
|
+
.trim()
|
|
2375
|
+
.toLowerCase();
|
|
2376
|
+
const profileOptions = [
|
|
2377
|
+
"simple-feature",
|
|
2378
|
+
"feature",
|
|
2379
|
+
"large-feature",
|
|
2380
|
+
"system",
|
|
2381
|
+
"multi-system",
|
|
2382
|
+
];
|
|
2383
|
+
const profileIdx = await prompt.choose(
|
|
2384
|
+
"Project requirements profile:",
|
|
2385
|
+
[
|
|
2386
|
+
"Simple Feature",
|
|
2387
|
+
"Feature",
|
|
2388
|
+
"Large Feature",
|
|
2389
|
+
"System",
|
|
2390
|
+
"Multi-System",
|
|
2391
|
+
],
|
|
2392
|
+
Math.max(0, profileOptions.indexOf(requirementsProfileDefault)),
|
|
2393
|
+
);
|
|
2394
|
+
env.PROJECT_REQUIREMENTS_PROFILE = profileOptions[profileIdx] || "feature";
|
|
2395
|
+
const requirementsNotes = await prompt.ask(
|
|
2396
|
+
"Requirements notes (optional)",
|
|
2397
|
+
process.env.PROJECT_REQUIREMENTS_NOTES ||
|
|
2398
|
+
configJson.projectRequirements?.notes ||
|
|
2399
|
+
"",
|
|
2400
|
+
);
|
|
2401
|
+
env.PROJECT_REQUIREMENTS_NOTES = requirementsNotes;
|
|
2402
|
+
configJson.projectRequirements = {
|
|
2403
|
+
profile: env.PROJECT_REQUIREMENTS_PROFILE,
|
|
2404
|
+
notes: env.PROJECT_REQUIREMENTS_NOTES,
|
|
2405
|
+
};
|
|
2406
|
+
|
|
2407
|
+
const replenishEnabled = await prompt.confirm(
|
|
2408
|
+
"Enable experimental autonomous backlog replenishment?",
|
|
2409
|
+
false,
|
|
2410
|
+
);
|
|
2411
|
+
env.INTERNAL_EXECUTOR_REPLENISH_ENABLED = replenishEnabled
|
|
2412
|
+
? "true"
|
|
2413
|
+
: "false";
|
|
2414
|
+
const replenishMin = replenishEnabled
|
|
2415
|
+
? await prompt.ask(
|
|
2416
|
+
"Minimum new tasks per completed task (1-2)",
|
|
2417
|
+
process.env.INTERNAL_EXECUTOR_REPLENISH_MIN_NEW_TASKS || "1",
|
|
2418
|
+
)
|
|
2419
|
+
: "1";
|
|
2420
|
+
const replenishMax = replenishEnabled
|
|
2421
|
+
? await prompt.ask(
|
|
2422
|
+
"Maximum new tasks per completed task (1-3)",
|
|
2423
|
+
process.env.INTERNAL_EXECUTOR_REPLENISH_MAX_NEW_TASKS || "2",
|
|
2424
|
+
)
|
|
2425
|
+
: "2";
|
|
2426
|
+
env.INTERNAL_EXECUTOR_REPLENISH_MIN_NEW_TASKS = replenishMin;
|
|
2427
|
+
env.INTERNAL_EXECUTOR_REPLENISH_MAX_NEW_TASKS = replenishMax;
|
|
2428
|
+
configJson.internalExecutor = {
|
|
2429
|
+
...(configJson.internalExecutor || {}),
|
|
2430
|
+
backlogReplenishment: {
|
|
2431
|
+
enabled: replenishEnabled,
|
|
2432
|
+
minNewTasks: toPositiveInt(replenishMin, 1),
|
|
2433
|
+
maxNewTasks: toPositiveInt(replenishMax, 2),
|
|
2434
|
+
requirePriority: true,
|
|
2435
|
+
},
|
|
2436
|
+
projectRequirements: {
|
|
2437
|
+
profile: env.PROJECT_REQUIREMENTS_PROFILE,
|
|
2438
|
+
notes: env.PROJECT_REQUIREMENTS_NOTES,
|
|
2439
|
+
},
|
|
2440
|
+
};
|
|
2441
|
+
|
|
2442
|
+
const vkNeeded =
|
|
2443
|
+
selectedKanbanBackend === "vk" ||
|
|
2444
|
+
selectedExecutorMode === "vk" ||
|
|
2445
|
+
selectedExecutorMode === "hybrid";
|
|
2446
|
+
|
|
2447
|
+
if (selectedKanbanBackend === "github") {
|
|
2448
|
+
const [slugOwner, slugRepo] = String(slug || "").split("/", 2);
|
|
2449
|
+
env.GITHUB_REPO_OWNER = await prompt.ask(
|
|
2450
|
+
"GitHub owner/org",
|
|
2451
|
+
process.env.GITHUB_REPO_OWNER || slugOwner || "",
|
|
2452
|
+
);
|
|
2453
|
+
env.GITHUB_REPO_NAME = await prompt.ask(
|
|
2454
|
+
"GitHub repository name",
|
|
2455
|
+
process.env.GITHUB_REPO_NAME || slugRepo || basename(repoRoot),
|
|
2456
|
+
);
|
|
2457
|
+
if (env.GITHUB_REPO_OWNER && env.GITHUB_REPO_NAME) {
|
|
2458
|
+
env.GITHUB_REPOSITORY = `${env.GITHUB_REPO_OWNER}/${env.GITHUB_REPO_NAME}`;
|
|
2459
|
+
env.KANBAN_PROJECT_ID = env.GITHUB_REPOSITORY;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
const githubTaskModeDefault = String(
|
|
2463
|
+
process.env.GITHUB_PROJECT_MODE ||
|
|
2464
|
+
configJson.kanban?.github?.mode ||
|
|
2465
|
+
"kanban",
|
|
2466
|
+
)
|
|
2467
|
+
.trim()
|
|
2468
|
+
.toLowerCase();
|
|
2469
|
+
const githubTaskModeIdx = await prompt.choose(
|
|
2470
|
+
"Use GitHub backend as:",
|
|
2471
|
+
[
|
|
2472
|
+
"GitHub Projects Kanban (default)",
|
|
2473
|
+
"GitHub Issues only (no Projects board)",
|
|
2474
|
+
],
|
|
2475
|
+
githubTaskModeDefault === "issues" ? 1 : 0,
|
|
2476
|
+
);
|
|
2477
|
+
const githubTaskMode = githubTaskModeIdx === 1 ? "issues" : "kanban";
|
|
2478
|
+
env.GITHUB_PROJECT_MODE = githubTaskMode;
|
|
2479
|
+
|
|
2480
|
+
const detectedLogin = detectGitHubUserLogin(repoRoot);
|
|
2481
|
+
if (!env.GITHUB_DEFAULT_ASSIGNEE) {
|
|
2482
|
+
env.GITHUB_DEFAULT_ASSIGNEE =
|
|
2483
|
+
process.env.GITHUB_DEFAULT_ASSIGNEE ||
|
|
2484
|
+
detectedLogin ||
|
|
2485
|
+
env.GITHUB_REPO_OWNER ||
|
|
2486
|
+
"";
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
const canonicalLabel = "openfleet";
|
|
2490
|
+
const existingScopeLabels = String(
|
|
2491
|
+
process.env.CODEX_MONITOR_TASK_LABELS || "",
|
|
2492
|
+
)
|
|
2493
|
+
.split(",")
|
|
2494
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
2495
|
+
.filter(Boolean);
|
|
2496
|
+
if (!existingScopeLabels.includes(canonicalLabel)) {
|
|
2497
|
+
existingScopeLabels.unshift(canonicalLabel);
|
|
2498
|
+
}
|
|
2499
|
+
if (!existingScopeLabels.includes("codex-mointor")) {
|
|
2500
|
+
existingScopeLabels.push("codex-mointor");
|
|
2501
|
+
}
|
|
2502
|
+
env.CODEX_MONITOR_TASK_LABEL = canonicalLabel;
|
|
2503
|
+
env.CODEX_MONITOR_TASK_LABELS = existingScopeLabels.join(",");
|
|
2504
|
+
env.CODEX_MONITOR_ENFORCE_TASK_LABEL = "true";
|
|
2505
|
+
|
|
2506
|
+
if (githubTaskMode === "kanban") {
|
|
2507
|
+
env.GITHUB_PROJECT_OWNER =
|
|
2508
|
+
process.env.GITHUB_PROJECT_OWNER || env.GITHUB_REPO_OWNER || "";
|
|
2509
|
+
env.GITHUB_PROJECT_TITLE = await prompt.ask(
|
|
2510
|
+
"GitHub Project title",
|
|
2511
|
+
process.env.GITHUB_PROJECT_TITLE ||
|
|
2512
|
+
configJson.kanban?.github?.projectTitle ||
|
|
2513
|
+
"OpenFleet",
|
|
2514
|
+
);
|
|
2515
|
+
const resolvedProject = resolveOrCreateGitHubProject({
|
|
2516
|
+
owner: env.GITHUB_PROJECT_OWNER,
|
|
2517
|
+
title: env.GITHUB_PROJECT_TITLE,
|
|
2518
|
+
cwd: repoRoot,
|
|
2519
|
+
repoOwner: env.GITHUB_REPO_OWNER,
|
|
2520
|
+
githubLogin: detectedLogin,
|
|
2521
|
+
});
|
|
2522
|
+
if (resolvedProject.number) {
|
|
2523
|
+
env.GITHUB_PROJECT_NUMBER = resolvedProject.number;
|
|
2524
|
+
const linkedOwner = resolvedProject.owner || env.GITHUB_PROJECT_OWNER;
|
|
2525
|
+
if (linkedOwner) {
|
|
2526
|
+
env.GITHUB_PROJECT_OWNER = linkedOwner;
|
|
2527
|
+
}
|
|
2528
|
+
success(
|
|
2529
|
+
`GitHub Project linked: ${env.GITHUB_PROJECT_OWNER}#${resolvedProject.number} (${env.GITHUB_PROJECT_TITLE})`,
|
|
2530
|
+
);
|
|
2531
|
+
} else {
|
|
2532
|
+
const reasonSuffix = resolvedProject.reason
|
|
2533
|
+
? ` Reason: ${resolvedProject.reason}`
|
|
2534
|
+
: "";
|
|
2535
|
+
warn(
|
|
2536
|
+
`Could not auto-detect/create GitHub Project. Issues will still be created and can be linked later.${reasonSuffix}`,
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
configJson.kanban = {
|
|
2542
|
+
backend: selectedKanbanBackend,
|
|
2543
|
+
syncPolicy: selectedSyncPolicy,
|
|
2544
|
+
github: {
|
|
2545
|
+
mode: githubTaskMode,
|
|
2546
|
+
projectTitle: env.GITHUB_PROJECT_TITLE || "OpenFleet",
|
|
2547
|
+
projectOwner: env.GITHUB_PROJECT_OWNER || env.GITHUB_REPO_OWNER || "",
|
|
2548
|
+
projectNumber: env.GITHUB_PROJECT_NUMBER || "",
|
|
2549
|
+
taskLabel: env.CODEX_MONITOR_TASK_LABEL || "openfleet",
|
|
2550
|
+
},
|
|
2551
|
+
};
|
|
2552
|
+
info(
|
|
2553
|
+
"GitHub backend configured. openfleet-scoped issues are auto-assigned/labeled and can be linked to a Projects kanban board.",
|
|
2554
|
+
);
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
if (selectedKanbanBackend === "jira") {
|
|
2558
|
+
const jiraBaseDefault =
|
|
2559
|
+
process.env.JIRA_BASE_URL || configJson.kanban?.jira?.baseUrl || "";
|
|
2560
|
+
const jiraEmailDefault =
|
|
2561
|
+
process.env.JIRA_EMAIL || configJson.kanban?.jira?.email || "";
|
|
2562
|
+
const jiraTokenDefault =
|
|
2563
|
+
process.env.JIRA_API_TOKEN || configJson.kanban?.jira?.apiToken || "";
|
|
2564
|
+
const jiraProjectDefault =
|
|
2565
|
+
process.env.JIRA_PROJECT_KEY || configJson.kanban?.jira?.projectKey || "";
|
|
2566
|
+
const jiraIssueTypeDefault =
|
|
2567
|
+
process.env.JIRA_ISSUE_TYPE || configJson.kanban?.jira?.issueType || "Task";
|
|
2568
|
+
|
|
2569
|
+
env.JIRA_BASE_URL = normalizeBaseUrl(
|
|
2570
|
+
await prompt.ask("Jira site URL", jiraBaseDefault),
|
|
2571
|
+
);
|
|
2572
|
+
if (env.JIRA_BASE_URL) {
|
|
2573
|
+
const openTokenPage = await prompt.confirm(
|
|
2574
|
+
"Open Jira API token page in your browser?",
|
|
2575
|
+
true,
|
|
2576
|
+
);
|
|
2577
|
+
if (openTokenPage) {
|
|
2578
|
+
const opened = openUrlInBrowser(
|
|
2579
|
+
"https://id.atlassian.com/manage-profile/security/api-tokens",
|
|
2580
|
+
);
|
|
2581
|
+
if (!opened) {
|
|
2582
|
+
warn(
|
|
2583
|
+
"Unable to open browser. Visit https://id.atlassian.com/manage-profile/security/api-tokens",
|
|
2584
|
+
);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
env.JIRA_EMAIL = await prompt.ask("Jira account email", jiraEmailDefault);
|
|
2590
|
+
env.JIRA_API_TOKEN = await prompt.ask(
|
|
2591
|
+
"Jira API token",
|
|
2592
|
+
jiraTokenDefault,
|
|
2593
|
+
);
|
|
2594
|
+
|
|
2595
|
+
const hasJiraCreds =
|
|
2596
|
+
Boolean(env.JIRA_BASE_URL) &&
|
|
2597
|
+
Boolean(env.JIRA_EMAIL) &&
|
|
2598
|
+
Boolean(env.JIRA_API_TOKEN);
|
|
2599
|
+
|
|
2600
|
+
let projects = [];
|
|
2601
|
+
if (hasJiraCreds) {
|
|
2602
|
+
const lookupProjects = await prompt.confirm(
|
|
2603
|
+
"Look up Jira projects now?",
|
|
2604
|
+
true,
|
|
2605
|
+
);
|
|
2606
|
+
if (lookupProjects) {
|
|
2607
|
+
try {
|
|
2608
|
+
projects = await listJiraProjects({
|
|
2609
|
+
baseUrl: env.JIRA_BASE_URL,
|
|
2610
|
+
email: env.JIRA_EMAIL,
|
|
2611
|
+
token: env.JIRA_API_TOKEN,
|
|
2612
|
+
});
|
|
2613
|
+
} catch (err) {
|
|
2614
|
+
warn(`Failed to load Jira projects: ${err.message}`);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
const selectProjectKey = async (projectList, fallbackKey) => {
|
|
2620
|
+
if (!Array.isArray(projectList) || projectList.length === 0) {
|
|
2621
|
+
return await prompt.ask("Jira project key", fallbackKey || "");
|
|
2622
|
+
}
|
|
2623
|
+
const filter = await prompt.ask(
|
|
2624
|
+
"Filter Jira projects (optional)",
|
|
2625
|
+
"",
|
|
2626
|
+
);
|
|
2627
|
+
const normalizedFilter = filter.trim().toLowerCase();
|
|
2628
|
+
const filtered = normalizedFilter
|
|
2629
|
+
? projectList.filter(
|
|
2630
|
+
(project) =>
|
|
2631
|
+
String(project.name || "").toLowerCase().includes(normalizedFilter) ||
|
|
2632
|
+
String(project.key || "").toLowerCase().includes(normalizedFilter),
|
|
2633
|
+
)
|
|
2634
|
+
: projectList;
|
|
2635
|
+
const visible = filtered.slice(0, 20);
|
|
2636
|
+
const options = visible.map(
|
|
2637
|
+
(project) => `${project.name} (${project.key})`,
|
|
2638
|
+
);
|
|
2639
|
+
options.push("Enter project key manually");
|
|
2640
|
+
options.push("Open Jira Projects page");
|
|
2641
|
+
options.push("Create a new Jira project");
|
|
2642
|
+
const choiceIdx = await prompt.choose(
|
|
2643
|
+
"Select Jira project for openfleet tasks:",
|
|
2644
|
+
options,
|
|
2645
|
+
0,
|
|
2646
|
+
);
|
|
2647
|
+
if (choiceIdx < visible.length) {
|
|
2648
|
+
return visible[choiceIdx].key;
|
|
2649
|
+
}
|
|
2650
|
+
if (choiceIdx === visible.length) {
|
|
2651
|
+
return await prompt.ask("Jira project key", fallbackKey || "");
|
|
2652
|
+
}
|
|
2653
|
+
if (choiceIdx === visible.length + 1) {
|
|
2654
|
+
const url = `${env.JIRA_BASE_URL}/jira/projects`;
|
|
2655
|
+
const opened = openUrlInBrowser(url);
|
|
2656
|
+
if (!opened) warn(`Open this URL manually: ${url}`);
|
|
2657
|
+
const requery = hasJiraCreds
|
|
2658
|
+
? await prompt.confirm("Re-fetch Jira projects now?", true)
|
|
2659
|
+
: false;
|
|
2660
|
+
if (requery) {
|
|
2661
|
+
try {
|
|
2662
|
+
const refreshed = await listJiraProjects({
|
|
2663
|
+
baseUrl: env.JIRA_BASE_URL,
|
|
2664
|
+
email: env.JIRA_EMAIL,
|
|
2665
|
+
token: env.JIRA_API_TOKEN,
|
|
2666
|
+
});
|
|
2667
|
+
return await selectProjectKey(refreshed, fallbackKey);
|
|
2668
|
+
} catch (err) {
|
|
2669
|
+
warn(`Failed to refresh Jira projects: ${err.message}`);
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
return await prompt.ask("Jira project key", fallbackKey || "");
|
|
2673
|
+
}
|
|
2674
|
+
const createUrl = `${env.JIRA_BASE_URL}/jira/projects`;
|
|
2675
|
+
const opened = openUrlInBrowser(createUrl);
|
|
2676
|
+
if (!opened) warn(`Open this URL manually: ${createUrl}`);
|
|
2677
|
+
info("Create the project in Jira, then enter the new project key.");
|
|
2678
|
+
const createdKey = await prompt.ask("New Jira project key", "");
|
|
2679
|
+
if (!createdKey) {
|
|
2680
|
+
return await prompt.ask("Jira project key", fallbackKey || "");
|
|
2681
|
+
}
|
|
2682
|
+
if (hasJiraCreds) {
|
|
2683
|
+
const requery = await prompt.confirm(
|
|
2684
|
+
"Re-fetch Jira projects now?",
|
|
2685
|
+
true,
|
|
2686
|
+
);
|
|
2687
|
+
if (requery) {
|
|
2688
|
+
try {
|
|
2689
|
+
const refreshed = await listJiraProjects({
|
|
2690
|
+
baseUrl: env.JIRA_BASE_URL,
|
|
2691
|
+
email: env.JIRA_EMAIL,
|
|
2692
|
+
token: env.JIRA_API_TOKEN,
|
|
2693
|
+
});
|
|
2694
|
+
const match = refreshed.find(
|
|
2695
|
+
(project) =>
|
|
2696
|
+
String(project.key || "").toUpperCase() ===
|
|
2697
|
+
String(createdKey || "").toUpperCase(),
|
|
2698
|
+
);
|
|
2699
|
+
if (match) return match.key;
|
|
2700
|
+
} catch (err) {
|
|
2701
|
+
warn(`Failed to refresh Jira projects: ${err.message}`);
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
return createdKey;
|
|
2706
|
+
};
|
|
2707
|
+
|
|
2708
|
+
env.JIRA_PROJECT_KEY = String(
|
|
2709
|
+
await selectProjectKey(projects, jiraProjectDefault),
|
|
2710
|
+
)
|
|
2711
|
+
.trim()
|
|
2712
|
+
.toUpperCase();
|
|
2713
|
+
|
|
2714
|
+
let jiraIssueType = jiraIssueTypeDefault;
|
|
2715
|
+
if (hasJiraCreds) {
|
|
2716
|
+
const lookupIssueTypes = await prompt.confirm(
|
|
2717
|
+
"Look up Jira issue types now?",
|
|
2718
|
+
isAdvancedSetup,
|
|
2719
|
+
);
|
|
2720
|
+
if (lookupIssueTypes) {
|
|
2721
|
+
try {
|
|
2722
|
+
const issueTypes = await listJiraIssueTypes({
|
|
2723
|
+
baseUrl: env.JIRA_BASE_URL,
|
|
2724
|
+
email: env.JIRA_EMAIL,
|
|
2725
|
+
token: env.JIRA_API_TOKEN,
|
|
2726
|
+
});
|
|
2727
|
+
if (issueTypes.length > 0) {
|
|
2728
|
+
const issueOptions = issueTypes.map((entry) =>
|
|
2729
|
+
entry.subtask ? `${entry.name} (subtask)` : entry.name,
|
|
2730
|
+
);
|
|
2731
|
+
issueOptions.push("Enter issue type manually");
|
|
2732
|
+
const defaultIdx = Math.max(
|
|
2733
|
+
0,
|
|
2734
|
+
issueOptions.findIndex(
|
|
2735
|
+
(option) =>
|
|
2736
|
+
option.toLowerCase() === jiraIssueType.toLowerCase() ||
|
|
2737
|
+
option
|
|
2738
|
+
.toLowerCase()
|
|
2739
|
+
.startsWith(jiraIssueType.toLowerCase()),
|
|
2740
|
+
),
|
|
2741
|
+
);
|
|
2742
|
+
const issueIdx = await prompt.choose(
|
|
2743
|
+
"Select default Jira issue type:",
|
|
2744
|
+
issueOptions,
|
|
2745
|
+
defaultIdx,
|
|
2746
|
+
);
|
|
2747
|
+
if (issueIdx < issueTypes.length) {
|
|
2748
|
+
jiraIssueType = issueTypes[issueIdx].name;
|
|
2749
|
+
} else {
|
|
2750
|
+
jiraIssueType = await prompt.ask(
|
|
2751
|
+
"Default Jira issue type",
|
|
2752
|
+
jiraIssueTypeDefault,
|
|
2753
|
+
);
|
|
2754
|
+
}
|
|
2755
|
+
} else {
|
|
2756
|
+
jiraIssueType = await prompt.ask(
|
|
2757
|
+
"Default Jira issue type",
|
|
2758
|
+
jiraIssueTypeDefault,
|
|
2759
|
+
);
|
|
2760
|
+
}
|
|
2761
|
+
} catch (err) {
|
|
2762
|
+
warn(`Failed to load Jira issue types: ${err.message}`);
|
|
2763
|
+
jiraIssueType = await prompt.ask(
|
|
2764
|
+
"Default Jira issue type",
|
|
2765
|
+
jiraIssueTypeDefault,
|
|
2766
|
+
);
|
|
2767
|
+
}
|
|
2768
|
+
} else {
|
|
2769
|
+
jiraIssueType = await prompt.ask(
|
|
2770
|
+
"Default Jira issue type",
|
|
2771
|
+
jiraIssueTypeDefault,
|
|
2772
|
+
);
|
|
2773
|
+
}
|
|
2774
|
+
} else {
|
|
2775
|
+
jiraIssueType = await prompt.ask(
|
|
2776
|
+
"Default Jira issue type",
|
|
2777
|
+
jiraIssueTypeDefault,
|
|
2778
|
+
);
|
|
2779
|
+
}
|
|
2780
|
+
env.JIRA_ISSUE_TYPE = jiraIssueType;
|
|
2781
|
+
|
|
2782
|
+
if (isSubtaskIssueType(env.JIRA_ISSUE_TYPE)) {
|
|
2783
|
+
env.JIRA_SUBTASK_PARENT_KEY = await prompt.ask(
|
|
2784
|
+
"Parent issue key for subtasks",
|
|
2785
|
+
process.env.JIRA_SUBTASK_PARENT_KEY || "",
|
|
2786
|
+
);
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
const canonicalLabel = "openfleet";
|
|
2790
|
+
const jiraScopeLabels = String(
|
|
2791
|
+
process.env.JIRA_TASK_LABELS ||
|
|
2792
|
+
process.env.CODEX_MONITOR_TASK_LABELS ||
|
|
2793
|
+
"",
|
|
2794
|
+
)
|
|
2795
|
+
.split(",")
|
|
2796
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
2797
|
+
.filter(Boolean);
|
|
2798
|
+
if (!jiraScopeLabels.includes(canonicalLabel)) {
|
|
2799
|
+
jiraScopeLabels.unshift(canonicalLabel);
|
|
2800
|
+
}
|
|
2801
|
+
if (!jiraScopeLabels.includes("codex-mointor")) {
|
|
2802
|
+
jiraScopeLabels.push("codex-mointor");
|
|
2803
|
+
}
|
|
2804
|
+
env.CODEX_MONITOR_TASK_LABEL = canonicalLabel;
|
|
2805
|
+
env.CODEX_MONITOR_TASK_LABELS = jiraScopeLabels.join(",");
|
|
2806
|
+
env.CODEX_MONITOR_ENFORCE_TASK_LABEL = "true";
|
|
2807
|
+
env.JIRA_TASK_LABELS = env.CODEX_MONITOR_TASK_LABELS;
|
|
2808
|
+
env.JIRA_ENFORCE_TASK_LABEL = "true";
|
|
2809
|
+
|
|
2810
|
+
if (hasJiraCreds) {
|
|
2811
|
+
const wantsAssignee = await prompt.confirm(
|
|
2812
|
+
"Set a default Jira assignee for new tasks?",
|
|
2813
|
+
false,
|
|
2814
|
+
);
|
|
2815
|
+
if (wantsAssignee) {
|
|
2816
|
+
const query = await prompt.ask(
|
|
2817
|
+
"Search users by name/email (optional)",
|
|
2818
|
+
"",
|
|
2819
|
+
);
|
|
2820
|
+
let selectedAccountId = "";
|
|
2821
|
+
if (query) {
|
|
2822
|
+
try {
|
|
2823
|
+
const users = await searchJiraUsers({
|
|
2824
|
+
baseUrl: env.JIRA_BASE_URL,
|
|
2825
|
+
email: env.JIRA_EMAIL,
|
|
2826
|
+
token: env.JIRA_API_TOKEN,
|
|
2827
|
+
query,
|
|
2828
|
+
});
|
|
2829
|
+
if (users.length > 0) {
|
|
2830
|
+
const userOptions = users.map((user) => {
|
|
2831
|
+
const emailSuffix = user.emailAddress
|
|
2832
|
+
? ` <${user.emailAddress}>`
|
|
2833
|
+
: "";
|
|
2834
|
+
return `${user.displayName}${emailSuffix} (${user.accountId})`;
|
|
2835
|
+
});
|
|
2836
|
+
userOptions.push("Enter account ID manually");
|
|
2837
|
+
const userIdx = await prompt.choose(
|
|
2838
|
+
"Select default Jira assignee:",
|
|
2839
|
+
userOptions,
|
|
2840
|
+
0,
|
|
2841
|
+
);
|
|
2842
|
+
if (userIdx < users.length) {
|
|
2843
|
+
selectedAccountId = users[userIdx].accountId;
|
|
2844
|
+
}
|
|
2845
|
+
} else {
|
|
2846
|
+
warn("No Jira users matched that search.");
|
|
2847
|
+
}
|
|
2848
|
+
} catch (err) {
|
|
2849
|
+
warn(`Failed to search Jira users: ${err.message}`);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
if (!selectedAccountId) {
|
|
2853
|
+
selectedAccountId = await prompt.ask(
|
|
2854
|
+
"Default assignee account ID",
|
|
2855
|
+
process.env.JIRA_DEFAULT_ASSIGNEE || "",
|
|
2856
|
+
);
|
|
2857
|
+
}
|
|
2858
|
+
env.JIRA_DEFAULT_ASSIGNEE = selectedAccountId;
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
if (isAdvancedSetup) {
|
|
2863
|
+
env.JIRA_STATUS_TODO = await prompt.ask(
|
|
2864
|
+
"Jira status for TODO",
|
|
2865
|
+
process.env.JIRA_STATUS_TODO ||
|
|
2866
|
+
configJson.kanban?.jira?.statusMapping?.todo ||
|
|
2867
|
+
"To Do",
|
|
2868
|
+
);
|
|
2869
|
+
env.JIRA_STATUS_INPROGRESS = await prompt.ask(
|
|
2870
|
+
"Jira status for IN PROGRESS",
|
|
2871
|
+
process.env.JIRA_STATUS_INPROGRESS ||
|
|
2872
|
+
configJson.kanban?.jira?.statusMapping?.inprogress ||
|
|
2873
|
+
"In Progress",
|
|
2874
|
+
);
|
|
2875
|
+
env.JIRA_STATUS_INREVIEW = await prompt.ask(
|
|
2876
|
+
"Jira status for IN REVIEW",
|
|
2877
|
+
process.env.JIRA_STATUS_INREVIEW ||
|
|
2878
|
+
configJson.kanban?.jira?.statusMapping?.inreview ||
|
|
2879
|
+
"In Review",
|
|
2880
|
+
);
|
|
2881
|
+
env.JIRA_STATUS_DONE = await prompt.ask(
|
|
2882
|
+
"Jira status for DONE",
|
|
2883
|
+
process.env.JIRA_STATUS_DONE ||
|
|
2884
|
+
configJson.kanban?.jira?.statusMapping?.done ||
|
|
2885
|
+
"Done",
|
|
2886
|
+
);
|
|
2887
|
+
env.JIRA_STATUS_CANCELLED = await prompt.ask(
|
|
2888
|
+
"Jira status for CANCELLED",
|
|
2889
|
+
process.env.JIRA_STATUS_CANCELLED ||
|
|
2890
|
+
configJson.kanban?.jira?.statusMapping?.cancelled ||
|
|
2891
|
+
"Cancelled",
|
|
2892
|
+
);
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
const configureSharedState = await prompt.confirm(
|
|
2896
|
+
"Configure Jira shared-state fields now?",
|
|
2897
|
+
isAdvancedSetup,
|
|
2898
|
+
);
|
|
2899
|
+
if (configureSharedState && hasJiraCreds) {
|
|
2900
|
+
let jiraFields = [];
|
|
2901
|
+
try {
|
|
2902
|
+
jiraFields = await listJiraFields({
|
|
2903
|
+
baseUrl: env.JIRA_BASE_URL,
|
|
2904
|
+
email: env.JIRA_EMAIL,
|
|
2905
|
+
token: env.JIRA_API_TOKEN,
|
|
2906
|
+
});
|
|
2907
|
+
} catch (err) {
|
|
2908
|
+
warn(`Failed to load Jira fields: ${err.message}`);
|
|
2909
|
+
}
|
|
2910
|
+
if (jiraFields.length === 0) {
|
|
2911
|
+
const openFields = await prompt.confirm(
|
|
2912
|
+
"Open Jira custom fields page in your browser?",
|
|
2913
|
+
false,
|
|
2914
|
+
);
|
|
2915
|
+
if (openFields) {
|
|
2916
|
+
const url = `${env.JIRA_BASE_URL}/jira/settings/issues/fields`;
|
|
2917
|
+
const opened = openUrlInBrowser(url);
|
|
2918
|
+
if (!opened) warn(`Open this URL manually: ${url}`);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
const selectFieldId = async (label, fallbackValue) => {
|
|
2923
|
+
if (!jiraFields.length) {
|
|
2924
|
+
return await prompt.ask(`${label} field id`, fallbackValue || "");
|
|
2925
|
+
}
|
|
2926
|
+
const filter = await prompt.ask(
|
|
2927
|
+
`Filter fields for ${label} (optional)`,
|
|
2928
|
+
"",
|
|
2929
|
+
);
|
|
2930
|
+
const normalized = filter.trim().toLowerCase();
|
|
2931
|
+
const filtered = normalized
|
|
2932
|
+
? jiraFields.filter((field) =>
|
|
2933
|
+
String(field.name || "")
|
|
2934
|
+
.toLowerCase()
|
|
2935
|
+
.includes(normalized),
|
|
2936
|
+
)
|
|
2937
|
+
: jiraFields;
|
|
2938
|
+
const visible = filtered.slice(0, 20);
|
|
2939
|
+
const options = visible.map(
|
|
2940
|
+
(field) => `${field.name} (${field.id})`,
|
|
2941
|
+
);
|
|
2942
|
+
options.push("Enter field id manually");
|
|
2943
|
+
options.push("Skip");
|
|
2944
|
+
const choiceIdx = await prompt.choose(
|
|
2945
|
+
`Select Jira field for ${label}:`,
|
|
2946
|
+
options,
|
|
2947
|
+
0,
|
|
2948
|
+
);
|
|
2949
|
+
if (choiceIdx < visible.length) return visible[choiceIdx].id;
|
|
2950
|
+
if (choiceIdx === visible.length) {
|
|
2951
|
+
return await prompt.ask(`${label} field id`, fallbackValue || "");
|
|
2952
|
+
}
|
|
2953
|
+
return "";
|
|
2954
|
+
};
|
|
2955
|
+
|
|
2956
|
+
const storageModeIdx = await prompt.choose(
|
|
2957
|
+
"Shared-state storage mode:",
|
|
2958
|
+
[
|
|
2959
|
+
"Single JSON custom field (recommended)",
|
|
2960
|
+
"Multiple custom fields (advanced)",
|
|
2961
|
+
"Comments only (no custom fields)",
|
|
2962
|
+
],
|
|
2963
|
+
0,
|
|
2964
|
+
);
|
|
2965
|
+
if (storageModeIdx === 0) {
|
|
2966
|
+
env.JIRA_CUSTOM_FIELD_SHARED_STATE = await selectFieldId(
|
|
2967
|
+
"shared state JSON",
|
|
2968
|
+
process.env.JIRA_CUSTOM_FIELD_SHARED_STATE || "",
|
|
2969
|
+
);
|
|
2970
|
+
} else if (storageModeIdx === 1) {
|
|
2971
|
+
env.JIRA_CUSTOM_FIELD_OWNER_ID = await selectFieldId(
|
|
2972
|
+
"ownerId",
|
|
2973
|
+
process.env.JIRA_CUSTOM_FIELD_OWNER_ID || "",
|
|
2974
|
+
);
|
|
2975
|
+
env.JIRA_CUSTOM_FIELD_ATTEMPT_TOKEN = await selectFieldId(
|
|
2976
|
+
"attemptToken",
|
|
2977
|
+
process.env.JIRA_CUSTOM_FIELD_ATTEMPT_TOKEN || "",
|
|
2978
|
+
);
|
|
2979
|
+
env.JIRA_CUSTOM_FIELD_ATTEMPT_STARTED = await selectFieldId(
|
|
2980
|
+
"attemptStarted",
|
|
2981
|
+
process.env.JIRA_CUSTOM_FIELD_ATTEMPT_STARTED || "",
|
|
2982
|
+
);
|
|
2983
|
+
env.JIRA_CUSTOM_FIELD_HEARTBEAT = await selectFieldId(
|
|
2984
|
+
"heartbeat",
|
|
2985
|
+
process.env.JIRA_CUSTOM_FIELD_HEARTBEAT || "",
|
|
2986
|
+
);
|
|
2987
|
+
env.JIRA_CUSTOM_FIELD_RETRY_COUNT = await selectFieldId(
|
|
2988
|
+
"retryCount",
|
|
2989
|
+
process.env.JIRA_CUSTOM_FIELD_RETRY_COUNT || "",
|
|
2990
|
+
);
|
|
2991
|
+
env.JIRA_CUSTOM_FIELD_IGNORE_REASON = await selectFieldId(
|
|
2992
|
+
"ignoreReason",
|
|
2993
|
+
process.env.JIRA_CUSTOM_FIELD_IGNORE_REASON || "",
|
|
2994
|
+
);
|
|
2995
|
+
} else {
|
|
2996
|
+
info(
|
|
2997
|
+
"Shared-state will be stored in Jira comments and labels only.",
|
|
2998
|
+
);
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
configJson.kanban = {
|
|
3003
|
+
backend: selectedKanbanBackend,
|
|
3004
|
+
syncPolicy: selectedSyncPolicy,
|
|
3005
|
+
jira: {
|
|
3006
|
+
baseUrl: env.JIRA_BASE_URL,
|
|
3007
|
+
email: env.JIRA_EMAIL,
|
|
3008
|
+
projectKey: env.JIRA_PROJECT_KEY,
|
|
3009
|
+
issueType: env.JIRA_ISSUE_TYPE || "Task",
|
|
3010
|
+
},
|
|
3011
|
+
};
|
|
3012
|
+
success("Jira backend configured.");
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
if (vkNeeded) {
|
|
3016
|
+
if (isAdvancedSetup) {
|
|
3017
|
+
env.VK_BASE_URL = await prompt.ask(
|
|
3018
|
+
"VK API URL",
|
|
3019
|
+
process.env.VK_BASE_URL || "http://127.0.0.1:54089",
|
|
3020
|
+
);
|
|
3021
|
+
env.VK_RECOVERY_PORT = await prompt.ask(
|
|
3022
|
+
"VK port",
|
|
3023
|
+
process.env.VK_RECOVERY_PORT || "54089",
|
|
3024
|
+
);
|
|
3025
|
+
} else {
|
|
3026
|
+
env.VK_BASE_URL = process.env.VK_BASE_URL || "http://127.0.0.1:54089";
|
|
3027
|
+
env.VK_RECOVERY_PORT = process.env.VK_RECOVERY_PORT || "54089";
|
|
3028
|
+
}
|
|
3029
|
+
const spawnVk = await prompt.confirm(
|
|
3030
|
+
"Auto-spawn vibe-kanban if not running?",
|
|
3031
|
+
true,
|
|
3032
|
+
);
|
|
3033
|
+
if (!spawnVk) env.VK_NO_SPAWN = "true";
|
|
3034
|
+
} else {
|
|
3035
|
+
env.VK_NO_SPAWN = "true";
|
|
3036
|
+
info("VK runtime disabled (not selected as board or executor).");
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
// ── Codex CLI Config (config.toml) ─────────────────────
|
|
3040
|
+
heading("Codex CLI Config");
|
|
3041
|
+
console.log(chalk.dim(" ~/.codex/config.toml — agent-level config\n"));
|
|
3042
|
+
|
|
3043
|
+
const existingToml = readCodexConfig();
|
|
3044
|
+
const configTomlPath = getConfigPath();
|
|
3045
|
+
|
|
3046
|
+
if (!existingToml) {
|
|
3047
|
+
info(
|
|
3048
|
+
"No Codex CLI config found. Will create one with recommended settings.",
|
|
3049
|
+
);
|
|
3050
|
+
} else {
|
|
3051
|
+
info(`Found existing config: ${configTomlPath}`);
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
// Check vibe-kanban MCP
|
|
3055
|
+
if (vkNeeded) {
|
|
3056
|
+
if (existingToml && hasVibeKanbanMcp(existingToml)) {
|
|
3057
|
+
info("Vibe-Kanban MCP server already configured in config.toml.");
|
|
3058
|
+
const updateVk = await prompt.confirm(
|
|
3059
|
+
"Update VK env vars to match your setup values?",
|
|
3060
|
+
true,
|
|
3061
|
+
);
|
|
3062
|
+
if (!updateVk) {
|
|
3063
|
+
env._SKIP_VK_TOML = "1";
|
|
3064
|
+
}
|
|
3065
|
+
} else {
|
|
3066
|
+
info("Will add Vibe-Kanban MCP server to Codex config for agent use.");
|
|
3067
|
+
}
|
|
3068
|
+
} else {
|
|
3069
|
+
env._SKIP_VK_TOML = "1";
|
|
3070
|
+
info(
|
|
3071
|
+
"Skipping Vibe-Kanban MCP setup (VK not selected as board or executor).",
|
|
3072
|
+
);
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
// Check stream timeouts
|
|
3076
|
+
const timeouts = auditStreamTimeouts(existingToml);
|
|
3077
|
+
const lowTimeouts = timeouts.filter((t) => t.needsUpdate);
|
|
3078
|
+
if (lowTimeouts.length > 0) {
|
|
3079
|
+
for (const t of lowTimeouts) {
|
|
3080
|
+
const label =
|
|
3081
|
+
t.currentValue === null
|
|
3082
|
+
? "not set"
|
|
3083
|
+
: `${(t.currentValue / 1000).toFixed(0)}s`;
|
|
3084
|
+
warn(
|
|
3085
|
+
`[${t.provider}] stream_idle_timeout_ms is ${label} — too low for complex reasoning.`,
|
|
3086
|
+
);
|
|
3087
|
+
}
|
|
3088
|
+
const fixTimeouts = await prompt.confirm(
|
|
3089
|
+
"Set stream timeouts to 60 minutes (recommended for agentic workloads)?",
|
|
3090
|
+
true,
|
|
3091
|
+
);
|
|
3092
|
+
if (!fixTimeouts) {
|
|
3093
|
+
env._SKIP_TIMEOUT_FIX = "1";
|
|
3094
|
+
}
|
|
3095
|
+
} else if (timeouts.length > 0) {
|
|
3096
|
+
success("Stream timeouts look good across all providers.");
|
|
3097
|
+
}
|
|
3098
|
+
|
|
3099
|
+
// ── Orchestrator ──────────────────────────────────────
|
|
3100
|
+
heading("Orchestrator Script");
|
|
3101
|
+
console.log(
|
|
3102
|
+
chalk.dim(
|
|
3103
|
+
" The orchestrator manages task execution and agent spawning.\n",
|
|
3104
|
+
),
|
|
3105
|
+
);
|
|
3106
|
+
|
|
3107
|
+
// Check for default scripts in repo first, then package fallback.
|
|
3108
|
+
const { orchestratorDefaults, selectedDefault, orchestratorScriptEnvValue } =
|
|
3109
|
+
resolveSetupOrchestratorDefaults({
|
|
3110
|
+
platform: process.platform,
|
|
3111
|
+
repoRoot,
|
|
3112
|
+
configDir,
|
|
3113
|
+
});
|
|
3114
|
+
const hasDefaultScripts = orchestratorDefaults.variants.length > 0;
|
|
3115
|
+
|
|
3116
|
+
if (hasDefaultScripts) {
|
|
3117
|
+
info(`Found default orchestrator scripts in openfleet:`);
|
|
3118
|
+
for (const variant of orchestratorDefaults.variants) {
|
|
3119
|
+
const preferredTag =
|
|
3120
|
+
variant.ext === orchestratorDefaults.preferredExt ? " (preferred)" : "";
|
|
3121
|
+
info(
|
|
3122
|
+
` - ve-orchestrator.${variant.ext} + ve-kanban.${variant.ext}${preferredTag}`,
|
|
3123
|
+
);
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
const useDefault = isAdvancedSetup
|
|
3127
|
+
? await prompt.confirm(
|
|
3128
|
+
`Use the default ${basename(selectedDefault.orchestratorPath)} script?`,
|
|
3129
|
+
true,
|
|
3130
|
+
)
|
|
3131
|
+
: true;
|
|
3132
|
+
|
|
3133
|
+
if (useDefault) {
|
|
3134
|
+
env.ORCHESTRATOR_SCRIPT = orchestratorScriptEnvValue;
|
|
3135
|
+
success(`Using default ${basename(selectedDefault.orchestratorPath)}`);
|
|
3136
|
+
} else {
|
|
3137
|
+
const customPath = await prompt.ask(
|
|
3138
|
+
"Path to your custom orchestrator script (or leave blank for Vibe-Kanban direct mode)",
|
|
3139
|
+
"",
|
|
3140
|
+
);
|
|
3141
|
+
if (customPath) {
|
|
3142
|
+
env.ORCHESTRATOR_SCRIPT = customPath;
|
|
3143
|
+
} else {
|
|
3144
|
+
info(
|
|
3145
|
+
"No orchestrator script configured. openfleet will manage tasks directly via Vibe-Kanban.",
|
|
3146
|
+
);
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
} else {
|
|
3150
|
+
const hasOrcScript = isAdvancedSetup
|
|
3151
|
+
? await prompt.confirm(
|
|
3152
|
+
"Do you have an existing orchestrator script?",
|
|
3153
|
+
false,
|
|
3154
|
+
)
|
|
3155
|
+
: false;
|
|
3156
|
+
if (hasOrcScript) {
|
|
3157
|
+
env.ORCHESTRATOR_SCRIPT = await prompt.ask(
|
|
3158
|
+
"Path to orchestrator script",
|
|
3159
|
+
"",
|
|
3160
|
+
);
|
|
3161
|
+
} else {
|
|
3162
|
+
info(
|
|
3163
|
+
"No orchestrator script configured. openfleet will manage tasks directly via Vibe-Kanban.",
|
|
3164
|
+
);
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
env.MAX_PARALLEL = await prompt.ask(
|
|
3169
|
+
"Max parallel agent slots",
|
|
3170
|
+
process.env.MAX_PARALLEL || "6",
|
|
3171
|
+
);
|
|
3172
|
+
|
|
3173
|
+
// ── Agent Templates ───────────────────────────────────
|
|
3174
|
+
heading("Agent Templates");
|
|
3175
|
+
console.log(
|
|
3176
|
+
chalk.dim(
|
|
3177
|
+
" openfleet prompt templates are scaffolded to .openfleet/agents and loaded automatically.\n",
|
|
3178
|
+
),
|
|
3179
|
+
);
|
|
3180
|
+
const generateAgents = isAdvancedSetup
|
|
3181
|
+
? await prompt.confirm(
|
|
3182
|
+
"Scaffold .openfleet/agents prompt files?",
|
|
3183
|
+
true,
|
|
3184
|
+
)
|
|
3185
|
+
: true;
|
|
3186
|
+
|
|
3187
|
+
if (generateAgents) {
|
|
3188
|
+
const promptsResult = ensureAgentPromptWorkspace(repoRoot);
|
|
3189
|
+
const addedGitIgnore = ensureRepoGitIgnoreEntry(
|
|
3190
|
+
repoRoot,
|
|
3191
|
+
"/.openfleet/",
|
|
3192
|
+
);
|
|
3193
|
+
configJson.agentPrompts = getDefaultPromptOverrides();
|
|
3194
|
+
|
|
3195
|
+
if (addedGitIgnore) {
|
|
3196
|
+
success("Updated .gitignore with '/.openfleet/'");
|
|
3197
|
+
}
|
|
3198
|
+
if (promptsResult.written.length > 0) {
|
|
3199
|
+
success(
|
|
3200
|
+
`Created ${promptsResult.written.length} prompt template file(s) in ${relative(repoRoot, promptsResult.workspaceDir)}`,
|
|
3201
|
+
);
|
|
3202
|
+
} else {
|
|
3203
|
+
info("Prompt templates already exist — keeping existing files");
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
// Optional AGENTS.md seed
|
|
3207
|
+
const agentsMdPath = resolve(repoRoot, "AGENTS.md");
|
|
3208
|
+
if (!existsSync(agentsMdPath)) {
|
|
3209
|
+
const createAgentsGuide = await prompt.confirm(
|
|
3210
|
+
"Create AGENTS.md guide file as well?",
|
|
3211
|
+
true,
|
|
3212
|
+
);
|
|
3213
|
+
if (createAgentsGuide) {
|
|
3214
|
+
writeFileSync(
|
|
3215
|
+
agentsMdPath,
|
|
3216
|
+
generateAgentsMd(env.PROJECT_NAME, env.GITHUB_REPO),
|
|
3217
|
+
"utf8",
|
|
3218
|
+
);
|
|
3219
|
+
success(`Created ${relative(repoRoot, agentsMdPath)}`);
|
|
3220
|
+
}
|
|
3221
|
+
} else {
|
|
3222
|
+
info("AGENTS.md already exists — leaving unchanged");
|
|
3223
|
+
}
|
|
3224
|
+
} else {
|
|
3225
|
+
configJson.agentPrompts = getDefaultPromptOverrides();
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
// ── Agent Hooks ───────────────────────────────────────
|
|
3229
|
+
heading("Agent Hooks");
|
|
3230
|
+
console.log(
|
|
3231
|
+
chalk.dim(
|
|
3232
|
+
" Configure shared hook policies for Codex, Claude Code, and Copilot CLI.\n",
|
|
3233
|
+
),
|
|
3234
|
+
);
|
|
3235
|
+
|
|
3236
|
+
const scaffoldHooks = isAdvancedSetup
|
|
3237
|
+
? await prompt.confirm(
|
|
3238
|
+
"Scaffold hook configs for Codex/Claude/Copilot?",
|
|
3239
|
+
true,
|
|
3240
|
+
)
|
|
3241
|
+
: true;
|
|
3242
|
+
|
|
3243
|
+
if (scaffoldHooks) {
|
|
3244
|
+
const profileMap = ["strict", "balanced", "lightweight", "none"];
|
|
3245
|
+
let profile = "balanced";
|
|
3246
|
+
let targets = ["codex", "claude", "copilot"];
|
|
3247
|
+
let prePushRaw = process.env.CODEX_MONITOR_HOOK_PREPUSH || "";
|
|
3248
|
+
let preCommitRaw = process.env.CODEX_MONITOR_HOOK_PRECOMMIT || "";
|
|
3249
|
+
let taskCompleteRaw = process.env.CODEX_MONITOR_HOOK_TASK_COMPLETE || "";
|
|
3250
|
+
let overwriteHooks = false;
|
|
3251
|
+
|
|
3252
|
+
if (isAdvancedSetup) {
|
|
3253
|
+
const profileIdx = await prompt.choose(
|
|
3254
|
+
"Select hook policy:",
|
|
3255
|
+
[
|
|
3256
|
+
"Strict — pre-commit + pre-push + task validation",
|
|
3257
|
+
"Balanced — pre-push + task validation",
|
|
3258
|
+
"Lightweight — session/audit hooks only (no validation gates)",
|
|
3259
|
+
"None — disable openfleet built-in validation hooks",
|
|
3260
|
+
],
|
|
3261
|
+
0,
|
|
3262
|
+
);
|
|
3263
|
+
profile = profileMap[profileIdx] || "strict";
|
|
3264
|
+
|
|
3265
|
+
const targetIdx = await prompt.choose(
|
|
3266
|
+
"Hook files to scaffold:",
|
|
3267
|
+
[
|
|
3268
|
+
"All agents (Codex + Claude + Copilot)",
|
|
3269
|
+
"Codex + Claude",
|
|
3270
|
+
"Codex + Copilot",
|
|
3271
|
+
"Codex only",
|
|
3272
|
+
"Custom target list",
|
|
3273
|
+
],
|
|
3274
|
+
0,
|
|
3275
|
+
);
|
|
3276
|
+
|
|
3277
|
+
if (targetIdx === 0) targets = ["codex", "claude", "copilot"];
|
|
3278
|
+
else if (targetIdx === 1) targets = ["codex", "claude"];
|
|
3279
|
+
else if (targetIdx === 2) targets = ["codex", "copilot"];
|
|
3280
|
+
else if (targetIdx === 3) targets = ["codex"];
|
|
3281
|
+
else {
|
|
3282
|
+
const customTargets = await prompt.ask(
|
|
3283
|
+
"Custom targets (comma-separated: codex,claude,copilot)",
|
|
3284
|
+
"codex,claude,copilot",
|
|
3285
|
+
);
|
|
3286
|
+
targets = normalizeHookTargets(customTargets);
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
console.log(
|
|
3290
|
+
chalk.dim(
|
|
3291
|
+
" Optional command overrides: use ';;' between commands, or 'none' to disable a hook event.\n",
|
|
3292
|
+
),
|
|
3293
|
+
);
|
|
3294
|
+
|
|
3295
|
+
prePushRaw = await prompt.ask(
|
|
3296
|
+
"Pre-push command override",
|
|
3297
|
+
process.env.CODEX_MONITOR_HOOK_PREPUSH || "",
|
|
3298
|
+
);
|
|
3299
|
+
preCommitRaw = await prompt.ask(
|
|
3300
|
+
"Pre-commit command override",
|
|
3301
|
+
process.env.CODEX_MONITOR_HOOK_PRECOMMIT || "",
|
|
3302
|
+
);
|
|
3303
|
+
taskCompleteRaw = await prompt.ask(
|
|
3304
|
+
"Task-complete command override",
|
|
3305
|
+
process.env.CODEX_MONITOR_HOOK_TASK_COMPLETE || "",
|
|
3306
|
+
);
|
|
3307
|
+
|
|
3308
|
+
overwriteHooks = await prompt.confirm(
|
|
3309
|
+
"Overwrite existing generated hook files when present?",
|
|
3310
|
+
false,
|
|
3311
|
+
);
|
|
3312
|
+
} else {
|
|
3313
|
+
info(
|
|
3314
|
+
"Using recommended hook defaults: balanced policy for codex, claude, and copilot.",
|
|
3315
|
+
);
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
const hookResult = scaffoldAgentHookFiles(repoRoot, {
|
|
3319
|
+
enabled: true,
|
|
3320
|
+
profile,
|
|
3321
|
+
targets,
|
|
3322
|
+
overwriteExisting: overwriteHooks,
|
|
3323
|
+
commands: {
|
|
3324
|
+
PrePush: parseHookCommandInput(prePushRaw),
|
|
3325
|
+
PreCommit: parseHookCommandInput(preCommitRaw),
|
|
3326
|
+
TaskComplete: parseHookCommandInput(taskCompleteRaw),
|
|
3327
|
+
},
|
|
3328
|
+
});
|
|
3329
|
+
|
|
3330
|
+
printHookScaffoldSummary(hookResult);
|
|
3331
|
+
Object.assign(env, hookResult.env);
|
|
3332
|
+
configJson.hookProfiles = {
|
|
3333
|
+
enabled: true,
|
|
3334
|
+
profile,
|
|
3335
|
+
targets,
|
|
3336
|
+
overwriteExisting: overwriteHooks,
|
|
3337
|
+
};
|
|
3338
|
+
} else {
|
|
3339
|
+
const hookResult = scaffoldAgentHookFiles(repoRoot, { enabled: false });
|
|
3340
|
+
Object.assign(env, hookResult.env);
|
|
3341
|
+
configJson.hookProfiles = {
|
|
3342
|
+
enabled: false,
|
|
3343
|
+
};
|
|
3344
|
+
info("Hook scaffolding skipped by user selection.");
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
// ── VK Auto-Wiring ────────────────────────────────────
|
|
3348
|
+
if (vkNeeded) {
|
|
3349
|
+
heading("Vibe-Kanban Auto-Configuration");
|
|
3350
|
+
const autoWireVk = isAdvancedSetup
|
|
3351
|
+
? await prompt.confirm(
|
|
3352
|
+
"Auto-configure Vibe-Kanban project, repos, and executor profiles?",
|
|
3353
|
+
true,
|
|
3354
|
+
)
|
|
3355
|
+
: true;
|
|
3356
|
+
|
|
3357
|
+
if (autoWireVk) {
|
|
3358
|
+
const vkConfig = {
|
|
3359
|
+
projectName: env.PROJECT_NAME,
|
|
3360
|
+
repoRoot,
|
|
3361
|
+
monitorDir: __dirname,
|
|
3362
|
+
};
|
|
3363
|
+
|
|
3364
|
+
// Generate VK scripts
|
|
3365
|
+
const setupScript = generateVkSetupScript(vkConfig);
|
|
3366
|
+
const cleanupScript = generateVkCleanupScript(vkConfig);
|
|
3367
|
+
|
|
3368
|
+
// Get current PATH for VK executor profiles
|
|
3369
|
+
const currentPath = process.env.PATH || "";
|
|
3370
|
+
|
|
3371
|
+
// Write to config for VK API auto-wiring
|
|
3372
|
+
configJson.vkAutoConfig = {
|
|
3373
|
+
setupScript,
|
|
3374
|
+
cleanupScript,
|
|
3375
|
+
executorProfiles: configJson.executors.map((e) => ({
|
|
3376
|
+
executor: e.executor,
|
|
3377
|
+
variant: e.variant,
|
|
3378
|
+
environmentVariables: {
|
|
3379
|
+
PATH: currentPath,
|
|
3380
|
+
// Ensure GitHub token is available in workspace
|
|
3381
|
+
GH_TOKEN: "${GH_TOKEN}",
|
|
3382
|
+
GITHUB_TOKEN: "${GITHUB_TOKEN}",
|
|
3383
|
+
},
|
|
3384
|
+
})),
|
|
3385
|
+
};
|
|
3386
|
+
|
|
3387
|
+
info("VK configuration will be applied on first launch.");
|
|
3388
|
+
info("Setup and cleanup scripts generated for your workspace.");
|
|
3389
|
+
info(
|
|
3390
|
+
`PATH environment variable configured for ${configJson.executors.length} executor profile(s)`,
|
|
3391
|
+
);
|
|
3392
|
+
}
|
|
3393
|
+
} else {
|
|
3394
|
+
info("Skipping VK auto-configuration (VK not selected).");
|
|
3395
|
+
delete configJson.vkAutoConfig;
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
// ── Step 8: Optional Channels ─────────────────────────
|
|
3399
|
+
heading("Step 8 of 9 — Optional Channels (WhatsApp & Container)");
|
|
3400
|
+
|
|
3401
|
+
console.log(
|
|
3402
|
+
chalk.dim(
|
|
3403
|
+
" These are optional features. Skip them if you only want Telegram.",
|
|
3404
|
+
),
|
|
3405
|
+
);
|
|
3406
|
+
|
|
3407
|
+
// WhatsApp
|
|
3408
|
+
const enableWhatsApp = await prompt.confirm(
|
|
3409
|
+
"Enable WhatsApp channel?",
|
|
3410
|
+
false,
|
|
3411
|
+
);
|
|
3412
|
+
if (enableWhatsApp) {
|
|
3413
|
+
env.WHATSAPP_ENABLED = "true";
|
|
3414
|
+
env.WHATSAPP_CHAT_ID = await prompt.ask(
|
|
3415
|
+
"WhatsApp Chat/Group ID (JID)",
|
|
3416
|
+
process.env.WHATSAPP_CHAT_ID || "",
|
|
3417
|
+
);
|
|
3418
|
+
env.WHATSAPP_ASSISTANT_NAME = isAdvancedSetup
|
|
3419
|
+
? await prompt.ask(
|
|
3420
|
+
"WhatsApp assistant display name",
|
|
3421
|
+
env.PROJECT_NAME || "Codex Monitor",
|
|
3422
|
+
)
|
|
3423
|
+
: env.PROJECT_NAME || "Codex Monitor";
|
|
3424
|
+
info(
|
|
3425
|
+
"Run `openfleet --whatsapp-auth` after setup to authenticate with WhatsApp.",
|
|
3426
|
+
);
|
|
3427
|
+
} else {
|
|
3428
|
+
env.WHATSAPP_ENABLED = "false";
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
// Container isolation
|
|
3432
|
+
const enableContainer = await prompt.confirm(
|
|
3433
|
+
"Enable container isolation for agent execution?",
|
|
3434
|
+
false,
|
|
3435
|
+
);
|
|
3436
|
+
if (enableContainer) {
|
|
3437
|
+
env.CONTAINER_ENABLED = "true";
|
|
3438
|
+
if (isAdvancedSetup) {
|
|
3439
|
+
const runtimeIdx = await prompt.choose(
|
|
3440
|
+
"Container runtime",
|
|
3441
|
+
["docker", "podman", "auto-detect"],
|
|
3442
|
+
2,
|
|
3443
|
+
);
|
|
3444
|
+
env.CONTAINER_RUNTIME = ["docker", "podman", "auto"][runtimeIdx];
|
|
3445
|
+
env.CONTAINER_IMAGE = await prompt.ask(
|
|
3446
|
+
"Container image",
|
|
3447
|
+
process.env.CONTAINER_IMAGE || "node:22-slim",
|
|
3448
|
+
);
|
|
3449
|
+
env.CONTAINER_MEMORY_LIMIT = await prompt.ask(
|
|
3450
|
+
"Memory limit (e.g. 2g)",
|
|
3451
|
+
process.env.CONTAINER_MEMORY_LIMIT || "4g",
|
|
3452
|
+
);
|
|
3453
|
+
} else {
|
|
3454
|
+
env.CONTAINER_RUNTIME = process.env.CONTAINER_RUNTIME || "auto";
|
|
3455
|
+
env.CONTAINER_IMAGE = process.env.CONTAINER_IMAGE || "node:22-slim";
|
|
3456
|
+
}
|
|
3457
|
+
} else {
|
|
3458
|
+
env.CONTAINER_ENABLED = "false";
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
// ── Step 9: Startup Service ────────────────────────────
|
|
3462
|
+
heading("Step 9 of 9 — Startup Service");
|
|
3463
|
+
|
|
3464
|
+
const { getStartupStatus, getStartupMethodName } =
|
|
3465
|
+
await import("./startup-service.mjs");
|
|
3466
|
+
const currentStartup = getStartupStatus();
|
|
3467
|
+
const methodName = getStartupMethodName();
|
|
3468
|
+
|
|
3469
|
+
if (currentStartup.installed) {
|
|
3470
|
+
info(`Startup service already installed via ${currentStartup.method}.`);
|
|
3471
|
+
const reinstall = await prompt.confirm(
|
|
3472
|
+
"Re-install startup service?",
|
|
3473
|
+
false,
|
|
3474
|
+
);
|
|
3475
|
+
env._STARTUP_SERVICE = reinstall ? "1" : "skip";
|
|
3476
|
+
} else {
|
|
3477
|
+
console.log(
|
|
3478
|
+
chalk.dim(
|
|
3479
|
+
` Auto-start openfleet when you log in using ${methodName}.`,
|
|
3480
|
+
),
|
|
3481
|
+
);
|
|
3482
|
+
console.log(
|
|
3483
|
+
chalk.dim(
|
|
3484
|
+
" It will run in daemon mode (background) with auto-restart on failure.",
|
|
3485
|
+
),
|
|
3486
|
+
);
|
|
3487
|
+
const enableStartup = await prompt.confirm(
|
|
3488
|
+
"Enable auto-start on login?",
|
|
3489
|
+
true,
|
|
3490
|
+
);
|
|
3491
|
+
env._STARTUP_SERVICE = enableStartup ? "1" : "0";
|
|
3492
|
+
}
|
|
3493
|
+
} finally {
|
|
3494
|
+
prompt.close();
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
// ── Write Files ─────────────────────────────────────────
|
|
3498
|
+
normalizeSetupConfiguration({ env, configJson, repoRoot, slug, configDir });
|
|
3499
|
+
await writeConfigFiles({ env, configJson, repoRoot, configDir });
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
// ── Non-Interactive Mode ─────────────────────────────────────────────────────
|
|
3503
|
+
|
|
3504
|
+
async function runNonInteractive({
|
|
3505
|
+
env,
|
|
3506
|
+
configJson,
|
|
3507
|
+
repoRoot,
|
|
3508
|
+
slug,
|
|
3509
|
+
projectName,
|
|
3510
|
+
configDir,
|
|
3511
|
+
}) {
|
|
3512
|
+
env.PROJECT_NAME = process.env.PROJECT_NAME || projectName;
|
|
3513
|
+
env.REPO_ROOT = process.env.REPO_ROOT || repoRoot;
|
|
3514
|
+
env.GITHUB_REPO = process.env.GITHUB_REPO || slug || "";
|
|
3515
|
+
env.TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || "";
|
|
3516
|
+
env.TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID || "";
|
|
3517
|
+
env.KANBAN_BACKEND = process.env.KANBAN_BACKEND || "internal";
|
|
3518
|
+
env.KANBAN_SYNC_POLICY =
|
|
3519
|
+
process.env.KANBAN_SYNC_POLICY || "internal-primary";
|
|
3520
|
+
env.EXECUTOR_MODE = process.env.EXECUTOR_MODE || "internal";
|
|
3521
|
+
env.PROJECT_REQUIREMENTS_PROFILE =
|
|
3522
|
+
process.env.PROJECT_REQUIREMENTS_PROFILE || "feature";
|
|
3523
|
+
env.PROJECT_REQUIREMENTS_NOTES = process.env.PROJECT_REQUIREMENTS_NOTES || "";
|
|
3524
|
+
env.INTERNAL_EXECUTOR_REPLENISH_ENABLED =
|
|
3525
|
+
process.env.INTERNAL_EXECUTOR_REPLENISH_ENABLED || "false";
|
|
3526
|
+
env.INTERNAL_EXECUTOR_REPLENISH_MIN_NEW_TASKS =
|
|
3527
|
+
process.env.INTERNAL_EXECUTOR_REPLENISH_MIN_NEW_TASKS || "1";
|
|
3528
|
+
env.INTERNAL_EXECUTOR_REPLENISH_MAX_NEW_TASKS =
|
|
3529
|
+
process.env.INTERNAL_EXECUTOR_REPLENISH_MAX_NEW_TASKS || "2";
|
|
3530
|
+
env.VK_BASE_URL = process.env.VK_BASE_URL || "http://127.0.0.1:54089";
|
|
3531
|
+
env.VK_RECOVERY_PORT = process.env.VK_RECOVERY_PORT || "54089";
|
|
3532
|
+
env.GITHUB_REPO_OWNER =
|
|
3533
|
+
process.env.GITHUB_REPO_OWNER || (slug ? String(slug).split("/")[0] : "");
|
|
3534
|
+
env.GITHUB_REPO_NAME =
|
|
3535
|
+
process.env.GITHUB_REPO_NAME || (slug ? String(slug).split("/")[1] : "");
|
|
3536
|
+
env.GITHUB_REPOSITORY =
|
|
3537
|
+
process.env.GITHUB_REPOSITORY ||
|
|
3538
|
+
(env.GITHUB_REPO_OWNER && env.GITHUB_REPO_NAME
|
|
3539
|
+
? `${env.GITHUB_REPO_OWNER}/${env.GITHUB_REPO_NAME}`
|
|
3540
|
+
: "");
|
|
3541
|
+
if (!env.GITHUB_REPO && env.GITHUB_REPOSITORY) {
|
|
3542
|
+
env.GITHUB_REPO = env.GITHUB_REPOSITORY;
|
|
3543
|
+
}
|
|
3544
|
+
env.OPENAI_API_KEY = process.env.OPENAI_API_KEY || "";
|
|
3545
|
+
env.CODEX_MODEL_PROFILE = process.env.CODEX_MODEL_PROFILE || "xl";
|
|
3546
|
+
env.CODEX_MODEL_PROFILE_SUBAGENT =
|
|
3547
|
+
process.env.CODEX_MODEL_PROFILE_SUBAGENT ||
|
|
3548
|
+
process.env.CODEX_SUBAGENT_PROFILE ||
|
|
3549
|
+
"m";
|
|
3550
|
+
env.CODEX_MODEL_PROFILE_XL_MODEL =
|
|
3551
|
+
process.env.CODEX_MODEL_PROFILE_XL_MODEL || "gpt-5.3-codex";
|
|
3552
|
+
env.CODEX_MODEL_PROFILE_M_MODEL =
|
|
3553
|
+
process.env.CODEX_MODEL_PROFILE_M_MODEL || "gpt-5.1-codex-mini";
|
|
3554
|
+
env.CODEX_MODEL_PROFILE_XL_PROVIDER =
|
|
3555
|
+
process.env.CODEX_MODEL_PROFILE_XL_PROVIDER || "openai";
|
|
3556
|
+
env.CODEX_MODEL_PROFILE_M_PROVIDER =
|
|
3557
|
+
process.env.CODEX_MODEL_PROFILE_M_PROVIDER || "openai";
|
|
3558
|
+
env.CODEX_SUBAGENT_MODEL =
|
|
3559
|
+
process.env.CODEX_SUBAGENT_MODEL || env.CODEX_MODEL_PROFILE_M_MODEL;
|
|
3560
|
+
env.CODEX_AGENT_MAX_THREADS =
|
|
3561
|
+
process.env.CODEX_AGENT_MAX_THREADS ||
|
|
3562
|
+
process.env.CODEX_AGENTS_MAX_THREADS ||
|
|
3563
|
+
"12";
|
|
3564
|
+
env.CODEX_SANDBOX = process.env.CODEX_SANDBOX || "workspace-write";
|
|
3565
|
+
env.MAX_PARALLEL = process.env.MAX_PARALLEL || "6";
|
|
3566
|
+
if (!process.env.ORCHESTRATOR_SCRIPT) {
|
|
3567
|
+
const { orchestratorScriptEnvValue } = resolveSetupOrchestratorDefaults({
|
|
3568
|
+
platform: process.platform,
|
|
3569
|
+
repoRoot,
|
|
3570
|
+
configDir,
|
|
3571
|
+
});
|
|
3572
|
+
if (orchestratorScriptEnvValue) {
|
|
3573
|
+
env.ORCHESTRATOR_SCRIPT = orchestratorScriptEnvValue;
|
|
3574
|
+
}
|
|
3575
|
+
} else {
|
|
3576
|
+
env.ORCHESTRATOR_SCRIPT = process.env.ORCHESTRATOR_SCRIPT;
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
// Optional channels
|
|
3580
|
+
env.WHATSAPP_ENABLED = process.env.WHATSAPP_ENABLED || "false";
|
|
3581
|
+
env.WHATSAPP_CHAT_ID = process.env.WHATSAPP_CHAT_ID || "";
|
|
3582
|
+
env.CONTAINER_ENABLED = process.env.CONTAINER_ENABLED || "false";
|
|
3583
|
+
env.CONTAINER_RUNTIME = process.env.CONTAINER_RUNTIME || "auto";
|
|
3584
|
+
|
|
3585
|
+
// Copilot cloud: disabled by default — set to 0 to allow @copilot PR comments
|
|
3586
|
+
env.COPILOT_CLOUD_DISABLED = process.env.COPILOT_CLOUD_DISABLED || "true";
|
|
3587
|
+
env.COPILOT_NO_EXPERIMENTAL =
|
|
3588
|
+
process.env.COPILOT_NO_EXPERIMENTAL || "false";
|
|
3589
|
+
env.COPILOT_NO_ALLOW_ALL = process.env.COPILOT_NO_ALLOW_ALL || "false";
|
|
3590
|
+
env.COPILOT_ENABLE_ASK_USER =
|
|
3591
|
+
process.env.COPILOT_ENABLE_ASK_USER || "false";
|
|
3592
|
+
env.COPILOT_ENABLE_ALL_GITHUB_MCP_TOOLS =
|
|
3593
|
+
process.env.COPILOT_ENABLE_ALL_GITHUB_MCP_TOOLS || "true";
|
|
3594
|
+
env.COPILOT_AGENT_MAX_REQUESTS =
|
|
3595
|
+
process.env.COPILOT_AGENT_MAX_REQUESTS || "500";
|
|
3596
|
+
|
|
3597
|
+
// Parse EXECUTORS env if set, else use default preset
|
|
3598
|
+
if (process.env.EXECUTORS) {
|
|
3599
|
+
const entries = process.env.EXECUTORS.split(",").map((e) => e.trim());
|
|
3600
|
+
const roles = ["primary", "backup", "tertiary"];
|
|
3601
|
+
for (let i = 0; i < entries.length; i++) {
|
|
3602
|
+
const parts = entries[i].split(":");
|
|
3603
|
+
if (parts.length >= 2) {
|
|
3604
|
+
configJson.executors.push({
|
|
3605
|
+
name: `${parts[0].toLowerCase()}-${parts[1].toLowerCase()}`,
|
|
3606
|
+
executor: parts[0].toUpperCase(),
|
|
3607
|
+
variant: parts[1],
|
|
3608
|
+
weight: parts[2]
|
|
3609
|
+
? Number(parts[2])
|
|
3610
|
+
: Math.floor(100 / entries.length),
|
|
3611
|
+
role: roles[i] || `executor-${i + 1}`,
|
|
3612
|
+
enabled: true,
|
|
3613
|
+
});
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
if (!configJson.executors.length) {
|
|
3618
|
+
configJson.executors = EXECUTOR_PRESETS["codex-only"];
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
configJson.projectName = env.PROJECT_NAME;
|
|
3622
|
+
configJson.kanban = {
|
|
3623
|
+
backend: env.KANBAN_BACKEND || "internal",
|
|
3624
|
+
syncPolicy: env.KANBAN_SYNC_POLICY || "internal-primary",
|
|
3625
|
+
};
|
|
3626
|
+
configJson.projectRequirements = {
|
|
3627
|
+
profile: env.PROJECT_REQUIREMENTS_PROFILE || "feature",
|
|
3628
|
+
notes: env.PROJECT_REQUIREMENTS_NOTES || "",
|
|
3629
|
+
};
|
|
3630
|
+
configJson.internalExecutor = {
|
|
3631
|
+
...(configJson.internalExecutor || {}),
|
|
3632
|
+
mode: env.EXECUTOR_MODE || "internal",
|
|
3633
|
+
backlogReplenishment: {
|
|
3634
|
+
enabled:
|
|
3635
|
+
String(env.INTERNAL_EXECUTOR_REPLENISH_ENABLED || "false").toLowerCase() ===
|
|
3636
|
+
"true",
|
|
3637
|
+
minNewTasks: toPositiveInt(env.INTERNAL_EXECUTOR_REPLENISH_MIN_NEW_TASKS, 1),
|
|
3638
|
+
maxNewTasks: toPositiveInt(env.INTERNAL_EXECUTOR_REPLENISH_MAX_NEW_TASKS, 2),
|
|
3639
|
+
requirePriority: true,
|
|
3640
|
+
},
|
|
3641
|
+
projectRequirements: {
|
|
3642
|
+
profile: env.PROJECT_REQUIREMENTS_PROFILE || "feature",
|
|
3643
|
+
notes: env.PROJECT_REQUIREMENTS_NOTES || "",
|
|
3644
|
+
},
|
|
3645
|
+
};
|
|
3646
|
+
configJson.failover = {
|
|
3647
|
+
strategy: process.env.FAILOVER_STRATEGY || "next-in-line",
|
|
3648
|
+
maxRetries: Number(process.env.FAILOVER_MAX_RETRIES || "3"),
|
|
3649
|
+
cooldownMinutes: Number(process.env.FAILOVER_COOLDOWN_MIN || "5"),
|
|
3650
|
+
disableOnConsecutiveFailures: Number(
|
|
3651
|
+
process.env.FAILOVER_DISABLE_AFTER || "3",
|
|
3652
|
+
),
|
|
3653
|
+
};
|
|
3654
|
+
configJson.distribution = process.env.EXECUTOR_DISTRIBUTION || "weighted";
|
|
3655
|
+
configJson.repositories = [
|
|
3656
|
+
{
|
|
3657
|
+
name: basename(repoRoot),
|
|
3658
|
+
slug: env.GITHUB_REPO,
|
|
3659
|
+
primary: true,
|
|
3660
|
+
},
|
|
3661
|
+
];
|
|
3662
|
+
configJson.agentPrompts = getDefaultPromptOverrides();
|
|
3663
|
+
ensureAgentPromptWorkspace(repoRoot);
|
|
3664
|
+
ensureRepoGitIgnoreEntry(repoRoot, "/.openfleet/");
|
|
3665
|
+
|
|
3666
|
+
const hookOptions = buildHookScaffoldOptionsFromEnv(process.env);
|
|
3667
|
+
const hookResult = scaffoldAgentHookFiles(repoRoot, hookOptions);
|
|
3668
|
+
Object.assign(env, hookResult.env);
|
|
3669
|
+
configJson.hookProfiles = {
|
|
3670
|
+
enabled: hookResult.enabled,
|
|
3671
|
+
profile: hookResult.profile,
|
|
3672
|
+
targets: hookResult.targets,
|
|
3673
|
+
overwriteExisting: Boolean(hookOptions.overwriteExisting),
|
|
3674
|
+
};
|
|
3675
|
+
printHookScaffoldSummary(hookResult);
|
|
3676
|
+
|
|
3677
|
+
// Startup service: respect STARTUP_SERVICE env in non-interactive mode
|
|
3678
|
+
if (parseBooleanEnvValue(process.env.STARTUP_SERVICE, false)) {
|
|
3679
|
+
env._STARTUP_SERVICE = "1";
|
|
3680
|
+
} else if (
|
|
3681
|
+
process.env.STARTUP_SERVICE !== undefined &&
|
|
3682
|
+
!parseBooleanEnvValue(process.env.STARTUP_SERVICE, true)
|
|
3683
|
+
) {
|
|
3684
|
+
env._STARTUP_SERVICE = "0";
|
|
3685
|
+
}
|
|
3686
|
+
// else: don't set — writeConfigFiles will skip silently
|
|
3687
|
+
|
|
3688
|
+
if (
|
|
3689
|
+
(env.KANBAN_BACKEND || "").toLowerCase() !== "vk" &&
|
|
3690
|
+
!["vk", "hybrid"].includes((env.EXECUTOR_MODE || "").toLowerCase())
|
|
3691
|
+
) {
|
|
3692
|
+
env.VK_NO_SPAWN = "true";
|
|
3693
|
+
delete configJson.vkAutoConfig;
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
normalizeSetupConfiguration({ env, configJson, repoRoot, slug, configDir });
|
|
3697
|
+
await writeConfigFiles({ env, configJson, repoRoot, configDir });
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
// ── File Writing ─────────────────────────────────────────────────────────────
|
|
3701
|
+
|
|
3702
|
+
async function writeConfigFiles({ env, configJson, repoRoot, configDir }) {
|
|
3703
|
+
heading("Writing Configuration");
|
|
3704
|
+
const targetDir = resolve(configDir || __dirname);
|
|
3705
|
+
mkdirSync(targetDir, { recursive: true });
|
|
3706
|
+
ensureAgentPromptWorkspace(repoRoot);
|
|
3707
|
+
ensureRepoGitIgnoreEntry(repoRoot, "/.openfleet/");
|
|
3708
|
+
if (
|
|
3709
|
+
!configJson.agentPrompts ||
|
|
3710
|
+
Object.keys(configJson.agentPrompts).length === 0
|
|
3711
|
+
) {
|
|
3712
|
+
configJson.agentPrompts = getDefaultPromptOverrides();
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
// ── .env file ──────────────────────────────────────────
|
|
3716
|
+
const envPath = resolve(targetDir, ".env");
|
|
3717
|
+
const targetEnvPath = existsSync(envPath)
|
|
3718
|
+
? resolve(targetDir, ".env.generated")
|
|
3719
|
+
: envPath;
|
|
3720
|
+
|
|
3721
|
+
if (existsSync(envPath)) {
|
|
3722
|
+
warn(`.env already exists. Writing to .env.generated`);
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
const envTemplatePath = resolve(__dirname, ".env.example");
|
|
3726
|
+
const templateText = existsSync(envTemplatePath)
|
|
3727
|
+
? readFileSync(envTemplatePath, "utf8")
|
|
3728
|
+
: "";
|
|
3729
|
+
|
|
3730
|
+
const envOut = templateText
|
|
3731
|
+
? buildStandardizedEnvFile(templateText, env)
|
|
3732
|
+
: buildStandardizedEnvFile("", env);
|
|
3733
|
+
|
|
3734
|
+
writeFileSync(targetEnvPath, envOut, "utf8");
|
|
3735
|
+
success(`Environment written to ${relative(repoRoot, targetEnvPath)}`);
|
|
3736
|
+
|
|
3737
|
+
// ── openfleet.config.json ──────────────────────────
|
|
3738
|
+
// Write config with schema reference for editor autocomplete
|
|
3739
|
+
const configOut = { $schema: "./openfleet.schema.json", ...configJson };
|
|
3740
|
+
// Keep vkAutoConfig in config file for monitor to apply on first launch
|
|
3741
|
+
// (includes executorProfiles with environment variables like PATH)
|
|
3742
|
+
const configPath = resolve(targetDir, "openfleet.config.json");
|
|
3743
|
+
writeFileSync(configPath, JSON.stringify(configOut, null, 2) + "\n", "utf8");
|
|
3744
|
+
success(`Config written to ${relative(repoRoot, configPath)}`);
|
|
3745
|
+
|
|
3746
|
+
// If the setup target directory differs from the package dir but a local .env
|
|
3747
|
+
// exists there without a config file, seed a config copy to avoid mismatches.
|
|
3748
|
+
const packageDir = resolve(__dirname);
|
|
3749
|
+
if (resolve(packageDir) !== resolve(targetDir)) {
|
|
3750
|
+
const packageEnvPath = resolve(packageDir, ".env");
|
|
3751
|
+
const packageConfigPath = resolve(packageDir, "openfleet.config.json");
|
|
3752
|
+
if (existsSync(packageEnvPath) && !existsSync(packageConfigPath)) {
|
|
3753
|
+
writeFileSync(
|
|
3754
|
+
packageConfigPath,
|
|
3755
|
+
JSON.stringify(configOut, null, 2) + "\n",
|
|
3756
|
+
"utf8",
|
|
3757
|
+
);
|
|
3758
|
+
success(`Config written to ${relative(repoRoot, packageConfigPath)}`);
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
// ── Workspace VS Code settings ─────────────────────────
|
|
3763
|
+
const vscodeSettingsResult = writeWorkspaceVsCodeSettings(repoRoot, env);
|
|
3764
|
+
if (vscodeSettingsResult.updated) {
|
|
3765
|
+
success(
|
|
3766
|
+
`Workspace settings updated: ${relative(repoRoot, vscodeSettingsResult.path)}`,
|
|
3767
|
+
);
|
|
3768
|
+
} else if (vscodeSettingsResult.error) {
|
|
3769
|
+
warn(`Could not update workspace settings: ${vscodeSettingsResult.error}`);
|
|
3770
|
+
}
|
|
3771
|
+
|
|
3772
|
+
const copilotMcpResult = writeWorkspaceCopilotMcpConfig(repoRoot);
|
|
3773
|
+
if (copilotMcpResult.updated) {
|
|
3774
|
+
success(
|
|
3775
|
+
`Copilot MCP config updated: ${relative(repoRoot, copilotMcpResult.path)}`,
|
|
3776
|
+
);
|
|
3777
|
+
} else if (copilotMcpResult.error) {
|
|
3778
|
+
warn(`Could not update Copilot MCP config: ${copilotMcpResult.error}`);
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
// ── Codex CLI config.toml ─────────────────────────────
|
|
3782
|
+
heading("Codex CLI Config");
|
|
3783
|
+
|
|
3784
|
+
if (env._SKIP_VK_TOML === "1") {
|
|
3785
|
+
info("Skipped Vibe-Kanban MCP config update.");
|
|
3786
|
+
} else {
|
|
3787
|
+
const vkPort = env.VK_RECOVERY_PORT || "54089";
|
|
3788
|
+
const vkBaseUrl = env.VK_BASE_URL || `http://127.0.0.1:${vkPort}`;
|
|
3789
|
+
const kanbanIsVk =
|
|
3790
|
+
(env.KANBAN_BACKEND || "internal").toLowerCase() === "vk" ||
|
|
3791
|
+
["vk", "hybrid"].includes((env.EXECUTOR_MODE || "internal").toLowerCase());
|
|
3792
|
+
const tomlResult = ensureCodexConfig({
|
|
3793
|
+
vkBaseUrl,
|
|
3794
|
+
skipVk: !kanbanIsVk,
|
|
3795
|
+
dryRun: false,
|
|
3796
|
+
env: {
|
|
3797
|
+
...process.env,
|
|
3798
|
+
...env,
|
|
3799
|
+
},
|
|
3800
|
+
});
|
|
3801
|
+
printConfigSummary(tomlResult, (msg) => console.log(msg));
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
// ── Install dependencies ───────────────────────────────
|
|
3805
|
+
heading("Installing Dependencies");
|
|
3806
|
+
try {
|
|
3807
|
+
if (commandExists("pnpm")) {
|
|
3808
|
+
execSync("pnpm install", { cwd: __dirname, stdio: "inherit" });
|
|
3809
|
+
} else {
|
|
3810
|
+
execSync("npm install", { cwd: __dirname, stdio: "inherit" });
|
|
3811
|
+
}
|
|
3812
|
+
success("Dependencies installed");
|
|
3813
|
+
} catch {
|
|
3814
|
+
warn(
|
|
3815
|
+
"Dependency install failed — run manually: pnpm install (or) npm install",
|
|
3816
|
+
);
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3819
|
+
// ── Startup Service ────────────────────────────────────
|
|
3820
|
+
if (env._STARTUP_SERVICE === "1") {
|
|
3821
|
+
heading("Startup Service");
|
|
3822
|
+
try {
|
|
3823
|
+
const { installStartupService } = await import("./startup-service.mjs");
|
|
3824
|
+
const result = await installStartupService({ daemon: true });
|
|
3825
|
+
if (result.success) {
|
|
3826
|
+
success(`Registered via ${result.method}`);
|
|
3827
|
+
if (result.name) info(`Service name: ${result.name}`);
|
|
3828
|
+
if (result.path) info(`Config path: ${result.path}`);
|
|
3829
|
+
} else {
|
|
3830
|
+
warn(`Could not register startup service: ${result.error}`);
|
|
3831
|
+
info("You can try manually later: openfleet --enable-startup");
|
|
3832
|
+
}
|
|
3833
|
+
} catch (err) {
|
|
3834
|
+
warn(`Startup service registration failed: ${err.message}`);
|
|
3835
|
+
info("You can try manually later: openfleet --enable-startup");
|
|
3836
|
+
}
|
|
3837
|
+
} else if (env._STARTUP_SERVICE === "0") {
|
|
3838
|
+
info(
|
|
3839
|
+
"Startup service skipped — enable anytime: openfleet --enable-startup",
|
|
3840
|
+
);
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3843
|
+
// ── Summary ────────────────────────────────────────────
|
|
3844
|
+
console.log("");
|
|
3845
|
+
console.log(
|
|
3846
|
+
" ╔═══════════════════════════════════════════════════════════════╗",
|
|
3847
|
+
);
|
|
3848
|
+
console.log(
|
|
3849
|
+
" ║ ✅ Setup Complete! ║",
|
|
3850
|
+
);
|
|
3851
|
+
console.log(
|
|
3852
|
+
" ╚═══════════════════════════════════════════════════════════════╝",
|
|
3853
|
+
);
|
|
3854
|
+
console.log("");
|
|
3855
|
+
|
|
3856
|
+
// Executor summary
|
|
3857
|
+
const totalWeight = configJson.executors.reduce((s, e) => s + e.weight, 0);
|
|
3858
|
+
console.log(chalk.bold(" Executors:"));
|
|
3859
|
+
for (const e of configJson.executors) {
|
|
3860
|
+
const pct =
|
|
3861
|
+
totalWeight > 0 ? Math.round((e.weight / totalWeight) * 100) : 0;
|
|
3862
|
+
console.log(
|
|
3863
|
+
` ${e.role.padEnd(10)} ${e.executor}:${e.variant} — ${pct}%`,
|
|
3864
|
+
);
|
|
3865
|
+
}
|
|
3866
|
+
console.log(
|
|
3867
|
+
chalk.dim(
|
|
3868
|
+
` Strategy: ${configJson.distribution} distribution, ${configJson.failover.strategy} failover`,
|
|
3869
|
+
),
|
|
3870
|
+
);
|
|
3871
|
+
|
|
3872
|
+
// Missing items
|
|
3873
|
+
console.log("");
|
|
3874
|
+
if (!env.TELEGRAM_BOT_TOKEN) {
|
|
3875
|
+
info("Telegram not configured — add TELEGRAM_BOT_TOKEN to .env later.");
|
|
3876
|
+
}
|
|
3877
|
+
if (
|
|
3878
|
+
!env.OPENAI_API_KEY &&
|
|
3879
|
+
!env.AZURE_OPENAI_API_KEY &&
|
|
3880
|
+
!env.CODEX_MODEL_PROFILE_XL_API_KEY &&
|
|
3881
|
+
!env.CODEX_MODEL_PROFILE_M_API_KEY &&
|
|
3882
|
+
!parseBooleanEnvValue(env.CODEX_SDK_DISABLED, false)
|
|
3883
|
+
) {
|
|
3884
|
+
info("No API key set — AI analysis & autofix will be disabled.");
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
console.log("");
|
|
3888
|
+
console.log(chalk.bold(" Next steps:"));
|
|
3889
|
+
console.log("");
|
|
3890
|
+
console.log(chalk.green(" openfleet"));
|
|
3891
|
+
console.log(chalk.dim(" Start the orchestrator supervisor\n"));
|
|
3892
|
+
console.log(chalk.green(" openfleet --setup"));
|
|
3893
|
+
console.log(chalk.dim(" Re-run this wizard anytime\n"));
|
|
3894
|
+
console.log(chalk.green(" openfleet --enable-startup"));
|
|
3895
|
+
console.log(chalk.dim(" Register auto-start on login\n"));
|
|
3896
|
+
console.log(chalk.green(" openfleet --help"));
|
|
3897
|
+
console.log(chalk.dim(" See all options & env vars\n"));
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
// ── Auto-Launch Detection ────────────────────────────────────────────────────
|
|
3901
|
+
|
|
3902
|
+
/**
|
|
3903
|
+
* Check whether setup should run automatically (first launch detection).
|
|
3904
|
+
* Called from monitor.mjs before starting the main loop.
|
|
3905
|
+
*/
|
|
3906
|
+
export function shouldRunSetup() {
|
|
3907
|
+
const repoRoot = detectRepoRoot();
|
|
3908
|
+
const configDir = resolveConfigDir(repoRoot);
|
|
3909
|
+
return !hasSetupMarkers(configDir);
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
/**
|
|
3913
|
+
* Run setup wizard. Can be imported and called from monitor.mjs.
|
|
3914
|
+
*/
|
|
3915
|
+
export async function runSetup() {
|
|
3916
|
+
await main();
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3919
|
+
export {
|
|
3920
|
+
extractProjectNumber,
|
|
3921
|
+
resolveOrCreateGitHubProjectNumber,
|
|
3922
|
+
resolveOrCreateGitHubProject,
|
|
3923
|
+
runGhCommand,
|
|
3924
|
+
buildRecommendedVsCodeSettings,
|
|
3925
|
+
writeWorkspaceVsCodeSettings,
|
|
3926
|
+
};
|
|
3927
|
+
|
|
3928
|
+
// ── Entry Point ──────────────────────────────────────────────────────────────
|
|
3929
|
+
|
|
3930
|
+
// Only run the wizard when executed directly (not when imported by cli.mjs)
|
|
3931
|
+
const __filename_setup = fileURLToPath(import.meta.url);
|
|
3932
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(__filename_setup)) {
|
|
3933
|
+
main().catch((err) => {
|
|
3934
|
+
console.error(`\n Setup failed: ${err.message}\n`);
|
|
3935
|
+
process.exit(1);
|
|
3936
|
+
});
|
|
3937
|
+
}
|