@pasko70/pibo 3.0.0 → 3.0.2

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 (77) hide show
  1. package/README.md +20 -6
  2. package/dist/agent-runtime/context-build.js +26 -2
  3. package/dist/agent-runtime/routed-session.js +108 -9
  4. package/dist/agent-runtimes/codex-native/adapter.js +12 -0
  5. package/dist/agent-runtimes/codex-native/resource-delivery.js +35 -7
  6. package/dist/agent-runtimes/omp/adapter.js +3 -7
  7. package/dist/agent-runtimes/pi/adapter.js +5 -3
  8. package/dist/agent-runtimes/pi/routed-session.js +49 -8
  9. package/dist/agent-runtimes/pi/runtime.js +6 -0
  10. package/dist/apps/chat/agent-profiles.js +31 -2
  11. package/dist/apps/chat/agent-store.js +43 -3
  12. package/dist/apps/chat/chat-request-normalizers.js +31 -0
  13. package/dist/apps/chat/data/chat-data-mappers.js +61 -11
  14. package/dist/apps/chat/data/session-query-service.js +33 -10
  15. package/dist/apps/chat/data/timeline-query-service.js +60 -35
  16. package/dist/apps/chat/output-compactor.js +144 -21
  17. package/dist/apps/chat/output-event-policy.js +179 -2
  18. package/dist/apps/chat/stream.js +16 -7
  19. package/dist/apps/chat/trace-v2.js +1 -0
  20. package/dist/apps/chat/web-app.js +236 -64
  21. package/dist/apps/chat-ui/assets/{dist-BBDMeU5-.js → dist-B6GZgWSj.js} +1 -1
  22. package/dist/apps/chat-ui/assets/{dist-mqCviI-c.js → dist-BPotHecb.js} +1 -1
  23. package/dist/apps/chat-ui/assets/{dist-JnW4v8UE.js → dist-BzqQKeVO.js} +1 -1
  24. package/dist/apps/chat-ui/assets/{dist-BDIATCQt.js → dist-CvaPTBTN.js} +1 -1
  25. package/dist/apps/chat-ui/assets/{dist-lRiLP9rr.js → dist-paagejNj.js} +1 -1
  26. package/dist/apps/chat-ui/assets/index-6JJV-eic.js +228 -0
  27. package/dist/apps/chat-ui/assets/index-CeL9JPP5.css +1 -0
  28. package/dist/apps/chat-ui/index.html +2 -2
  29. package/dist/apps/chat-vscode-web/assets/{index-Dtw2Z7jz.js → index-U-MSErGa.js} +4 -4
  30. package/dist/apps/chat-vscode-web/index.html +1 -1
  31. package/dist/apps/cli-ui/cliSessionsCommand.js +34 -8
  32. package/dist/cli-session/localSessionSource.js +153 -161
  33. package/dist/cli.js +7 -11
  34. package/dist/core/context-build.js +7 -16
  35. package/dist/core/output-persistence-retry.js +484 -0
  36. package/dist/core/output-render-sequence.js +331 -0
  37. package/dist/core/profiles.js +24 -0
  38. package/dist/core/provider-recovery.js +7 -1
  39. package/dist/core/session-model.js +37 -1
  40. package/dist/core/session-router.js +67 -20
  41. package/dist/data/ingest-service.js +91 -9
  42. package/dist/data/navigation-store.js +5 -5
  43. package/dist/data/schema.js +122 -2
  44. package/dist/data/session-store.js +3 -3
  45. package/dist/debug/index.js +44 -2
  46. package/dist/gateway/web.js +8 -8
  47. package/dist/local/client.js +3 -2
  48. package/dist/plugins/context-files-store.js +30 -0
  49. package/dist/plugins/context-files.js +52 -1
  50. package/dist/plugins/registry.js +2 -0
  51. package/dist/plugins/user-profile-resources.js +22 -0
  52. package/dist/reliability/store.js +246 -37
  53. package/dist/session-ui/terminalRows.js +6 -3
  54. package/dist/sessions/pibo-data-store.js +145 -0
  55. package/dist/sessions/store.js +42 -0
  56. package/dist/setup/cli.js +422 -6
  57. package/dist/setup/installation-profiles.js +464 -0
  58. package/dist/shared/deterministic-digest.js +94 -0
  59. package/dist/shared/trace-engine.js +54 -0
  60. package/dist/shared/trace-event-projection.js +124 -67
  61. package/dist/shared/trace-history.js +47 -12
  62. package/dist/shared/trace-live-reducer.js +23 -4
  63. package/dist/shared/trace-nodes.js +17 -28
  64. package/dist/shared/trace-order.js +41 -5
  65. package/dist/shared/trace-page-merge.js +44 -14
  66. package/dist/shared/trace-patch-nodes.js +2 -0
  67. package/dist/shared/trace-subagent-links.js +4 -1
  68. package/dist/shared/trace-tool-identity.js +15 -8
  69. package/dist/signals/projector.js +28 -6
  70. package/dist/subagents/tool.js +7 -3
  71. package/docs/ops/vscode-extension-release.md +9 -1
  72. package/npm-shrinkwrap.json +2 -2
  73. package/package.json +2 -1
  74. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +0 -228
  75. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +0 -1
  76. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  77. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.0.vsix +0 -0
package/dist/setup/cli.js CHANGED
@@ -1,17 +1,26 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { resolve4 } from "node:dns/promises";
3
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
- import { dirname, isAbsolute, join } from "node:path";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { dirname, isAbsolute, join, resolve } from "node:path";
5
+ import { createInterface } from "node:readline/promises";
5
6
  import { Command } from "commander";
6
7
  import { getPiboConfigValue, loadPiboConfig } from "../config/config.js";
7
8
  import { getPiboHome } from "../core/pibo-home.js";
8
9
  import { getWslInfo, isWsl } from "../core/wsl.js";
10
+ import { createInstallationPlan, inspectInstallation, installationManifestPath, materializeInstallationPlan, parseInstallationComponent, parseInstallationProfile, readInstallationManifest, runPendingInstallationActions, uninstallInstallation, validateInstallationPlanTargets, } from "./installation-profiles.js";
9
11
  function parsePort(value) {
10
12
  const port = Number(value);
11
13
  if (!Number.isInteger(port) || port < 1 || port > 65535)
12
14
  throw new Error("Port must be an integer between 1 and 65535");
13
15
  return port;
14
16
  }
17
+ function parseInstallationDomain(value) {
18
+ const domain = value.trim().toLowerCase();
19
+ if (domain.length > 253 || !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(domain)) {
20
+ throw new Error("Domain must be a DNS hostname such as pibo.example.com");
21
+ }
22
+ return domain;
23
+ }
15
24
  function parseNonNegativeNumber(value) {
16
25
  const parsed = Number(value);
17
26
  if (!Number.isFinite(parsed) || parsed < 0)
@@ -356,7 +365,7 @@ function commandExists(command) {
356
365
  }
357
366
  function commandOutput(command, args) {
358
367
  try {
359
- return execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
368
+ return execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 5_000 }).trim();
360
369
  }
361
370
  catch {
362
371
  return undefined;
@@ -498,11 +507,100 @@ async function createDoctorStatus(options) {
498
507
  }
499
508
  checks.push(...swapCheck(options.minSwapGb));
500
509
  checks.push(...authConfigChecks(piboHome));
510
+ const installation = inspectInstallation({ piboHome });
511
+ if (installation.installed) {
512
+ const installationManifest = readInstallationManifest(installation.manifestPath);
513
+ if (installationManifest?.domain && existsSync(join(piboHome, "config.json"))) {
514
+ const config = loadPiboConfig(join(piboHome, "config.json"));
515
+ if (getPiboConfigValue(config, "auth.mode") === "local")
516
+ checks.push({ name: "setup.auth-boundary", status: "fail", detail: "A public installation domain cannot use local auth; configure Better Auth and rerun setup upgrade" });
517
+ }
518
+ for (const check of installation.checks)
519
+ checks.push({ ...check, name: `setup.${check.name}` });
520
+ const componentNames = new Set(installation.components.map((component) => component.name));
521
+ for (const service of componentNames.has("vscode-web") ? ["pibo-web", "pibo-code-server", "caddy"] : ["pibo-web", "caddy"]) {
522
+ const active = commandOutput("systemctl", ["is-active", service]) === "active";
523
+ checks.push({ name: `service:${service}`, status: active ? "ok" : "fail", detail: active ? `${service} is active` : `${service} is not active; repair with ${installation.repairCommand}` });
524
+ }
525
+ if (componentNames.has("browser-tools")) {
526
+ for (const name of ["browser-use", "agent-browser"]) {
527
+ const executable = name === "browser-use" ? join(piboHome, "tools", name, ".venv", "bin", name) : join(piboHome, "tools", name, "node", "node_modules", ".bin", name);
528
+ const installed = existsSync(executable);
529
+ checks.push({ name: `tool:${name}`, status: installed ? "ok" : "fail", detail: installed ? `${name} is installed through the curated tool registry` : `${name} is missing; repair with \`pibo setup component add browser-tools --apply --yes\`` });
530
+ const profileRoots = name === "browser-use"
531
+ ? [join(piboHome, "tools", name, "home", "chrome-profiles"), join(piboHome, "tools", name, "home", "auth-pool"), join(piboHome, "tools", name, "home", "pibo-browser-pool")]
532
+ : [join(piboHome, "tools", name, "home", "profiles"), join(piboHome, "tools", name, "home", "profiles", "auth-template"), join(piboHome, "tools", name, "home", "profiles", "leases")];
533
+ for (const profileRoot of profileRoots) {
534
+ const privateDirectory = existsSync(profileRoot) && (statSync(profileRoot).mode & 0o077) === 0;
535
+ checks.push({ name: `browser-profiles:${name}:${profileRoot.split("/").at(-1)}`, status: privateDirectory ? "ok" : "fail", detail: privateDirectory ? `${profileRoot} is present with private permissions` : `${profileRoot} is missing or accessible to group/other users` });
536
+ }
537
+ }
538
+ }
539
+ if (componentNames.has("mcp-defaults")) {
540
+ const binDir = join(piboHome, "setup", "mcp-runtime", "node_modules", ".bin");
541
+ for (const binary of ["chrome-devtools-mcp", "mcp-server-filesystem"]) {
542
+ const path = join(binDir, binary);
543
+ checks.push({ name: `mcp:${binary}`, status: existsSync(path) ? "ok" : "fail", detail: existsSync(path) ? `${path} is installed from the pinned setup manifest` : `${path} is missing; repair with ${installation.repairCommand}` });
544
+ }
545
+ const chromeDevtoolsPath = join(binDir, "chrome-devtools-mcp");
546
+ if (existsSync(chromeDevtoolsPath)) {
547
+ const starts = commandOutput(chromeDevtoolsPath, ["--help"]) !== undefined;
548
+ checks.push({ name: "mcp:chrome-devtools-startup", status: starts ? "ok" : "fail", detail: starts ? "Chrome DevTools MCP starts and parses its command line" : `Chrome DevTools MCP failed its startup probe; repair with ${installation.repairCommand}` });
549
+ }
550
+ }
551
+ if (componentNames.has("web-annotations")) {
552
+ try {
553
+ const response = await fetch("http://127.0.0.1:4788/apps/web-annotations/overlay.js?pibo-setup-doctor=1", { redirect: "manual", signal: AbortSignal.timeout(3_000) });
554
+ const available = [200, 204, 302, 303, 307, 308, 401, 403].includes(response.status);
555
+ checks.push({ name: "http:web-annotations", status: available ? "ok" : "fail", detail: available ? `Web Annotations route is available (HTTP ${response.status})` : `Web Annotations route returned unexpected HTTP ${response.status}` });
556
+ }
557
+ catch (error) {
558
+ checks.push({ name: "http:web-annotations", status: "fail", detail: `Web Annotations route probe failed: ${error instanceof Error ? error.message : String(error)}` });
559
+ }
560
+ }
561
+ if (componentNames.has("vscode-web")) {
562
+ const listeners = commandOutput("ss", ["-ltnH"]);
563
+ if (!listeners)
564
+ checks.push({ name: "listener:vscode-web", status: "warn", detail: "Could not inspect TCP listeners with ss" });
565
+ else {
566
+ const lines = listeners.split("\n").filter((line) => /:4790\b/.test(line));
567
+ const unsafe = lines.some((line) => /(?:0\.0\.0\.0|\[::\]|\*):4790\b/.test(line));
568
+ const loopback = lines.some((line) => /127\.0\.0\.1:4790\b/.test(line));
569
+ checks.push({ name: "listener:vscode-web", status: unsafe || !loopback ? "fail" : "ok", detail: unsafe ? "VS Code Web is exposed beyond loopback" : loopback ? "VS Code Web listens only on 127.0.0.1:4790" : "VS Code Web is not listening on 127.0.0.1:4790" });
570
+ }
571
+ try {
572
+ const response = await fetch("http://127.0.0.1:4790/healthz", { signal: AbortSignal.timeout(3_000) });
573
+ checks.push({ name: "http:vscode-web-internal", status: response.ok ? "ok" : "fail", detail: response.ok ? "VS Code Web internal health endpoint is reachable" : `VS Code Web internal health returned HTTP ${response.status}` });
574
+ }
575
+ catch (error) {
576
+ checks.push({ name: "http:vscode-web-internal", status: "fail", detail: `VS Code Web internal health failed: ${error instanceof Error ? error.message : String(error)}` });
577
+ }
578
+ const publicDomain = options.domain ?? installationManifest?.domain;
579
+ if (publicDomain) {
580
+ try {
581
+ const response = await fetch(`https://${publicDomain}/apps/vscode/`, { redirect: "manual", signal: AbortSignal.timeout(5_000) });
582
+ const protectedStatus = [302, 303, 307, 308, 401, 403].includes(response.status);
583
+ checks.push({ name: "proxy:vscode-auth", status: protectedStatus ? "ok" : "fail", detail: protectedStatus ? `Unauthenticated VS Code Web request was blocked with HTTP ${response.status}` : response.status === 200 ? "VS Code Web returned HTTP 200 without authentication" : `VS Code Web proxy returned unexpected HTTP ${response.status}` });
584
+ }
585
+ catch (error) {
586
+ checks.push({ name: "proxy:vscode-auth", status: "fail", detail: `VS Code Web public auth-gate probe failed: ${error instanceof Error ? error.message : String(error)}` });
587
+ }
588
+ const websocketStatus = commandOutput("curl", ["-sS", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "5", "--http1.1", "-H", "Connection: Upgrade", "-H", "Upgrade: websocket", "-H", "Sec-WebSocket-Version: 13", "-H", "Sec-WebSocket-Key: cGliby1zZXR1cC1wcm9iZQ==", `https://${publicDomain}/apps/vscode/ws?pibo-setup-doctor=1`]);
589
+ const websocketProtected = websocketStatus !== undefined && [302, 303, 307, 308, 401, 403].includes(Number(websocketStatus));
590
+ checks.push({ name: "proxy:vscode-websocket-auth", status: websocketProtected ? "ok" : "fail", detail: websocketProtected ? `Unauthenticated VS Code WebSocket upgrade was blocked with HTTP ${websocketStatus}` : `VS Code WebSocket auth-gate probe failed${websocketStatus ? ` with HTTP ${websocketStatus}` : ""}` });
591
+ }
592
+ else
593
+ checks.push({ name: "proxy:vscode-auth", status: "warn", detail: "No installation domain is recorded; pass --domain to verify the public auth gate" });
594
+ }
595
+ }
596
+ else {
597
+ checks.push({ name: "setup.manifest", status: "warn", detail: "No setup-managed installation profile is recorded. Inspect one with `pibo setup plan --profile batteries-included`." });
598
+ }
501
599
  checks.push(...await dnsChecks(options.domain, options.expectedIp, "production"));
502
600
  checks.push(...await dnsChecks(options.devDomain, options.expectedIp, "development"));
503
601
  const recommendations = [
504
- "Use user-host setup for normal npm installs.",
505
- "Use developer-host setup only when you need prod/dev gateways, Docker compute workers, GitHub App PR flow, and branch worktrees.",
602
+ "Use the Batteries Included profile for the recommended complete self-hosted workstation, or Vanilla for a minimal controlled base.",
603
+ "Use developer-host topology only when you need prod/dev gateways, Docker compute workers, GitHub App PR flow, and branch worktrees.",
506
604
  "Configure auth before starting pibo-web; Better Auth requires baseURL, secret, Google OAuth values, and allowed emails.",
507
605
  "Docker is only required for developer-host compute workers; user-host installs can ignore Docker warnings.",
508
606
  "Swap is not created automatically; for developer hosts, provision swap at the OS level and verify it with `--min-swap-gb`.",
@@ -541,9 +639,327 @@ function printDoctorStatus(status) {
541
639
  for (const item of status.recommendations)
542
640
  console.log(`- ${item}`);
543
641
  }
642
+ function printInstallationPlan(plan) {
643
+ console.log(`${plan.profile} (profile version ${plan.profileVersion}): ${plan.summary}`);
644
+ console.log("\nComponents:");
645
+ for (const component of plan.components)
646
+ console.log(`- ${component.name}: ${component.version} — ${component.description}`);
647
+ console.log("\nHost packages:");
648
+ for (const packageName of plan.hostPackages)
649
+ console.log(`- ${packageName}`);
650
+ console.log("\nDownloads:");
651
+ if (plan.downloads.length === 0)
652
+ console.log("- none");
653
+ for (const download of plan.downloads)
654
+ console.log(`- ${download.name} ${download.version}: ${Object.keys(download.urls).join(", ")} (SHA-256 pinned)`);
655
+ console.log("\nServices and listeners:");
656
+ for (const service of plan.services)
657
+ console.log(`- ${service.name}: ${service.bind}, user=${service.user}, public=${service.public ? "yes" : "no"}`);
658
+ console.log("\nOwned files:");
659
+ for (const file of plan.files)
660
+ console.log(`- ${file.path}: ${file.purpose}`);
661
+ console.log("\nApply actions:");
662
+ for (const action of plan.actions)
663
+ console.log(`- ${action.id}: ${action.description}${action.restartEffect ? ` (${action.restartEffect})` : ""}`);
664
+ console.log("\nSecurity boundaries:");
665
+ for (const item of plan.securityBoundaries)
666
+ console.log(`- ${item}`);
667
+ if (plan.warnings.length > 0) {
668
+ console.log("\nWarnings:");
669
+ for (const warning of plan.warnings)
670
+ console.log(`- ${warning}`);
671
+ }
672
+ console.log(`\nRepair: ${plan.repairCommand}`);
673
+ }
674
+ function runInstallationAction(action) {
675
+ console.log(`[pibo setup] ${action.id}: ${action.description}`);
676
+ execFileSync("sh", ["-lc", action.command], { stdio: "inherit" });
677
+ }
678
+ function installationActionIsComplete(action) {
679
+ if (!action.checkCommand)
680
+ return true;
681
+ try {
682
+ execFileSync("sh", ["-lc", action.checkCommand], { stdio: "ignore" });
683
+ return true;
684
+ }
685
+ catch {
686
+ return false;
687
+ }
688
+ }
689
+ function removeComponentsNoLongerPlanned(manifest, plan) {
690
+ const installed = new Set(manifest.components.map((component) => component.name));
691
+ const planned = new Set(plan.components.map((component) => component.name));
692
+ if (installed.has("vscode-web") && !planned.has("vscode-web"))
693
+ execFileSync("systemctl", ["disable", "--now", "pibo-code-server"], { stdio: "inherit" });
694
+ if (installed.has("browser-tools") && !planned.has("browser-tools")) {
695
+ for (const path of [join(manifest.piboHome, "tools/browser-use/.venv"), join(manifest.piboHome, "tools/agent-browser/node")])
696
+ rmSync(path, { recursive: true, force: true });
697
+ }
698
+ }
699
+ function stopInstalledServices(manifest) {
700
+ const componentNames = new Set(manifest.components.map((component) => component.name));
701
+ if (componentNames.has("vscode-web"))
702
+ execFileSync("systemctl", ["disable", "--now", "pibo-code-server"], { stdio: "inherit" });
703
+ execFileSync("systemctl", ["disable", "--now", "pibo-web"], { stdio: "inherit" });
704
+ if (componentNames.has("browser-tools")) {
705
+ for (const path of [join(manifest.piboHome, "tools/browser-use/.venv"), join(manifest.piboHome, "tools/agent-browser/node")])
706
+ rmSync(path, { recursive: true, force: true });
707
+ }
708
+ }
709
+ function requireConfirmedApply(options) {
710
+ if (!options.apply)
711
+ return;
712
+ if (options.yes !== true)
713
+ throw new Error("Refusing to modify the host without --yes");
714
+ if (typeof process.getuid === "function" && process.getuid() !== 0)
715
+ throw new Error("Host installation requires root; rerun with sudo");
716
+ if (process.platform !== "linux")
717
+ throw new Error("Host installation apply mode currently supports Linux only");
718
+ }
719
+ function validatePublicAuthBoundary(plan) {
720
+ if (!plan.domain)
721
+ return;
722
+ const configPath = join(plan.piboHome, "config.json");
723
+ if (!existsSync(configPath))
724
+ throw new Error(`Public setup requires Better Auth configuration at ${configPath}; run pibo config set before --apply`);
725
+ const config = loadPiboConfig(configPath);
726
+ if (getPiboConfigValue(config, "auth.mode") === "local")
727
+ throw new Error("Refusing to expose a local-auth gateway through a public installation domain; configure Better Auth first");
728
+ const failures = authConfigChecks(plan.piboHome).filter((check) => check.status === "fail");
729
+ if (failures.length > 0)
730
+ throw new Error(`Public setup requires complete Better Auth configuration: ${failures.map((check) => check.name).join(", ")}`);
731
+ const baseUrlValue = getPiboConfigValue(config, "auth.baseURL");
732
+ let baseUrl;
733
+ try {
734
+ baseUrl = new URL(String(baseUrlValue));
735
+ }
736
+ catch {
737
+ throw new Error("Public setup requires auth.baseURL to be a valid HTTPS URL");
738
+ }
739
+ if (baseUrl.protocol !== "https:" || baseUrl.hostname !== plan.domain)
740
+ throw new Error(`Public setup domain ${plan.domain} must match the HTTPS auth.baseURL hostname`);
741
+ }
742
+ function emitInstallationPlan(plan, options) {
743
+ if (options.json)
744
+ printJson(plan);
745
+ else
746
+ printInstallationPlan(plan);
747
+ }
748
+ function applyOrStageInstallation(plan, options) {
749
+ if (options.apply && options.writeTo)
750
+ throw new Error("Use either --apply or --write-to, not both");
751
+ if (!options.apply && !options.writeTo) {
752
+ emitInstallationPlan(plan, options);
753
+ return;
754
+ }
755
+ if (options.json)
756
+ throw new Error("--json cannot be combined with --apply or --write-to");
757
+ requireConfirmedApply(options);
758
+ if (options.apply)
759
+ validatePublicAuthBoundary(plan);
760
+ validateInstallationPlanTargets(plan, { root: options.writeTo });
761
+ const previousManifest = options.apply ? readInstallationManifest(installationManifestPath(plan.piboHome)) : undefined;
762
+ if (previousManifest)
763
+ removeComponentsNoLongerPlanned(previousManifest, plan);
764
+ const bootstrapActions = plan.actions.filter((action) => action.id === "host-packages");
765
+ const bootstrapCompleted = [];
766
+ if (options.apply && !existsSync(installationManifestPath(plan.piboHome))) {
767
+ for (const action of bootstrapActions) {
768
+ runInstallationAction(action);
769
+ bootstrapCompleted.push(action.id);
770
+ }
771
+ }
772
+ const result = materializeInstallationPlan(plan, { root: options.writeTo });
773
+ if (options.apply && bootstrapCompleted.length > 0) {
774
+ runPendingInstallationActions({ ...plan, actions: bootstrapActions }, result.manifestPath, () => undefined);
775
+ }
776
+ const actions = options.apply ? runPendingInstallationActions(plan, result.manifestPath, runInstallationAction, { isComplete: installationActionIsComplete }) : undefined;
777
+ console.log(`${options.apply ? "Installed" : "Staged"} ${plan.profile} profile.`);
778
+ console.log(`Manifest: ${result.manifestPath}`);
779
+ console.log(`Written: ${result.written.length}; unchanged: ${result.unchanged.length}; removed obsolete: ${result.removed.length}; preserved modified: ${result.preserved.length}`);
780
+ if (actions)
781
+ console.log(`Actions completed: ${bootstrapCompleted.length + actions.completed.length}; skipped: ${actions.skipped.filter((id) => !bootstrapCompleted.includes(id)).length}`);
782
+ }
783
+ function currentPiboVersion() {
784
+ const packageJson = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
785
+ if (typeof packageJson.version !== "string" || packageJson.version.length === 0)
786
+ throw new Error("Unable to read Pibo package version");
787
+ return packageJson.version;
788
+ }
789
+ function installationPlanOptions(options) {
790
+ return createInstallationPlan({ ...options, piboVersion: currentPiboVersion() });
791
+ }
792
+ function currentPiboCommand() {
793
+ const entrypoint = process.argv[1] ? resolve(process.argv[1]) : "/usr/bin/pibo";
794
+ return entrypoint === "/usr/bin/pibo" ? entrypoint : `${process.execPath} ${entrypoint}`;
795
+ }
796
+ async function chooseInteractiveInstallationProfile(profile) {
797
+ if (profile)
798
+ return profile;
799
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
800
+ throw new Error("Non-interactive setup requires an explicit --profile batteries-included or --profile vanilla");
801
+ console.log("Batteries Included is recommended. It adds a loopback-only IDE, managed browser tooling, and curated MCP defaults behind Pibo authentication.");
802
+ console.log("Vanilla installs only the gateway and Chat Web host requirements.");
803
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
804
+ try {
805
+ const answer = (await prompt.question("Profile [batteries-included]: ")).trim();
806
+ return answer === "" ? "batteries-included" : parseInstallationProfile(answer);
807
+ }
808
+ finally {
809
+ prompt.close();
810
+ }
811
+ }
544
812
  export async function runSetupCli(argv = process.argv) {
545
813
  const program = new Command();
546
- program.name("pibo setup").description("Plan Pibo host installation and developer upgrades").helpOption("-h, --help", "Display help for command").showHelpAfterError();
814
+ program.name("pibo setup").description("Plan, install, inspect, upgrade, and remove supported Pibo host profiles").helpOption("-h, --help", "Display help for command").showHelpAfterError();
815
+ program
816
+ .command("plan")
817
+ .description("Inspect an installation profile without changing the host")
818
+ .option("--profile <profile>", "batteries-included (recommended) or vanilla", parseInstallationProfile, "batteries-included")
819
+ .option("--pibo-home <path>", "PIBO_HOME", "/root/.pibo")
820
+ .option("--workspace-root <path>", "Workspace directory exposed to VS Code Web", "/srv/pibo-workspaces")
821
+ .option("--pibo-command <command>", "Pibo command used by generated services and lifecycle actions", currentPiboCommand())
822
+ .option("--domain <domain>", "Public HTTPS domain", parseInstallationDomain)
823
+ .option("--json", "Print machine-readable JSON")
824
+ .action((options) => {
825
+ emitInstallationPlan(installationPlanOptions(options), options);
826
+ });
827
+ program
828
+ .command("install")
829
+ .description("Install a supported profile; Batteries Included is the recommended default")
830
+ .option("--profile <profile>", "batteries-included (recommended) or vanilla; required outside an interactive terminal", parseInstallationProfile)
831
+ .option("--pibo-home <path>", "PIBO_HOME", "/root/.pibo")
832
+ .option("--workspace-root <path>", "Workspace directory exposed to VS Code Web", "/srv/pibo-workspaces")
833
+ .option("--pibo-command <command>", "Pibo command used by generated services and lifecycle actions", currentPiboCommand())
834
+ .option("--domain <domain>", "Public HTTPS domain", parseInstallationDomain)
835
+ .option("--json", "Print machine-readable plan JSON without applying")
836
+ .option("--write-to <dir>", "Stage the complete owned filesystem tree under a review directory")
837
+ .option("--apply", "Apply generated files, packages, services, and safe restarts to this Linux host")
838
+ .option("--yes", "Confirm privileged host changes")
839
+ .action(async (options) => {
840
+ const profile = await chooseInteractiveInstallationProfile(options.profile);
841
+ applyOrStageInstallation(installationPlanOptions({ ...options, profile }), options);
842
+ });
843
+ program
844
+ .command("status")
845
+ .description("Inspect the installed profile, components, versions, and owned-file drift")
846
+ .option("--pibo-home <path>", "PIBO_HOME", getPiboHome())
847
+ .option("--root <dir>", "Inspect a staged filesystem root")
848
+ .option("--json", "Print machine-readable JSON")
849
+ .action((options) => {
850
+ const status = inspectInstallation(options);
851
+ if (options.json)
852
+ printJson(status);
853
+ else {
854
+ console.log(status.installed ? `${status.profile} profile version ${status.profileVersion}` : "No setup-managed profile installed");
855
+ for (const component of status.components)
856
+ console.log(`- ${component.name}: ${component.version}`);
857
+ console.log("Checks:");
858
+ for (const check of status.checks)
859
+ console.log(`- ${check.status.toUpperCase()} ${check.name}: ${check.detail}`);
860
+ console.log(`Repair: ${status.repairCommand}`);
861
+ }
862
+ });
863
+ program
864
+ .command("upgrade")
865
+ .description("Reconcile the installed profile with the current pinned component catalog")
866
+ .option("--pibo-home <path>", "PIBO_HOME", getPiboHome())
867
+ .option("--root <dir>", "Reconcile a staged filesystem root")
868
+ .option("--apply", "Apply the upgrade to this Linux host")
869
+ .option("--yes", "Confirm privileged host changes")
870
+ .option("--json", "Print the upgrade plan as JSON without applying")
871
+ .action((options) => {
872
+ if (options.root && options.apply)
873
+ throw new Error("Use either --root for staging or --apply for the host, not both");
874
+ const manifestPath = options.root ? join(options.root, installationManifestPath(options.piboHome).replace(/^\/+/, "")) : installationManifestPath(options.piboHome);
875
+ const manifest = readInstallationManifest(manifestPath);
876
+ if (!manifest)
877
+ throw new Error(`No installation manifest found at ${manifestPath}`);
878
+ const plan = createInstallationPlan({ profile: manifest.profile, piboHome: manifest.piboHome, workspaceRoot: manifest.workspaceRoot, piboCommand: manifest.piboCommand, piboVersion: currentPiboVersion(), domain: manifest.domain, additionalComponents: manifest.components.map((component) => component.name) });
879
+ if (options.json)
880
+ emitInstallationPlan(plan, { json: true });
881
+ else
882
+ applyOrStageInstallation(plan, { apply: options.apply, yes: options.yes, writeTo: options.root });
883
+ });
884
+ const component = program.command("component").description("Manage optional setup components");
885
+ component
886
+ .command("add <name>")
887
+ .description("Add one component to an installed profile")
888
+ .option("--pibo-home <path>", "PIBO_HOME", getPiboHome())
889
+ .option("--root <dir>", "Update a staged filesystem root")
890
+ .option("--apply", "Apply component installation to this Linux host")
891
+ .option("--yes", "Confirm privileged host changes")
892
+ .option("--json", "Print the component-add plan as JSON without applying")
893
+ .action((name, options) => {
894
+ if (options.root && options.apply)
895
+ throw new Error("Use either --root for staging or --apply for the host, not both");
896
+ const manifestPath = options.root ? join(options.root, installationManifestPath(options.piboHome).replace(/^\/+/, "")) : installationManifestPath(options.piboHome);
897
+ const manifest = readInstallationManifest(manifestPath);
898
+ if (!manifest)
899
+ throw new Error(`No installation manifest found at ${manifestPath}`);
900
+ const additionalComponents = [...manifest.components.map((entry) => entry.name), parseInstallationComponent(name)];
901
+ const plan = createInstallationPlan({ profile: manifest.profile, piboHome: manifest.piboHome, workspaceRoot: manifest.workspaceRoot, piboCommand: manifest.piboCommand, piboVersion: currentPiboVersion(), domain: manifest.domain, additionalComponents });
902
+ if (options.json)
903
+ emitInstallationPlan(plan, { json: true });
904
+ else
905
+ applyOrStageInstallation(plan, { apply: options.apply, yes: options.yes, writeTo: options.root });
906
+ });
907
+ program
908
+ .command("uninstall")
909
+ .description("Remove unchanged setup-owned files while preserving Pibo data and workspaces")
910
+ .option("--pibo-home <path>", "PIBO_HOME", getPiboHome())
911
+ .option("--root <dir>", "Remove a staged installation")
912
+ .option("--apply", "Remove setup-owned host files")
913
+ .option("--yes", "Confirm removal")
914
+ .option("--json", "Print machine-readable JSON")
915
+ .action((options) => {
916
+ if (options.root && options.apply)
917
+ throw new Error("Use either --root for staging or --apply for the host, not both");
918
+ const manifestPath = options.root ? join(options.root, installationManifestPath(options.piboHome).replace(/^\/+/, "")) : installationManifestPath(options.piboHome);
919
+ const manifest = readInstallationManifest(manifestPath);
920
+ if (!manifest)
921
+ throw new Error(`No installation manifest found at ${manifestPath}`);
922
+ const uninstallPlan = {
923
+ profile: manifest.profile,
924
+ components: manifest.components,
925
+ services: ["pibo-web", ...(manifest.components.some((component) => component.name === "vscode-web") ? ["pibo-code-server"] : []), "caddy"],
926
+ ownedFiles: manifest.ownedFiles.map((file) => file.path),
927
+ preserves: [manifest.piboHome, manifest.workspaceRoot, "VS Code settings", "authenticated browser profiles", "sessions and product data"],
928
+ };
929
+ const execute = options.apply === true || (options.root !== undefined && options.yes === true);
930
+ if (!execute) {
931
+ if (options.json)
932
+ printJson(uninstallPlan);
933
+ else {
934
+ console.log(`Uninstall plan for ${manifest.profile}:`);
935
+ for (const path of uninstallPlan.ownedFiles)
936
+ console.log(`- remove owned file: ${path}`);
937
+ for (const path of uninstallPlan.preserves)
938
+ console.log(`- preserve: ${path}`);
939
+ console.log("Apply with --apply --yes.");
940
+ }
941
+ return;
942
+ }
943
+ requireConfirmedApply({ apply: options.apply, yes: options.yes });
944
+ if (options.apply)
945
+ stopInstalledServices(manifest);
946
+ const result = uninstallInstallation(options);
947
+ if (options.apply) {
948
+ execFileSync("systemctl", ["daemon-reload"], { stdio: "inherit" });
949
+ if (result.removed.includes("/etc/caddy/Caddyfile"))
950
+ execFileSync("systemctl", ["stop", "caddy"], { stdio: "inherit" });
951
+ else
952
+ execFileSync("systemctl", ["reload", "caddy"], { stdio: "inherit" });
953
+ }
954
+ if (options.json)
955
+ printJson(result);
956
+ else {
957
+ console.log(`Removed ${result.removed.length} setup-owned files.`);
958
+ for (const path of result.preserved)
959
+ console.log(`Preserved modified file: ${path}`);
960
+ console.log("Pibo Home data and workspaces were preserved.");
961
+ }
962
+ });
547
963
  program
548
964
  .command("user-host")
549
965
  .description("Plan a simple one-gateway Pibo host for normal users")