omegon 0.6.23 → 0.6.25

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.
@@ -62,7 +62,8 @@ export function diagnoseError(stderr: string): { status: AuthStatus; reason: str
62
62
  // Not logged in — check before invalid to avoid "not authenticated" matching "invalid"
63
63
  if (lower.includes("not logged") || lower.includes("no token") || lower.includes("not authenticated")
64
64
  || lower.includes("login required") || lower.includes("no credentials")
65
- || lower.includes("no valid credentials")) {
65
+ || lower.includes("no valid credentials")
66
+ || lower.includes("missing client token")) {
66
67
  return { status: "none", reason: "Not authenticated" };
67
68
  }
68
69
 
@@ -75,7 +76,7 @@ export function diagnoseError(stderr: string): { status: AuthStatus; reason: str
75
76
 
76
77
  // Forbidden (authenticated but insufficient permissions)
77
78
  if (/\b403\b/.test(lower) || lower.includes("insufficient scope")
78
- || lower.includes("access denied")) {
79
+ || lower.includes("access denied") || lower.includes("permission denied")) {
79
80
  return { status: "invalid", reason: `Authenticated but forbidden: ${extractErrorLine(stderr)}` };
80
81
  }
81
82
 
@@ -211,6 +211,20 @@ export const DEPS: Dep[] = [
211
211
  },
212
212
 
213
213
  // --- Recommended: common workflows ---
214
+ {
215
+ id: "vault",
216
+ name: "Vault CLI",
217
+ purpose: "HashiCorp Vault authentication status checking and secret management",
218
+ usedBy: ["01-auth"],
219
+ tier: "recommended",
220
+ check: () => hasCmd("vault"),
221
+ install: [
222
+ { platform: "darwin", cmd: "brew install hashicorp/tap/vault" },
223
+ { platform: "linux", cmd: "brew install hashicorp/tap/vault" },
224
+ { platform: "linux", cmd: "wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --yes --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg && echo \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main\" | sudo tee /etc/apt/sources.list.d/hashicorp.list && sudo apt update && sudo apt install -y vault" },
225
+ ],
226
+ url: "https://developer.hashicorp.com/vault/install",
227
+ },
214
228
  {
215
229
  id: "gh",
216
230
  name: "GitHub CLI",
@@ -333,20 +347,6 @@ export const DEPS: Dep[] = [
333
347
  { platform: "any", cmd: "brew install kubectl" },
334
348
  ],
335
349
  },
336
- {
337
- id: "vault",
338
- name: "Vault CLI",
339
- purpose: "HashiCorp Vault authentication status checking and secret management",
340
- usedBy: ["01-auth"],
341
- tier: "optional",
342
- check: () => hasCmd("vault"),
343
- requires: ["nix"],
344
- install: [
345
- { platform: "any", cmd: "nix profile install nixpkgs#vault" },
346
- { platform: "any", cmd: "brew install hashicorp/tap/vault" },
347
- ],
348
- url: "https://developer.hashicorp.com/vault/install",
349
- },
350
350
  ];
351
351
 
352
352
  export type DepStatus = { dep: Dep; available: boolean };
@@ -19,7 +19,7 @@
19
19
  * - Never auto-installs anything — always asks or requires explicit command
20
20
  */
21
21
 
22
- import { spawn } from "node:child_process";
22
+ import { spawn, spawnSync } from "node:child_process";
23
23
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
24
24
  import { dirname, join } from "node:path";
25
25
  import { fileURLToPath } from "node:url";
@@ -455,18 +455,45 @@ function restartOmegon(): never {
455
455
 
456
456
  const parts = [command, ...argvPrefix, ...userArgs].map(shellEscape);
457
457
  const script = join(tmpdir(), `omegon-restart-${process.pid}.sh`);
458
+ const oldPid = process.pid;
458
459
  writeFileSync(script, [
459
460
  "#!/bin/sh",
460
- // Wait for the old process to fully die and release the terminal
461
- "sleep 0.3",
462
- // Reset terminal to sane cooked state
461
+ // Trap signals so Ctrl+C works; ignore HUP so parent death doesn't kill us
462
+ "trap 'stty sane 2>/dev/null; exit 130' INT TERM",
463
+ "trap '' HUP",
464
+ // Wait for the old process to fully die (poll with timeout)
465
+ "_w=0",
466
+ `while kill -0 ${oldPid} 2>/dev/null; do`,
467
+ " sleep 0.1",
468
+ " _w=$((_w + 1))",
469
+ // Bail after ~5 seconds — proceed anyway
470
+ ' [ "$_w" -ge 50 ] && break',
471
+ "done",
472
+ // Extra grace period for fd/terminal release
473
+ "sleep 0.2",
474
+ // Reset terminal to sane cooked state (stty sane is sufficient;
475
+ // avoid printf '\\033c' which nukes scrollback history)
463
476
  "stty sane 2>/dev/null",
464
- "printf '\\033c'",
477
+ // Clean up this script
478
+ `rm -f "${script}"`,
465
479
  // Replace this shell with new omegon
466
480
  `exec ${parts.join(" ")}`,
467
481
  ].join("\n") + "\n", { mode: 0o755 });
468
482
 
469
- // Spawn the restart script detached it will outlive us
483
+ // Reset terminal to cooked mode BEFORE exiting so the restart script
484
+ // (and the user) aren't stuck with raw-mode terminal if something goes wrong.
485
+ try {
486
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
487
+ process.stdin.setRawMode(false);
488
+ }
489
+ // Also reset via stty — timeout guards against blocking on contested stdin
490
+ spawnSync("stty", ["sane"], { stdio: "inherit", timeout: 2000 });
491
+ } catch { /* best-effort */ }
492
+
493
+ // Spawn restart script detached (survives parent exit) but with SIGHUP
494
+ // ignored in the script so process-group teardown doesn't kill it.
495
+ // detached: true puts it in its own process group; the script's signal
496
+ // traps ensure Ctrl+C still works once the new omegon exec's.
470
497
  const child = spawn("sh", [script], {
471
498
  stdio: "inherit",
472
499
  detached: true,
@@ -774,7 +801,10 @@ async function interactiveSetup(pi: ExtensionAPI, ctx: CommandContext): Promise<
774
801
  const statuses = checkAll();
775
802
  const missing = statuses.filter((s) => !s.available);
776
803
 
777
- ctx.ui.notify(formatReport(statuses));
804
+ // Emit the dep report as a permanent warning-level message so it is never
805
+ // replaced by subsequent showStatus() calls (showWarning adds nodes to
806
+ // chatContainer without updating lastStatusText, breaking the deduplication chain).
807
+ ctx.ui.notify(formatReport(statuses), "warning");
778
808
 
779
809
  if (missing.length === 0 && !needsOperatorProfileSetup(getConfigRoot(ctx))) {
780
810
  markDone();
@@ -782,7 +812,7 @@ async function interactiveSetup(pi: ExtensionAPI, ctx: CommandContext): Promise<
782
812
  }
783
813
 
784
814
  if (!ctx.hasUI || !ctx.ui) {
785
- ctx.ui.notify("\nRun individual install commands above, or use `/bootstrap install` to install all core + recommended deps.");
815
+ ctx.ui.notify("\nRun individual install commands above, or use `/bootstrap install` to install all core + recommended deps.", "warning");
786
816
  await ensureOperatorProfile(pi, ctx);
787
817
  return;
788
818
  }
@@ -798,6 +828,7 @@ async function interactiveSetup(pi: ExtensionAPI, ctx: CommandContext): Promise<
798
828
  `${coreMissing.length} missing: ${names}`,
799
829
  );
800
830
  if (proceed) {
831
+ ctx.ui.notify(`Installing ${coreMissing.length} core dep${coreMissing.length > 1 ? "s" : ""}… (this may take a while)`, "info");
801
832
  await installDeps(ctx, coreMissing);
802
833
  }
803
834
  }
@@ -809,14 +840,19 @@ async function interactiveSetup(pi: ExtensionAPI, ctx: CommandContext): Promise<
809
840
  `${recMissing.length} missing: ${names}`,
810
841
  );
811
842
  if (proceed) {
843
+ ctx.ui.notify(`Installing ${recMissing.length} recommended dep${recMissing.length > 1 ? "s" : ""}… (this may take a while)`, "info");
812
844
  await installDeps(ctx, recMissing);
813
845
  }
814
846
  }
815
847
 
848
+ // Collect remaining summary lines and emit as a single notify at the end
849
+ // to avoid each line replacing the previous via showStatus() deduplication.
850
+ const summary: string[] = [];
851
+
816
852
  if (optMissing.length > 0) {
817
- ctx.ui.notify(
818
- `\n${optMissing.length} optional dep${optMissing.length > 1 ? "s" : ""} not installed: ${optMissing.map((s) => s.dep.name).join(", ")}.\n`
819
- + "Install individually when needed — see `/bootstrap status` for commands.",
853
+ summary.push(
854
+ `${optMissing.length} optional dep${optMissing.length > 1 ? "s" : ""} not installed: ${optMissing.map((s) => s.dep.name).join(", ")}.`
855
+ + "\nInstall individually when needed — see `/bootstrap status` for commands.",
820
856
  );
821
857
  }
822
858
 
@@ -826,13 +862,12 @@ async function interactiveSetup(pi: ExtensionAPI, ctx: CommandContext): Promise<
826
862
  (r: AuthResult) => r.status === "ok" && r.provider !== "local",
827
863
  );
828
864
  if (!hasAnyCloudKey) {
829
- ctx.ui.notify(
830
- "\n🔑 **No cloud API keys detected.**\n" +
865
+ summary.push(
866
+ "🔑 **No cloud API keys detected.**\n" +
831
867
  "Omegon needs at least one provider key to function. The fastest options:\n" +
832
868
  " • Anthropic: `/secrets configure ANTHROPIC_API_KEY` (get key at console.anthropic.com)\n" +
833
869
  " • OpenAI: `/secrets configure OPENAI_API_KEY` (get key at platform.openai.com)\n" +
834
- " • GitHub Copilot: `/login github` (requires Copilot subscription)\n",
835
- "warning"
870
+ " • GitHub Copilot: `/login github` (requires Copilot subscription)",
836
871
  );
837
872
  }
838
873
 
@@ -842,19 +877,21 @@ async function interactiveSetup(pi: ExtensionAPI, ctx: CommandContext): Promise<
842
877
  const stillMissing = recheck.filter((s) => !s.available && (s.dep.tier === "core" || s.dep.tier === "recommended"));
843
878
 
844
879
  if (stillMissing.length === 0 && hasAnyCloudKey) {
845
- ctx.ui.notify("\n🎉 Setup complete! All core and recommended dependencies are available.");
880
+ summary.push("🎉 Setup complete! All core and recommended dependencies are available.");
846
881
  markDone();
847
882
  } else if (stillMissing.length === 0) {
848
- ctx.ui.notify(
849
- "\n✅ Dependencies installed. Configure an API key (see above) to start using Omegon.",
850
- );
883
+ summary.push("✅ Dependencies installed. Configure an API key (see above) to start using Omegon.");
851
884
  markDone();
852
885
  } else {
853
- ctx.ui.notify(
854
- `\n⚠️ ${stillMissing.length} dep${stillMissing.length > 1 ? "s" : ""} still missing. `
886
+ summary.push(
887
+ `⚠️ ${stillMissing.length} dep${stillMissing.length > 1 ? "s" : ""} still missing. `
855
888
  + "Run `/bootstrap` again after installing manually.",
856
889
  );
857
890
  }
891
+
892
+ if (summary.length > 0) {
893
+ ctx.ui.notify(summary.join("\n\n"), "info");
894
+ }
858
895
  }
859
896
 
860
897
  async function installMissing(ctx: CommandContext, tiers: DepTier[]): Promise<void> {
@@ -951,9 +988,6 @@ function isSignificantLine(raw: string): boolean {
951
988
  * piped so output is captured and forwarded through pi's notification
952
989
  * system rather than fighting with the TUI renderer.
953
990
  *
954
- * A heartbeat tick fires every `heartbeatMs` so the operator knows the
955
- * process is still alive during long compilations (e.g. cargo build).
956
- *
957
991
  * The install commands come exclusively from the static `deps.ts`
958
992
  * registry and are never influenced by operator input.
959
993
  *
@@ -963,7 +997,6 @@ export function runAsync(
963
997
  cmd: string,
964
998
  onLine: (line: string) => void,
965
999
  timeoutMs: number = 600_000,
966
- heartbeatMs: number = 15_000,
967
1000
  ): Promise<number> {
968
1001
  return new Promise((resolve) => {
969
1002
  const env = {
@@ -987,23 +1020,15 @@ export function runAsync(
987
1020
 
988
1021
  let settled = false;
989
1022
  let sigkillTimer: ReturnType<typeof setTimeout> | undefined;
990
- let elapsedSec = 0;
991
1023
 
992
1024
  const settle = (code: number) => {
993
1025
  if (settled) return;
994
1026
  settled = true;
995
1027
  clearTimeout(timer);
996
- clearInterval(heartbeat);
997
1028
  clearTimeout(sigkillTimer);
998
1029
  resolve(code);
999
1030
  };
1000
1031
 
1001
- // Heartbeat — fires every heartbeatMs while the process is running.
1002
- const heartbeat = setInterval(() => {
1003
- elapsedSec += heartbeatMs / 1000;
1004
- onLine(` ⏳ still running… (${elapsedSec}s)`);
1005
- }, heartbeatMs);
1006
-
1007
1032
  // Forward captured lines from both streams.
1008
1033
  const attachStream = (stream: NodeJS.ReadableStream | null) => {
1009
1034
  if (!stream) return;
@@ -1084,6 +1109,15 @@ async function installDeps(ctx: CommandContext, deps: DepStatus[]): Promise<void
1084
1109
  }
1085
1110
  }
1086
1111
 
1112
+ // Stream install output: consecutive notify("info") calls update the
1113
+ // same text node in place via showStatus() deduplication. A final
1114
+ // notify("warning") pins the completed output permanently.
1115
+ let output = `${step} 📦 Installing ${dep.name}…`;
1116
+ const stream = (line: string) => {
1117
+ output += `\n${line}`;
1118
+ ctx.ui.notify(output, "info");
1119
+ };
1120
+
1087
1121
  // Check prerequisites — re-verify availability live (not from stale array)
1088
1122
  if (dep.requires?.length) {
1089
1123
  const unmet = dep.requires.filter((reqId) => {
@@ -1091,29 +1125,20 @@ async function installDeps(ctx: CommandContext, deps: DepStatus[]): Promise<void
1091
1125
  return reqDep ? !reqDep.check() : false;
1092
1126
  });
1093
1127
  if (unmet.length > 0) {
1094
- ctx.ui.notify(`\n${step} ⚠️ Skipping ${dep.name} — requires ${unmet.join(", ")} (not yet available)`);
1128
+ ctx.ui.notify(`${step} ⚠️ Skipping ${dep.name} — requires ${unmet.join(", ")} (not yet available)`, "info");
1095
1129
  continue;
1096
1130
  }
1097
1131
  }
1098
1132
 
1099
1133
  const cmd = bestInstallCmd(dep);
1100
1134
  if (!cmd) {
1101
- ctx.ui.notify(`\n${step} ⚠️ No install command available for ${dep.name} on this platform`);
1135
+ ctx.ui.notify(`${step} ⚠️ No install command available for ${dep.name} on this platform`, "info");
1102
1136
  continue;
1103
1137
  }
1104
1138
 
1105
- ctx.ui.notify(`\n${step} 📦 Installing ${dep.name}…\n → \`${cmd}\``);
1106
-
1107
- // Collect output lines show progress inline but also keep them
1108
- // so we can dump a readable block on failure.
1109
- const outputLines: string[] = [];
1110
- const exitCode = await runAsync(
1111
- cmd,
1112
- (line) => {
1113
- outputLines.push(line);
1114
- ctx.ui.notify(line);
1115
- },
1116
- );
1139
+ stream(` → \`${cmd}\``);
1140
+
1141
+ const exitCode = await runAsync(cmd, stream);
1117
1142
 
1118
1143
  // Patch PATH immediately after installing bootstrapping deps so the rest
1119
1144
  // of the install sequence can find them without a new shell.
@@ -1125,25 +1150,20 @@ async function installDeps(ctx: CommandContext, deps: DepStatus[]): Promise<void
1125
1150
  }
1126
1151
 
1127
1152
  if (exitCode === 0 && dep.check()) {
1128
- ctx.ui.notify(`${step} ✅ ${dep.name} installed successfully`);
1153
+ output += `\n${step} ✅ ${dep.name} installed successfully`;
1129
1154
  } else if (exitCode === 124) {
1130
- ctx.ui.notify(`${step} ❌ ${dep.name} install timed out (10 min limit)`);
1155
+ output += `\n${step} ❌ ${dep.name} install timed out (10 min limit)`;
1156
+ } else if (exitCode === 0) {
1157
+ output += `\n${step} ⚠️ Command succeeded but ${dep.name} not found on PATH — you may need to open a new shell.`;
1131
1158
  } else {
1132
- // Dump the last N lines of output so the operator can see what went wrong
1133
- const tail = outputLines.slice(-20).join("\n");
1134
- const status = exitCode === 0
1135
- ? `Command succeeded but ${dep.name} not found on PATH`
1136
- : `Failed to install ${dep.name} (exit ${exitCode})`;
1137
- const block = [
1138
- `${step} ❌ ${status}`,
1139
- "",
1140
- "Output (last 20 lines):",
1141
- "```",
1142
- tail,
1143
- "```",
1144
- ...(dep.url ? [`Manual install: ${dep.url}`] : []),
1145
- ].join("\n");
1146
- ctx.ui.notify(block);
1159
+ output += `\n${step} Failed to install ${dep.name} (exit ${exitCode})`;
1160
+ const hints = dep.install.filter((o) => o.cmd !== cmd);
1161
+ if (hints.length > 0) output += `\n Alternative: \`${hints[0]!.cmd}\``;
1162
+ if (dep.url) output += `\n Manual install: ${dep.url}`;
1147
1163
  }
1164
+
1165
+ // Pin the completed output permanently as warning so subsequent
1166
+ // showStatus() calls cannot overwrite it.
1167
+ ctx.ui.notify(output, "warning");
1148
1168
  }
1149
1169
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omegon",
3
- "version": "0.6.23",
3
+ "version": "0.6.25",
4
4
  "description": "Omegon — an opinionated distribution of pi (by Mario Zechner) with extensions for lifecycle management, memory, orchestration, and visualization",
5
5
  "bin": {
6
6
  "omegon": "bin/omegon.mjs",