bopodev 0.1.26 → 0.1.28
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 +167 -41
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -449,7 +449,7 @@ async function runDoctorCommand(cwd) {
|
|
|
449
449
|
// src/commands/onboard.ts
|
|
450
450
|
import { access as access3, copyFile, readFile, writeFile } from "fs/promises";
|
|
451
451
|
import { homedir as homedir3 } from "os";
|
|
452
|
-
import { join as join3, resolve as resolve3 } from "path";
|
|
452
|
+
import { basename, dirname, join as join3, resolve as resolve3 } from "path";
|
|
453
453
|
import { confirm, isCancel, log, select, spinner, text } from "@clack/prompts";
|
|
454
454
|
import dotenv from "dotenv";
|
|
455
455
|
var DEFAULT_COMPANY_NAME_ENV = "BOPO_DEFAULT_COMPANY_NAME";
|
|
@@ -460,6 +460,8 @@ var DEFAULT_AGENT_MODEL_ENV = "BOPO_DEFAULT_AGENT_MODEL";
|
|
|
460
460
|
var DEFAULT_TEMPLATE_ENV = "BOPO_DEFAULT_TEMPLATE_ID";
|
|
461
461
|
var DEFAULT_DEPLOYMENT_MODE_ENV = "BOPO_DEPLOYMENT_MODE";
|
|
462
462
|
var DEFAULT_ENV_TEMPLATE = "NEXT_PUBLIC_API_URL=http://localhost:4020\n";
|
|
463
|
+
var DB_INIT_TIMEOUT_MS = 12e4;
|
|
464
|
+
var ONBOARD_SEED_TIMEOUT_MS = 6e4;
|
|
463
465
|
var CLI_ONBOARD_VISIBLE_PROVIDERS = [
|
|
464
466
|
{ value: "codex", label: "Codex" },
|
|
465
467
|
{ value: "claude_code", label: "Claude Code" },
|
|
@@ -483,6 +485,7 @@ var defaultDeps = {
|
|
|
483
485
|
initializeDatabase: async (workspaceRoot, dbPath) => {
|
|
484
486
|
const result = await runCommandCapture("pnpm", ["--filter", "bopodev-api", "db:init"], {
|
|
485
487
|
cwd: workspaceRoot,
|
|
488
|
+
timeoutMs: DB_INIT_TIMEOUT_MS,
|
|
486
489
|
env: {
|
|
487
490
|
...process.env,
|
|
488
491
|
...dbPath ? { BOPO_DB_PATH: dbPath } : {}
|
|
@@ -496,6 +499,7 @@ var defaultDeps = {
|
|
|
496
499
|
seedOnboardingDatabase: async (workspaceRoot, input) => {
|
|
497
500
|
const result = await runCommandCapture("pnpm", ["--filter", "bopodev-api", "onboard:seed"], {
|
|
498
501
|
cwd: workspaceRoot,
|
|
502
|
+
timeoutMs: ONBOARD_SEED_TIMEOUT_MS,
|
|
499
503
|
env: {
|
|
500
504
|
...process.env,
|
|
501
505
|
[DEFAULT_COMPANY_NAME_ENV]: input.companyName,
|
|
@@ -626,21 +630,9 @@ async function runOnboardFlow(options, deps = defaultDeps) {
|
|
|
626
630
|
}
|
|
627
631
|
const envPath = join3(workspaceRoot, ".env");
|
|
628
632
|
const preEnvValues = await fileExists2(envPath) ? await readEnvValues(envPath) : {};
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
-
}
|
|
633
|
+
let checks = [];
|
|
634
|
+
let passed = 0;
|
|
635
|
+
let warnings = 0;
|
|
644
636
|
let companyName = preEnvValues[DEFAULT_COMPANY_NAME_ENV]?.trim() ?? "";
|
|
645
637
|
if (companyName.length > 0) {
|
|
646
638
|
printCheck("ok", "Default company", companyName);
|
|
@@ -648,7 +640,7 @@ async function runOnboardFlow(options, deps = defaultDeps) {
|
|
|
648
640
|
companyName = await deps.promptForCompanyName();
|
|
649
641
|
printCheck("ok", "Default company", companyName);
|
|
650
642
|
}
|
|
651
|
-
const selectableProviders =
|
|
643
|
+
const selectableProviders = CLI_ONBOARD_VISIBLE_PROVIDERS.map((entry) => entry.value);
|
|
652
644
|
const configuredProvider = parseAgentProvider(preEnvValues[DEFAULT_AGENT_PROVIDER_ENV]);
|
|
653
645
|
let agentProvider = configuredProvider ?? selectableProviders[0] ?? "codex";
|
|
654
646
|
const canReuseProvider = Boolean(configuredProvider && selectableProviders.includes(configuredProvider));
|
|
@@ -709,7 +701,14 @@ async function runOnboardFlow(options, deps = defaultDeps) {
|
|
|
709
701
|
}
|
|
710
702
|
dotenv.config({ path: envPath, quiet: true });
|
|
711
703
|
const envValues = await readEnvValues(envPath);
|
|
712
|
-
const
|
|
704
|
+
const configuredDbPathInfo = await normalizeConfiguredDbPathForOnboarding(normalizeOptionalEnvValue(envValues.BOPO_DB_PATH));
|
|
705
|
+
const configuredDbPath = configuredDbPathInfo.path;
|
|
706
|
+
if (configuredDbPathInfo.rewrittenFrom && configuredDbPath) {
|
|
707
|
+
await updateEnvFile(envPath, {
|
|
708
|
+
BOPO_DB_PATH: configuredDbPath
|
|
709
|
+
});
|
|
710
|
+
printCheck("warn", "Database path", `Updated legacy local DB path to ${configuredDbPath}`);
|
|
711
|
+
}
|
|
713
712
|
if (configuredDbPath) {
|
|
714
713
|
process.env.BOPO_DB_PATH = configuredDbPath;
|
|
715
714
|
} else {
|
|
@@ -780,11 +779,29 @@ async function runOnboardFlow(options, deps = defaultDeps) {
|
|
|
780
779
|
seedResult.templateApplied ? `Applied ${seedResult.templateId ?? requestedTemplateId}` : `Template not applied (${requestedTemplateId})`
|
|
781
780
|
);
|
|
782
781
|
}
|
|
782
|
+
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
|
+
printCheck("ok", "Doctor", "checks complete");
|
|
790
|
+
printCheck("ok", "Doctor summary", `${passed} passed, ${warnings} warning${warnings === 1 ? "" : "s"}`);
|
|
791
|
+
if (warnings === 0) {
|
|
792
|
+
printCheck("ok", "Doctor status", "All checks passed");
|
|
793
|
+
}
|
|
794
|
+
for (const check of checks) {
|
|
795
|
+
printCheck(check.ok ? "ok" : "warn", check.label, check.details);
|
|
796
|
+
}
|
|
797
|
+
} else {
|
|
798
|
+
printCheck("ok", "Doctor", "Available on demand with `pnpm doctor`.");
|
|
799
|
+
}
|
|
783
800
|
const dbPathSummary = resolveDbPathSummary(configuredDbPath);
|
|
784
801
|
printSummaryCard([
|
|
785
802
|
`Mode ${padSummaryValue("local")}`,
|
|
786
803
|
`Deploy ${padSummaryValue("local_mac")}`,
|
|
787
|
-
`Doctor ${padSummaryValue(`${passed} passed, ${warnings} warning${warnings === 1 ? "" : "s"}`)}`,
|
|
804
|
+
`Doctor ${padSummaryValue(options.start ? "On demand" : `${passed} passed, ${warnings} warning${warnings === 1 ? "" : "s"}`)}`,
|
|
788
805
|
`Company ${padSummaryValue(`${seedResult.companyName} (${seedResult.companyId})`)}`,
|
|
789
806
|
`Agent ${padSummaryValue(formatAgentProvider(seedResult.ceoProviderType))}`,
|
|
790
807
|
`Model ${padSummaryValue(seedResult.ceoRuntimeModel ?? selectedAgentModel ?? "provider default")}`,
|
|
@@ -870,6 +887,28 @@ async function sanitizeBlankDbPathEnvEntry(envPath) {
|
|
|
870
887
|
await writeFile(envPath, nextContent.endsWith("\n") ? nextContent : `${nextContent}
|
|
871
888
|
`, "utf8");
|
|
872
889
|
}
|
|
890
|
+
async function normalizeConfiguredDbPathForOnboarding(rawValue) {
|
|
891
|
+
const configuredPath = normalizeOptionalEnvValue(rawValue);
|
|
892
|
+
if (!configuredPath || !looksLikeLegacyDbFilePath(configuredPath)) {
|
|
893
|
+
return {
|
|
894
|
+
path: configuredPath,
|
|
895
|
+
rewrittenFrom: void 0
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
const resolvedPath = resolve3(expandHomePrefix3(configuredPath));
|
|
899
|
+
const postgresMarker = join3(resolvedPath, "PG_VERSION");
|
|
900
|
+
if (await fileExists2(postgresMarker)) {
|
|
901
|
+
return {
|
|
902
|
+
path: resolvedPath,
|
|
903
|
+
rewrittenFrom: void 0
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
const normalizedPath = join3(dirname(resolvedPath), "postgres");
|
|
907
|
+
return {
|
|
908
|
+
path: normalizedPath,
|
|
909
|
+
rewrittenFrom: configuredPath
|
|
910
|
+
};
|
|
911
|
+
}
|
|
873
912
|
async function removeEnvKeys(envPath, keys) {
|
|
874
913
|
if (keys.length === 0) {
|
|
875
914
|
return;
|
|
@@ -887,34 +926,13 @@ function normalizeOptionalEnvValue(value) {
|
|
|
887
926
|
const normalized = value?.trim();
|
|
888
927
|
return normalized && normalized.length > 0 ? normalized : void 0;
|
|
889
928
|
}
|
|
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
929
|
function resolveDbPathSummary(configuredDbPath) {
|
|
912
930
|
if (configuredDbPath) {
|
|
913
931
|
return resolve3(expandHomePrefix3(configuredDbPath));
|
|
914
932
|
}
|
|
915
933
|
const home = process.env.BOPO_HOME?.trim() ? expandHomePrefix3(process.env.BOPO_HOME.trim()) : join3(homedir3(), ".bopodev");
|
|
916
934
|
const instanceId = process.env.BOPO_INSTANCE_ID?.trim() || "default";
|
|
917
|
-
return resolve3(home, "instances", instanceId, "db", "
|
|
935
|
+
return resolve3(home, "instances", instanceId, "db", "postgres");
|
|
918
936
|
}
|
|
919
937
|
function expandHomePrefix3(value) {
|
|
920
938
|
if (value === "~") {
|
|
@@ -925,6 +943,10 @@ function expandHomePrefix3(value) {
|
|
|
925
943
|
}
|
|
926
944
|
return value;
|
|
927
945
|
}
|
|
946
|
+
function looksLikeLegacyDbFilePath(value) {
|
|
947
|
+
const name = basename(value).toLowerCase();
|
|
948
|
+
return name.endsWith(".db");
|
|
949
|
+
}
|
|
928
950
|
function padSummaryValue(value) {
|
|
929
951
|
return `| ${value}`;
|
|
930
952
|
}
|
|
@@ -1056,6 +1078,96 @@ async function runStartCommand(cwd, options) {
|
|
|
1056
1078
|
}
|
|
1057
1079
|
}
|
|
1058
1080
|
|
|
1081
|
+
// src/commands/upgrade.ts
|
|
1082
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1083
|
+
import { homedir as homedir4 } from "os";
|
|
1084
|
+
import { join as join4, resolve as resolve4 } from "path";
|
|
1085
|
+
import dotenv2 from "dotenv";
|
|
1086
|
+
var UPGRADE_TIMEOUT_MS = 12e4;
|
|
1087
|
+
var UNSTICK_TIMEOUT_MS = 3e4;
|
|
1088
|
+
async function runUpgradeCommand(cwd, options) {
|
|
1089
|
+
const workspaceRoot = await resolveWorkspaceRootOrManaged(cwd);
|
|
1090
|
+
if (!workspaceRoot) {
|
|
1091
|
+
throw new Error("Could not find a Bopodev workspace root. Run `bopodev onboard` first.");
|
|
1092
|
+
}
|
|
1093
|
+
printBanner();
|
|
1094
|
+
printSection("bopodev upgrade");
|
|
1095
|
+
printLine(`Workspace: ${workspaceRoot}`);
|
|
1096
|
+
printDivider();
|
|
1097
|
+
const envPath = join4(workspaceRoot, ".env");
|
|
1098
|
+
const envValues = await readEnvValues2(envPath);
|
|
1099
|
+
const configuredDbPath = normalizeOptionalEnvValue2(envValues.BOPO_DB_PATH);
|
|
1100
|
+
const dbPathSummary = resolveDbPathSummary2(configuredDbPath);
|
|
1101
|
+
printCheck("warn", "Backup", `Back up local data before major upgrades if needed: ${dbPathSummary}`);
|
|
1102
|
+
const stopResult = await runCommandCapture("pnpm", ["unstick"], {
|
|
1103
|
+
cwd: workspaceRoot,
|
|
1104
|
+
timeoutMs: UNSTICK_TIMEOUT_MS
|
|
1105
|
+
});
|
|
1106
|
+
if (!stopResult.ok) {
|
|
1107
|
+
throw new Error(renderCommandFailure("pnpm unstick", stopResult.stderr, stopResult.stdout, stopResult.code));
|
|
1108
|
+
}
|
|
1109
|
+
printCheck("ok", "Runtime", "Stopped active local processes");
|
|
1110
|
+
const migrateResult = await runCommandCapture("pnpm", ["db:migrate"], {
|
|
1111
|
+
cwd: workspaceRoot,
|
|
1112
|
+
env: {
|
|
1113
|
+
...process.env,
|
|
1114
|
+
...configuredDbPath ? { BOPO_DB_PATH: configuredDbPath } : {}
|
|
1115
|
+
},
|
|
1116
|
+
timeoutMs: UPGRADE_TIMEOUT_MS
|
|
1117
|
+
});
|
|
1118
|
+
if (!migrateResult.ok) {
|
|
1119
|
+
throw new Error(renderCommandFailure("pnpm db:migrate", migrateResult.stderr, migrateResult.stdout, migrateResult.code));
|
|
1120
|
+
}
|
|
1121
|
+
printCheck("ok", "Database", "Migrations applied and schema verified");
|
|
1122
|
+
printSummaryCard([
|
|
1123
|
+
`Mode | local upgrade`,
|
|
1124
|
+
`DB | ${dbPathSummary}`,
|
|
1125
|
+
`Status | migrated`
|
|
1126
|
+
]);
|
|
1127
|
+
if (options?.start === false) {
|
|
1128
|
+
printSection("Next commands");
|
|
1129
|
+
printLine("- Run: pnpm start:quiet");
|
|
1130
|
+
printLine("- Diagnose: pnpm doctor");
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
printLine("Restarting services after upgrade...");
|
|
1134
|
+
printDivider();
|
|
1135
|
+
await runStartCommand(workspaceRoot, { quiet: options?.quiet !== false });
|
|
1136
|
+
}
|
|
1137
|
+
async function readEnvValues2(path) {
|
|
1138
|
+
try {
|
|
1139
|
+
const content = await readFile2(path, "utf8");
|
|
1140
|
+
return dotenv2.parse(content);
|
|
1141
|
+
} catch {
|
|
1142
|
+
return {};
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
function normalizeOptionalEnvValue2(value) {
|
|
1146
|
+
const normalized = value?.trim();
|
|
1147
|
+
return normalized && normalized.length > 0 ? normalized : void 0;
|
|
1148
|
+
}
|
|
1149
|
+
function resolveDbPathSummary2(configuredDbPath) {
|
|
1150
|
+
if (configuredDbPath) {
|
|
1151
|
+
return resolve4(expandHomePrefix4(configuredDbPath));
|
|
1152
|
+
}
|
|
1153
|
+
const home = process.env.BOPO_HOME?.trim() ? expandHomePrefix4(process.env.BOPO_HOME.trim()) : join4(homedir4(), ".bopodev");
|
|
1154
|
+
const instanceId = process.env.BOPO_INSTANCE_ID?.trim() || "default";
|
|
1155
|
+
return resolve4(home, "instances", instanceId, "db", "postgres");
|
|
1156
|
+
}
|
|
1157
|
+
function expandHomePrefix4(value) {
|
|
1158
|
+
if (value === "~") {
|
|
1159
|
+
return homedir4();
|
|
1160
|
+
}
|
|
1161
|
+
if (value.startsWith("~/")) {
|
|
1162
|
+
return resolve4(homedir4(), value.slice(2));
|
|
1163
|
+
}
|
|
1164
|
+
return value;
|
|
1165
|
+
}
|
|
1166
|
+
function renderCommandFailure(command, stderr, stdout, code) {
|
|
1167
|
+
const details = [stderr, stdout].filter((value) => value.trim().length > 0).join("\n").trim();
|
|
1168
|
+
return details.length > 0 ? details : `${command} failed with exit code ${String(code)}`;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1059
1171
|
// src/index.ts
|
|
1060
1172
|
var program = new Command();
|
|
1061
1173
|
program.name("bopodev").description("Bopodev CLI");
|
|
@@ -1093,4 +1205,18 @@ program.command("doctor").description("Run local preflight checks").action(async
|
|
|
1093
1205
|
process.exitCode = 1;
|
|
1094
1206
|
}
|
|
1095
1207
|
});
|
|
1208
|
+
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
|
+
try {
|
|
1210
|
+
await runUpgradeCommand(process.cwd(), {
|
|
1211
|
+
start: options.start,
|
|
1212
|
+
quiet: !options.fullLogs
|
|
1213
|
+
});
|
|
1214
|
+
if (!options.start) {
|
|
1215
|
+
outro("Upgrade finished.");
|
|
1216
|
+
}
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
cancel(String(error));
|
|
1219
|
+
process.exitCode = 1;
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1096
1222
|
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.28",
|
|
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"
|