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.
Files changed (2) hide show
  1. package/dist/index.js +167 -41
  2. 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
- 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
- }
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 = runtimeAvailability.length > 0 ? runtimeAvailability : CLI_ONBOARD_VISIBLE_PROVIDERS.map((entry) => entry.value);
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 configuredDbPath = normalizeOptionalEnvValue(envValues.BOPO_DB_PATH);
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", "bopodev.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.26",
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"