bopodev 0.1.27 → 0.1.29
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 +325 -59
- package/package.json +2 -2
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,10 +491,82 @@ 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";
|
|
452
|
-
import { join as join3, resolve as resolve3 } from "path";
|
|
569
|
+
import { basename, dirname, join as join3, resolve as resolve3 } from "path";
|
|
453
570
|
import { confirm, isCancel, log, select, spinner, text } from "@clack/prompts";
|
|
454
571
|
import dotenv from "dotenv";
|
|
455
572
|
var DEFAULT_COMPANY_NAME_ENV = "BOPO_DEFAULT_COMPANY_NAME";
|
|
@@ -460,6 +577,8 @@ var DEFAULT_AGENT_MODEL_ENV = "BOPO_DEFAULT_AGENT_MODEL";
|
|
|
460
577
|
var DEFAULT_TEMPLATE_ENV = "BOPO_DEFAULT_TEMPLATE_ID";
|
|
461
578
|
var DEFAULT_DEPLOYMENT_MODE_ENV = "BOPO_DEPLOYMENT_MODE";
|
|
462
579
|
var DEFAULT_ENV_TEMPLATE = "NEXT_PUBLIC_API_URL=http://localhost:4020\n";
|
|
580
|
+
var DB_INIT_TIMEOUT_MS = 12e4;
|
|
581
|
+
var ONBOARD_SEED_TIMEOUT_MS = 6e4;
|
|
463
582
|
var CLI_ONBOARD_VISIBLE_PROVIDERS = [
|
|
464
583
|
{ value: "codex", label: "Codex" },
|
|
465
584
|
{ value: "claude_code", label: "Claude Code" },
|
|
@@ -483,6 +602,7 @@ var defaultDeps = {
|
|
|
483
602
|
initializeDatabase: async (workspaceRoot, dbPath) => {
|
|
484
603
|
const result = await runCommandCapture("pnpm", ["--filter", "bopodev-api", "db:init"], {
|
|
485
604
|
cwd: workspaceRoot,
|
|
605
|
+
timeoutMs: DB_INIT_TIMEOUT_MS,
|
|
486
606
|
env: {
|
|
487
607
|
...process.env,
|
|
488
608
|
...dbPath ? { BOPO_DB_PATH: dbPath } : {}
|
|
@@ -496,6 +616,7 @@ var defaultDeps = {
|
|
|
496
616
|
seedOnboardingDatabase: async (workspaceRoot, input) => {
|
|
497
617
|
const result = await runCommandCapture("pnpm", ["--filter", "bopodev-api", "onboard:seed"], {
|
|
498
618
|
cwd: workspaceRoot,
|
|
619
|
+
timeoutMs: ONBOARD_SEED_TIMEOUT_MS,
|
|
499
620
|
env: {
|
|
500
621
|
...process.env,
|
|
501
622
|
[DEFAULT_COMPANY_NAME_ENV]: input.companyName,
|
|
@@ -624,23 +745,19 @@ async function runOnboardFlow(options, deps = defaultDeps) {
|
|
|
624
745
|
} else {
|
|
625
746
|
printCheck("ok", "Dependencies", "Already installed");
|
|
626
747
|
}
|
|
748
|
+
let checks = [];
|
|
749
|
+
let passed = 0;
|
|
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
|
+
}
|
|
627
759
|
const envPath = join3(workspaceRoot, ".env");
|
|
628
760
|
const preEnvValues = await fileExists2(envPath) ? await readEnvValues(envPath) : {};
|
|
629
|
-
const doctorSpin = spinner();
|
|
630
|
-
doctorSpin.start("Running doctor checks");
|
|
631
|
-
const checks = await deps.runDoctor(workspaceRoot);
|
|
632
|
-
doctorSpin.stop("Doctor checks complete");
|
|
633
|
-
const runtimeAvailability = deriveAvailableAgentProviders(checks);
|
|
634
|
-
const passed = checks.filter((check) => check.ok).length;
|
|
635
|
-
const warnings = checks.length - passed;
|
|
636
|
-
printCheck("ok", "Doctor", "checks complete");
|
|
637
|
-
printCheck("ok", "Doctor summary", `${passed} passed, ${warnings} warning${warnings === 1 ? "" : "s"}`);
|
|
638
|
-
if (warnings === 0) {
|
|
639
|
-
printCheck("ok", "Doctor status", "All checks passed");
|
|
640
|
-
}
|
|
641
|
-
for (const check of checks) {
|
|
642
|
-
printCheck(check.ok ? "ok" : "warn", check.label, check.details);
|
|
643
|
-
}
|
|
644
761
|
let companyName = preEnvValues[DEFAULT_COMPANY_NAME_ENV]?.trim() ?? "";
|
|
645
762
|
if (companyName.length > 0) {
|
|
646
763
|
printCheck("ok", "Default company", companyName);
|
|
@@ -648,7 +765,7 @@ async function runOnboardFlow(options, deps = defaultDeps) {
|
|
|
648
765
|
companyName = await deps.promptForCompanyName();
|
|
649
766
|
printCheck("ok", "Default company", companyName);
|
|
650
767
|
}
|
|
651
|
-
const selectableProviders =
|
|
768
|
+
const selectableProviders = CLI_ONBOARD_VISIBLE_PROVIDERS.map((entry) => entry.value);
|
|
652
769
|
const configuredProvider = parseAgentProvider(preEnvValues[DEFAULT_AGENT_PROVIDER_ENV]);
|
|
653
770
|
let agentProvider = configuredProvider ?? selectableProviders[0] ?? "codex";
|
|
654
771
|
const canReuseProvider = Boolean(configuredProvider && selectableProviders.includes(configuredProvider));
|
|
@@ -709,7 +826,14 @@ async function runOnboardFlow(options, deps = defaultDeps) {
|
|
|
709
826
|
}
|
|
710
827
|
dotenv.config({ path: envPath, quiet: true });
|
|
711
828
|
const envValues = await readEnvValues(envPath);
|
|
712
|
-
const
|
|
829
|
+
const configuredDbPathInfo = await normalizeConfiguredDbPathForOnboarding(normalizeOptionalEnvValue(envValues.BOPO_DB_PATH));
|
|
830
|
+
const configuredDbPath = configuredDbPathInfo.path;
|
|
831
|
+
if (configuredDbPathInfo.rewrittenFrom && configuredDbPath) {
|
|
832
|
+
await updateEnvFile(envPath, {
|
|
833
|
+
BOPO_DB_PATH: configuredDbPath
|
|
834
|
+
});
|
|
835
|
+
printCheck("warn", "Database path", `Updated legacy local DB path to ${configuredDbPath}`);
|
|
836
|
+
}
|
|
713
837
|
if (configuredDbPath) {
|
|
714
838
|
process.env.BOPO_DB_PATH = configuredDbPath;
|
|
715
839
|
} else {
|
|
@@ -780,11 +904,23 @@ async function runOnboardFlow(options, deps = defaultDeps) {
|
|
|
780
904
|
seedResult.templateApplied ? `Applied ${seedResult.templateId ?? requestedTemplateId}` : `Template not applied (${requestedTemplateId})`
|
|
781
905
|
);
|
|
782
906
|
}
|
|
907
|
+
if (!options.start) {
|
|
908
|
+
printCheck("ok", "Doctor", "checks complete");
|
|
909
|
+
printCheck("ok", "Doctor summary", `${passed} passed, ${warnings} warning${warnings === 1 ? "" : "s"}`);
|
|
910
|
+
if (warnings === 0) {
|
|
911
|
+
printCheck("ok", "Doctor status", "All checks passed");
|
|
912
|
+
}
|
|
913
|
+
for (const check of checks) {
|
|
914
|
+
printCheck(check.ok ? "ok" : "warn", check.label, check.details);
|
|
915
|
+
}
|
|
916
|
+
} else {
|
|
917
|
+
printCheck("ok", "Doctor", "Available on demand with `pnpm doctor`.");
|
|
918
|
+
}
|
|
783
919
|
const dbPathSummary = resolveDbPathSummary(configuredDbPath);
|
|
784
920
|
printSummaryCard([
|
|
785
921
|
`Mode ${padSummaryValue("local")}`,
|
|
786
922
|
`Deploy ${padSummaryValue("local_mac")}`,
|
|
787
|
-
`Doctor ${padSummaryValue(`${passed} passed, ${warnings} warning${warnings === 1 ? "" : "s"}`)}`,
|
|
923
|
+
`Doctor ${padSummaryValue(options.start ? "On demand" : `${passed} passed, ${warnings} warning${warnings === 1 ? "" : "s"}`)}`,
|
|
788
924
|
`Company ${padSummaryValue(`${seedResult.companyName} (${seedResult.companyId})`)}`,
|
|
789
925
|
`Agent ${padSummaryValue(formatAgentProvider(seedResult.ceoProviderType))}`,
|
|
790
926
|
`Model ${padSummaryValue(seedResult.ceoRuntimeModel ?? selectedAgentModel ?? "provider default")}`,
|
|
@@ -870,6 +1006,28 @@ async function sanitizeBlankDbPathEnvEntry(envPath) {
|
|
|
870
1006
|
await writeFile(envPath, nextContent.endsWith("\n") ? nextContent : `${nextContent}
|
|
871
1007
|
`, "utf8");
|
|
872
1008
|
}
|
|
1009
|
+
async function normalizeConfiguredDbPathForOnboarding(rawValue) {
|
|
1010
|
+
const configuredPath = normalizeOptionalEnvValue(rawValue);
|
|
1011
|
+
if (!configuredPath || !looksLikeLegacyDbFilePath(configuredPath)) {
|
|
1012
|
+
return {
|
|
1013
|
+
path: configuredPath,
|
|
1014
|
+
rewrittenFrom: void 0
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
const resolvedPath = resolve3(expandHomePrefix3(configuredPath));
|
|
1018
|
+
const postgresMarker = join3(resolvedPath, "PG_VERSION");
|
|
1019
|
+
if (await fileExists2(postgresMarker)) {
|
|
1020
|
+
return {
|
|
1021
|
+
path: resolvedPath,
|
|
1022
|
+
rewrittenFrom: void 0
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
const normalizedPath = join3(dirname(resolvedPath), "postgres");
|
|
1026
|
+
return {
|
|
1027
|
+
path: normalizedPath,
|
|
1028
|
+
rewrittenFrom: configuredPath
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
873
1031
|
async function removeEnvKeys(envPath, keys) {
|
|
874
1032
|
if (keys.length === 0) {
|
|
875
1033
|
return;
|
|
@@ -887,34 +1045,13 @@ function normalizeOptionalEnvValue(value) {
|
|
|
887
1045
|
const normalized = value?.trim();
|
|
888
1046
|
return normalized && normalized.length > 0 ? normalized : void 0;
|
|
889
1047
|
}
|
|
890
|
-
function deriveAvailableAgentProviders(checks) {
|
|
891
|
-
const providers = [];
|
|
892
|
-
for (const check of checks) {
|
|
893
|
-
if (!check.ok) {
|
|
894
|
-
continue;
|
|
895
|
-
}
|
|
896
|
-
if (check.label === "Codex runtime") {
|
|
897
|
-
providers.push("codex");
|
|
898
|
-
}
|
|
899
|
-
if (check.label === "Claude Code runtime") {
|
|
900
|
-
providers.push("claude_code");
|
|
901
|
-
}
|
|
902
|
-
if (check.label === "Gemini runtime") {
|
|
903
|
-
providers.push("gemini_cli");
|
|
904
|
-
}
|
|
905
|
-
if (check.label === "OpenCode runtime") {
|
|
906
|
-
providers.push("opencode");
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
return Array.from(new Set(providers));
|
|
910
|
-
}
|
|
911
1048
|
function resolveDbPathSummary(configuredDbPath) {
|
|
912
1049
|
if (configuredDbPath) {
|
|
913
1050
|
return resolve3(expandHomePrefix3(configuredDbPath));
|
|
914
1051
|
}
|
|
915
1052
|
const home = process.env.BOPO_HOME?.trim() ? expandHomePrefix3(process.env.BOPO_HOME.trim()) : join3(homedir3(), ".bopodev");
|
|
916
1053
|
const instanceId = process.env.BOPO_INSTANCE_ID?.trim() || "default";
|
|
917
|
-
return resolve3(home, "instances", instanceId, "db", "
|
|
1054
|
+
return resolve3(home, "instances", instanceId, "db", "postgres");
|
|
918
1055
|
}
|
|
919
1056
|
function expandHomePrefix3(value) {
|
|
920
1057
|
if (value === "~") {
|
|
@@ -925,6 +1062,10 @@ function expandHomePrefix3(value) {
|
|
|
925
1062
|
}
|
|
926
1063
|
return value;
|
|
927
1064
|
}
|
|
1065
|
+
function looksLikeLegacyDbFilePath(value) {
|
|
1066
|
+
const name = basename(value).toLowerCase();
|
|
1067
|
+
return name.endsWith(".db");
|
|
1068
|
+
}
|
|
928
1069
|
function padSummaryValue(value) {
|
|
929
1070
|
return `| ${value}`;
|
|
930
1071
|
}
|
|
@@ -1056,6 +1197,96 @@ async function runStartCommand(cwd, options) {
|
|
|
1056
1197
|
}
|
|
1057
1198
|
}
|
|
1058
1199
|
|
|
1200
|
+
// src/commands/upgrade.ts
|
|
1201
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1202
|
+
import { homedir as homedir4 } from "os";
|
|
1203
|
+
import { join as join4, resolve as resolve4 } from "path";
|
|
1204
|
+
import dotenv2 from "dotenv";
|
|
1205
|
+
var UPGRADE_TIMEOUT_MS = 12e4;
|
|
1206
|
+
var UNSTICK_TIMEOUT_MS = 3e4;
|
|
1207
|
+
async function runUpgradeCommand(cwd, options) {
|
|
1208
|
+
const workspaceRoot = await resolveWorkspaceRootOrManaged(cwd);
|
|
1209
|
+
if (!workspaceRoot) {
|
|
1210
|
+
throw new Error("Could not find a Bopodev workspace root. Run `bopodev onboard` first.");
|
|
1211
|
+
}
|
|
1212
|
+
printBanner();
|
|
1213
|
+
printSection("bopodev upgrade");
|
|
1214
|
+
printLine(`Workspace: ${workspaceRoot}`);
|
|
1215
|
+
printDivider();
|
|
1216
|
+
const envPath = join4(workspaceRoot, ".env");
|
|
1217
|
+
const envValues = await readEnvValues2(envPath);
|
|
1218
|
+
const configuredDbPath = normalizeOptionalEnvValue2(envValues.BOPO_DB_PATH);
|
|
1219
|
+
const dbPathSummary = resolveDbPathSummary2(configuredDbPath);
|
|
1220
|
+
printCheck("warn", "Backup", `Back up local data before major upgrades if needed: ${dbPathSummary}`);
|
|
1221
|
+
const stopResult = await runCommandCapture("pnpm", ["unstick"], {
|
|
1222
|
+
cwd: workspaceRoot,
|
|
1223
|
+
timeoutMs: UNSTICK_TIMEOUT_MS
|
|
1224
|
+
});
|
|
1225
|
+
if (!stopResult.ok) {
|
|
1226
|
+
throw new Error(renderCommandFailure("pnpm unstick", stopResult.stderr, stopResult.stdout, stopResult.code));
|
|
1227
|
+
}
|
|
1228
|
+
printCheck("ok", "Runtime", "Stopped active local processes");
|
|
1229
|
+
const migrateResult = await runCommandCapture("pnpm", ["db:migrate"], {
|
|
1230
|
+
cwd: workspaceRoot,
|
|
1231
|
+
env: {
|
|
1232
|
+
...process.env,
|
|
1233
|
+
...configuredDbPath ? { BOPO_DB_PATH: configuredDbPath } : {}
|
|
1234
|
+
},
|
|
1235
|
+
timeoutMs: UPGRADE_TIMEOUT_MS
|
|
1236
|
+
});
|
|
1237
|
+
if (!migrateResult.ok) {
|
|
1238
|
+
throw new Error(renderCommandFailure("pnpm db:migrate", migrateResult.stderr, migrateResult.stdout, migrateResult.code));
|
|
1239
|
+
}
|
|
1240
|
+
printCheck("ok", "Database", "Migrations applied and schema verified");
|
|
1241
|
+
printSummaryCard([
|
|
1242
|
+
`Mode | local upgrade`,
|
|
1243
|
+
`DB | ${dbPathSummary}`,
|
|
1244
|
+
`Status | migrated`
|
|
1245
|
+
]);
|
|
1246
|
+
if (options?.start === false) {
|
|
1247
|
+
printSection("Next commands");
|
|
1248
|
+
printLine("- Run: pnpm start:quiet");
|
|
1249
|
+
printLine("- Diagnose: pnpm doctor");
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
printLine("Restarting services after upgrade...");
|
|
1253
|
+
printDivider();
|
|
1254
|
+
await runStartCommand(workspaceRoot, { quiet: options?.quiet !== false });
|
|
1255
|
+
}
|
|
1256
|
+
async function readEnvValues2(path) {
|
|
1257
|
+
try {
|
|
1258
|
+
const content = await readFile2(path, "utf8");
|
|
1259
|
+
return dotenv2.parse(content);
|
|
1260
|
+
} catch {
|
|
1261
|
+
return {};
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
function normalizeOptionalEnvValue2(value) {
|
|
1265
|
+
const normalized = value?.trim();
|
|
1266
|
+
return normalized && normalized.length > 0 ? normalized : void 0;
|
|
1267
|
+
}
|
|
1268
|
+
function resolveDbPathSummary2(configuredDbPath) {
|
|
1269
|
+
if (configuredDbPath) {
|
|
1270
|
+
return resolve4(expandHomePrefix4(configuredDbPath));
|
|
1271
|
+
}
|
|
1272
|
+
const home = process.env.BOPO_HOME?.trim() ? expandHomePrefix4(process.env.BOPO_HOME.trim()) : join4(homedir4(), ".bopodev");
|
|
1273
|
+
const instanceId = process.env.BOPO_INSTANCE_ID?.trim() || "default";
|
|
1274
|
+
return resolve4(home, "instances", instanceId, "db", "postgres");
|
|
1275
|
+
}
|
|
1276
|
+
function expandHomePrefix4(value) {
|
|
1277
|
+
if (value === "~") {
|
|
1278
|
+
return homedir4();
|
|
1279
|
+
}
|
|
1280
|
+
if (value.startsWith("~/")) {
|
|
1281
|
+
return resolve4(homedir4(), value.slice(2));
|
|
1282
|
+
}
|
|
1283
|
+
return value;
|
|
1284
|
+
}
|
|
1285
|
+
function renderCommandFailure(command, stderr, stdout, code) {
|
|
1286
|
+
const details = [stderr, stdout].filter((value) => value.trim().length > 0).join("\n").trim();
|
|
1287
|
+
return details.length > 0 ? details : `${command} failed with exit code ${String(code)}`;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1059
1290
|
// src/index.ts
|
|
1060
1291
|
var program = new Command();
|
|
1061
1292
|
program.name("bopodev").description("Bopodev CLI");
|
|
@@ -1093,4 +1324,39 @@ program.command("doctor").description("Run local preflight checks").action(async
|
|
|
1093
1324
|
process.exitCode = 1;
|
|
1094
1325
|
}
|
|
1095
1326
|
});
|
|
1327
|
+
var issueCommand = program.command("issue").description("Issue helpers for terminal workflows");
|
|
1328
|
+
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(
|
|
1329
|
+
async (issueId, opts) => {
|
|
1330
|
+
try {
|
|
1331
|
+
const companyId = (opts.companyId ?? process.env.BOPODEV_COMPANY_ID ?? "").trim();
|
|
1332
|
+
if (!companyId) {
|
|
1333
|
+
cancel("Set --company-id or BOPODEV_COMPANY_ID.");
|
|
1334
|
+
process.exitCode = 1;
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
await runIssueShellEnvCommand(issueId, {
|
|
1338
|
+
apiUrl: opts.apiUrl,
|
|
1339
|
+
companyId,
|
|
1340
|
+
json: opts.json
|
|
1341
|
+
});
|
|
1342
|
+
} catch (error) {
|
|
1343
|
+
cancel(String(error));
|
|
1344
|
+
process.exitCode = 1;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
);
|
|
1348
|
+
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) => {
|
|
1349
|
+
try {
|
|
1350
|
+
await runUpgradeCommand(process.cwd(), {
|
|
1351
|
+
start: options.start,
|
|
1352
|
+
quiet: !options.fullLogs
|
|
1353
|
+
});
|
|
1354
|
+
if (!options.start) {
|
|
1355
|
+
outro("Upgrade finished.");
|
|
1356
|
+
}
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
cancel(String(error));
|
|
1359
|
+
process.exitCode = 1;
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1096
1362
|
void program.parseAsync(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bopodev",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"typescript": "^5.9.2"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
|
-
"cli:dev": "tsx src/index.ts",
|
|
29
|
+
"cli:dev": "node --import tsx src/index.ts",
|
|
30
30
|
"build": "tsup src/index.ts --format esm --platform node --target node20 --out-dir dist",
|
|
31
31
|
"lint": "tsc -p tsconfig.json --noEmit",
|
|
32
32
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|