bopodev 0.1.28 → 0.1.30
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/dist/index.js +170 -27
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -20,19 +20,41 @@ async function commandExists(command) {
|
|
|
20
20
|
}
|
|
21
21
|
async function runCommandCapture(command, args, options) {
|
|
22
22
|
return new Promise((resolvePromise) => {
|
|
23
|
+
const supportsProcessGroups = process.platform !== "win32";
|
|
23
24
|
const child = spawn(command, args, {
|
|
24
25
|
cwd: options?.cwd ?? process.cwd(),
|
|
25
26
|
env: options?.env ?? process.env,
|
|
26
27
|
stdio: ["ignore", "pipe", "pipe"],
|
|
27
|
-
shell: false
|
|
28
|
+
shell: false,
|
|
29
|
+
detached: supportsProcessGroups
|
|
28
30
|
});
|
|
29
31
|
let stdout = "";
|
|
30
32
|
let stderr = "";
|
|
33
|
+
let settled = false;
|
|
34
|
+
let timedOut = false;
|
|
31
35
|
const timeoutMs = Math.max(0, Math.floor(options?.timeoutMs ?? 1e4));
|
|
36
|
+
let killHandle = null;
|
|
37
|
+
const terminate = (signal) => {
|
|
38
|
+
if (child.killed || child.exitCode !== null) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
if (supportsProcessGroups && child.pid) {
|
|
43
|
+
process.kill(-child.pid, signal);
|
|
44
|
+
} else {
|
|
45
|
+
child.kill(signal);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
};
|
|
32
50
|
const timeoutHandle = timeoutMs > 0 ? setTimeout(() => {
|
|
51
|
+
timedOut = true;
|
|
33
52
|
stderr = `${stderr}
|
|
34
53
|
Command '${command}' timed out after ${timeoutMs}ms.`.trim();
|
|
35
|
-
|
|
54
|
+
terminate("SIGTERM");
|
|
55
|
+
killHandle = setTimeout(() => {
|
|
56
|
+
terminate("SIGKILL");
|
|
57
|
+
}, 5e3);
|
|
36
58
|
}, timeoutMs) : null;
|
|
37
59
|
child.stdout.on("data", (chunk) => {
|
|
38
60
|
stdout += String(chunk);
|
|
@@ -41,9 +63,16 @@ Command '${command}' timed out after ${timeoutMs}ms.`.trim();
|
|
|
41
63
|
stderr += String(chunk);
|
|
42
64
|
});
|
|
43
65
|
child.on("error", (error) => {
|
|
66
|
+
if (settled) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
settled = true;
|
|
44
70
|
if (timeoutHandle) {
|
|
45
71
|
clearTimeout(timeoutHandle);
|
|
46
72
|
}
|
|
73
|
+
if (killHandle) {
|
|
74
|
+
clearTimeout(killHandle);
|
|
75
|
+
}
|
|
47
76
|
resolvePromise({
|
|
48
77
|
ok: false,
|
|
49
78
|
code: null,
|
|
@@ -53,11 +82,18 @@ ${String(error)}`.trim()
|
|
|
53
82
|
});
|
|
54
83
|
});
|
|
55
84
|
child.on("close", (code) => {
|
|
85
|
+
if (settled) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
settled = true;
|
|
56
89
|
if (timeoutHandle) {
|
|
57
90
|
clearTimeout(timeoutHandle);
|
|
58
91
|
}
|
|
92
|
+
if (killHandle) {
|
|
93
|
+
clearTimeout(killHandle);
|
|
94
|
+
}
|
|
59
95
|
resolvePromise({
|
|
60
|
-
ok: code === 0,
|
|
96
|
+
ok: code === 0 && !timedOut,
|
|
61
97
|
code,
|
|
62
98
|
stdout,
|
|
63
99
|
stderr
|
|
@@ -175,41 +211,43 @@ async function runDoctorChecks(options) {
|
|
|
175
211
|
ok: nodeMajor >= 20,
|
|
176
212
|
details: `Detected ${process.versions.node}; requires >= 20`
|
|
177
213
|
});
|
|
178
|
-
const
|
|
214
|
+
const codexCommand = process.env.BOPO_CODEX_COMMAND?.trim() || "codex";
|
|
215
|
+
const openCodeCommand = process.env.BOPO_OPENCODE_COMMAND?.trim() || "opencode";
|
|
216
|
+
const claudeCommand = process.env.BOPO_CLAUDE_COMMAND?.trim() || "claude";
|
|
217
|
+
const geminiCommand = process.env.BOPO_GEMINI_COMMAND?.trim() || "gemini";
|
|
218
|
+
const [pnpmAvailable, gitRuntime, codex, openCode, claude, gemini] = await Promise.all([
|
|
219
|
+
commandExists("pnpm"),
|
|
220
|
+
checkRuntimeCommandHealth("git", options?.workspaceRoot),
|
|
221
|
+
checkRuntimeCommandHealth(codexCommand, options?.workspaceRoot),
|
|
222
|
+
checkRuntimeCommandHealth(openCodeCommand, options?.workspaceRoot),
|
|
223
|
+
checkRuntimeCommandHealth(claudeCommand, options?.workspaceRoot),
|
|
224
|
+
checkRuntimeCommandHealth(geminiCommand, options?.workspaceRoot)
|
|
225
|
+
]);
|
|
179
226
|
checks.push({
|
|
180
227
|
label: "pnpm",
|
|
181
228
|
ok: pnpmAvailable,
|
|
182
229
|
details: pnpmAvailable ? "pnpm is available" : "pnpm is not installed or not in PATH"
|
|
183
230
|
});
|
|
184
|
-
const codexCommand = process.env.BOPO_CODEX_COMMAND?.trim() || "codex";
|
|
185
|
-
const gitRuntime = await checkRuntimeCommandHealth("git", options?.workspaceRoot);
|
|
186
231
|
checks.push({
|
|
187
232
|
label: "Git runtime",
|
|
188
233
|
ok: gitRuntime.available && gitRuntime.exitCode === 0,
|
|
189
234
|
details: gitRuntime.available && gitRuntime.exitCode === 0 ? "Command 'git' is available (required for repo bootstrap/worktree execution)" : gitRuntime.error ?? "Command 'git' is not available"
|
|
190
235
|
});
|
|
191
|
-
const codex = await checkRuntimeCommandHealth(codexCommand, options?.workspaceRoot);
|
|
192
236
|
checks.push({
|
|
193
237
|
label: "Codex runtime",
|
|
194
238
|
ok: codex.available && codex.exitCode === 0,
|
|
195
239
|
details: codex.available && codex.exitCode === 0 ? `Command '${codexCommand}' is available` : codex.error ?? `Command '${codexCommand}' exited with ${String(codex.exitCode)}`
|
|
196
240
|
});
|
|
197
|
-
const openCodeCommand = process.env.BOPO_OPENCODE_COMMAND?.trim() || "opencode";
|
|
198
|
-
const openCode = await checkRuntimeCommandHealth(openCodeCommand, options?.workspaceRoot);
|
|
199
241
|
checks.push({
|
|
200
242
|
label: "OpenCode runtime",
|
|
201
243
|
ok: openCode.available && openCode.exitCode === 0,
|
|
202
244
|
details: openCode.available && openCode.exitCode === 0 ? `Command '${openCodeCommand}' is available` : openCode.error ?? `Command '${openCodeCommand}' exited with ${String(openCode.exitCode)}`
|
|
203
245
|
});
|
|
204
|
-
const claudeCommand = process.env.BOPO_CLAUDE_COMMAND?.trim() || "claude";
|
|
205
|
-
const claude = await checkRuntimeCommandHealth(claudeCommand, options?.workspaceRoot);
|
|
206
246
|
checks.push({
|
|
207
247
|
label: "Claude Code runtime",
|
|
208
248
|
ok: claude.available && claude.exitCode === 0,
|
|
209
249
|
details: claude.available && claude.exitCode === 0 ? `Command '${claudeCommand}' is available` : claude.error ?? `Command '${claudeCommand}' exited with ${String(claude.exitCode)}`
|
|
210
250
|
});
|
|
211
|
-
const geminiCommand = process.env.BOPO_GEMINI_COMMAND?.trim() || "gemini";
|
|
212
|
-
const gemini = await checkRuntimeCommandHealth(geminiCommand, options?.workspaceRoot);
|
|
213
251
|
checks.push({
|
|
214
252
|
label: "Gemini runtime",
|
|
215
253
|
ok: gemini.available && gemini.exitCode === 0,
|
|
@@ -219,19 +257,24 @@ async function runDoctorChecks(options) {
|
|
|
219
257
|
const instanceRoot = resolveInstanceRoot2();
|
|
220
258
|
const storageRoot = join2(instanceRoot, "data", "storage");
|
|
221
259
|
const workspaceRoot = join2(instanceRoot, "workspaces");
|
|
260
|
+
const [instanceRootWritable, workspaceRootWritable, storageRootWritable] = await Promise.all([
|
|
261
|
+
ensureWritableDirectory(instanceRoot),
|
|
262
|
+
ensureWritableDirectory(workspaceRoot),
|
|
263
|
+
ensureWritableDirectory(storageRoot)
|
|
264
|
+
]);
|
|
222
265
|
checks.push({
|
|
223
266
|
label: "Instance root writable",
|
|
224
|
-
ok:
|
|
267
|
+
ok: instanceRootWritable,
|
|
225
268
|
details: instanceRoot
|
|
226
269
|
});
|
|
227
270
|
checks.push({
|
|
228
271
|
label: "Workspace root writable",
|
|
229
|
-
ok:
|
|
272
|
+
ok: workspaceRootWritable,
|
|
230
273
|
details: workspaceRoot
|
|
231
274
|
});
|
|
232
275
|
checks.push({
|
|
233
276
|
label: "Storage root writable",
|
|
234
|
-
ok:
|
|
277
|
+
ok: storageRootWritable,
|
|
235
278
|
details: storageRoot
|
|
236
279
|
});
|
|
237
280
|
} catch (error) {
|
|
@@ -242,9 +285,11 @@ async function runDoctorChecks(options) {
|
|
|
242
285
|
});
|
|
243
286
|
}
|
|
244
287
|
if (options?.workspaceRoot) {
|
|
245
|
-
const driftCheck = await
|
|
288
|
+
const [driftCheck, backfillCheck] = await Promise.all([
|
|
289
|
+
runWorkspacePathDriftCheck(options.workspaceRoot),
|
|
290
|
+
runWorkspaceBackfillDryRunCheck(options.workspaceRoot)
|
|
291
|
+
]);
|
|
246
292
|
checks.push(driftCheck);
|
|
247
|
-
const backfillCheck = await runWorkspaceBackfillDryRunCheck(options.workspaceRoot);
|
|
248
293
|
checks.push(backfillCheck);
|
|
249
294
|
}
|
|
250
295
|
return checks;
|
|
@@ -446,6 +491,78 @@ async function runDoctorCommand(cwd) {
|
|
|
446
491
|
printSummaryCard([`Summary: ${passed} passed, ${failed} warnings`]);
|
|
447
492
|
}
|
|
448
493
|
|
|
494
|
+
// src/commands/issue-shell-env.ts
|
|
495
|
+
import process2 from "process";
|
|
496
|
+
function readEnv(name, fallback = "") {
|
|
497
|
+
return (process2.env[name] ?? fallback).trim();
|
|
498
|
+
}
|
|
499
|
+
function buildActorHeaders() {
|
|
500
|
+
const token = readEnv("BOPO_ACTOR_TOKEN") || readEnv("BOPODEV_ACTOR_TOKEN");
|
|
501
|
+
if (token) {
|
|
502
|
+
return { authorization: `Bearer ${token}` };
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
"x-actor-type": "board",
|
|
506
|
+
"x-actor-id": "bopodev-cli",
|
|
507
|
+
"x-actor-companies": "",
|
|
508
|
+
"x-actor-permissions": ""
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
async function runIssueShellEnvCommand(issueId, options) {
|
|
512
|
+
const base = options.apiUrl.replace(/\/$/, "");
|
|
513
|
+
const headers = {
|
|
514
|
+
"x-company-id": options.companyId,
|
|
515
|
+
...buildActorHeaders()
|
|
516
|
+
};
|
|
517
|
+
const issueRes = await fetch(`${base}/issues/${encodeURIComponent(issueId)}`, { headers });
|
|
518
|
+
const issueRaw = await issueRes.json();
|
|
519
|
+
if (!issueRes.ok || !issueRaw.ok) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
!issueRaw.ok && "error" in issueRaw ? String(issueRaw.error) : `Failed to load issue (${issueRes.status})`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
const issue = issueRaw.data;
|
|
525
|
+
const wsRes = await fetch(`${base}/projects/${encodeURIComponent(issue.projectId)}/workspaces`, { headers });
|
|
526
|
+
const wsRaw = await wsRes.json();
|
|
527
|
+
if (!wsRes.ok || !wsRaw.ok) {
|
|
528
|
+
throw new Error(
|
|
529
|
+
!wsRaw.ok && "error" in wsRaw ? String(wsRaw.error) : `Failed to load workspaces (${wsRes.status})`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
const workspaces = wsRaw.data;
|
|
533
|
+
const primary = workspaces.find((w) => w.isPrimary) ?? workspaces[0];
|
|
534
|
+
const cwd = primary?.cwd?.trim() || "";
|
|
535
|
+
const env = {
|
|
536
|
+
BOPODEV_API_BASE_URL: base,
|
|
537
|
+
BOPODEV_COMPANY_ID: options.companyId,
|
|
538
|
+
BOPODEV_ISSUE_ID: issue.id,
|
|
539
|
+
BOPODEV_PROJECT_ID: issue.projectId
|
|
540
|
+
};
|
|
541
|
+
if (options.json) {
|
|
542
|
+
console.log(JSON.stringify({ ...env, suggestedCwd: cwd || null, issueTitle: issue.title }, null, 2));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const lines = [
|
|
546
|
+
`# Issue: ${issue.title}`,
|
|
547
|
+
`export BOPODEV_API_BASE_URL=${shellQuote(base)}`,
|
|
548
|
+
`export BOPODEV_COMPANY_ID=${shellQuote(options.companyId)}`,
|
|
549
|
+
`export BOPODEV_ISSUE_ID=${shellQuote(issue.id)}`,
|
|
550
|
+
`export BOPODEV_PROJECT_ID=${shellQuote(issue.projectId)}`
|
|
551
|
+
];
|
|
552
|
+
if (cwd) {
|
|
553
|
+
lines.push(`cd ${shellQuote(cwd)}`);
|
|
554
|
+
} else {
|
|
555
|
+
lines.push("# No primary workspace cwd set for this project; set cwd in project workspaces in the UI.");
|
|
556
|
+
}
|
|
557
|
+
console.log(lines.join("\n"));
|
|
558
|
+
}
|
|
559
|
+
function shellQuote(value) {
|
|
560
|
+
if (!/[\s'"\\$`!]/.test(value)) {
|
|
561
|
+
return value;
|
|
562
|
+
}
|
|
563
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
564
|
+
}
|
|
565
|
+
|
|
449
566
|
// src/commands/onboard.ts
|
|
450
567
|
import { access as access3, copyFile, readFile, writeFile } from "fs/promises";
|
|
451
568
|
import { homedir as homedir3 } from "os";
|
|
@@ -628,11 +745,19 @@ async function runOnboardFlow(options, deps = defaultDeps) {
|
|
|
628
745
|
} else {
|
|
629
746
|
printCheck("ok", "Dependencies", "Already installed");
|
|
630
747
|
}
|
|
631
|
-
const envPath = join3(workspaceRoot, ".env");
|
|
632
|
-
const preEnvValues = await fileExists2(envPath) ? await readEnvValues(envPath) : {};
|
|
633
748
|
let checks = [];
|
|
634
749
|
let passed = 0;
|
|
635
750
|
let warnings = 0;
|
|
751
|
+
if (!options.start) {
|
|
752
|
+
const doctorSpin = spinner();
|
|
753
|
+
doctorSpin.start("Running doctor checks");
|
|
754
|
+
checks = await deps.runDoctor(workspaceRoot);
|
|
755
|
+
doctorSpin.stop("Doctor checks complete");
|
|
756
|
+
passed = checks.filter((check) => check.ok).length;
|
|
757
|
+
warnings = checks.length - passed;
|
|
758
|
+
}
|
|
759
|
+
const envPath = join3(workspaceRoot, ".env");
|
|
760
|
+
const preEnvValues = await fileExists2(envPath) ? await readEnvValues(envPath) : {};
|
|
636
761
|
let companyName = preEnvValues[DEFAULT_COMPANY_NAME_ENV]?.trim() ?? "";
|
|
637
762
|
if (companyName.length > 0) {
|
|
638
763
|
printCheck("ok", "Default company", companyName);
|
|
@@ -780,12 +905,6 @@ async function runOnboardFlow(options, deps = defaultDeps) {
|
|
|
780
905
|
);
|
|
781
906
|
}
|
|
782
907
|
if (!options.start) {
|
|
783
|
-
const doctorSpin = spinner();
|
|
784
|
-
doctorSpin.start("Running doctor checks");
|
|
785
|
-
checks = await deps.runDoctor(workspaceRoot);
|
|
786
|
-
doctorSpin.stop("Doctor checks complete");
|
|
787
|
-
passed = checks.filter((check) => check.ok).length;
|
|
788
|
-
warnings = checks.length - passed;
|
|
789
908
|
printCheck("ok", "Doctor", "checks complete");
|
|
790
909
|
printCheck("ok", "Doctor summary", `${passed} passed, ${warnings} warning${warnings === 1 ? "" : "s"}`);
|
|
791
910
|
if (warnings === 0) {
|
|
@@ -1025,7 +1144,7 @@ function parseSeedResult(stdout) {
|
|
|
1025
1144
|
};
|
|
1026
1145
|
}
|
|
1027
1146
|
function parseAgentProvider(value) {
|
|
1028
|
-
if (value === "codex" || value === "claude_code" || value === "gemini_cli" || value === "opencode" || value === "openai_api" || value === "anthropic_api" || value === "shell") {
|
|
1147
|
+
if (value === "codex" || value === "claude_code" || value === "gemini_cli" || value === "opencode" || value === "openai_api" || value === "anthropic_api" || value === "openclaw_gateway" || value === "shell") {
|
|
1029
1148
|
return value;
|
|
1030
1149
|
}
|
|
1031
1150
|
return null;
|
|
@@ -1049,6 +1168,9 @@ function formatAgentProvider(provider) {
|
|
|
1049
1168
|
if (provider === "anthropic_api") {
|
|
1050
1169
|
return "Anthropic API (direct)";
|
|
1051
1170
|
}
|
|
1171
|
+
if (provider === "openclaw_gateway") {
|
|
1172
|
+
return "OpenClaw Gateway";
|
|
1173
|
+
}
|
|
1052
1174
|
return "Shell Runtime";
|
|
1053
1175
|
}
|
|
1054
1176
|
async function fileExists2(path) {
|
|
@@ -1205,6 +1327,27 @@ program.command("doctor").description("Run local preflight checks").action(async
|
|
|
1205
1327
|
process.exitCode = 1;
|
|
1206
1328
|
}
|
|
1207
1329
|
});
|
|
1330
|
+
var issueCommand = program.command("issue").description("Issue helpers for terminal workflows");
|
|
1331
|
+
issueCommand.command("shell-env <issueId>").description("Print shell exports for BOPODEV_* and cd to the project primary workspace cwd when set").option("--api-url <url>", "API base URL", process.env.BOPODEV_API_URL ?? "http://localhost:4020").option("--company-id <id>", "Company id (default: BOPODEV_COMPANY_ID)").option("--json", "Print JSON instead of shell exports", false).action(
|
|
1332
|
+
async (issueId, opts) => {
|
|
1333
|
+
try {
|
|
1334
|
+
const companyId = (opts.companyId ?? process.env.BOPODEV_COMPANY_ID ?? "").trim();
|
|
1335
|
+
if (!companyId) {
|
|
1336
|
+
cancel("Set --company-id or BOPODEV_COMPANY_ID.");
|
|
1337
|
+
process.exitCode = 1;
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
await runIssueShellEnvCommand(issueId, {
|
|
1341
|
+
apiUrl: opts.apiUrl,
|
|
1342
|
+
companyId,
|
|
1343
|
+
json: opts.json
|
|
1344
|
+
});
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
cancel(String(error));
|
|
1347
|
+
process.exitCode = 1;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
);
|
|
1208
1351
|
program.command("upgrade").description("Stop local services, apply migrations, verify schema, and optionally restart").option("--no-start", "Only migrate and verify without restarting services").option("--full-logs", "Use full startup logs instead of quiet mode when restarting", false).action(async (options) => {
|
|
1209
1352
|
try {
|
|
1210
1353
|
await runUpgradeCommand(process.cwd(), {
|