omegon 0.6.19 → 0.6.21

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.
@@ -317,6 +317,79 @@ const ociProvider: AuthProvider = {
317
317
  },
318
318
  };
319
319
 
320
+ const vaultProvider: AuthProvider = {
321
+ id: "vault",
322
+ name: "Vault",
323
+ cli: "vault",
324
+ tokenEnvVar: "VAULT_TOKEN",
325
+ refreshCommand: "vault login",
326
+
327
+ async check(pi, signal) {
328
+ // 1. Check CLI is installed
329
+ const which = await pi.exec("which", ["vault"], { signal, timeout: 3_000 });
330
+ if (which.code !== 0) {
331
+ return { provider: this.id, status: "missing", detail: "vault CLI not installed" };
332
+ }
333
+
334
+ // 2. Check VAULT_ADDR is configured — without it, no meaningful check is possible
335
+ const addr = process.env["VAULT_ADDR"];
336
+ if (!addr) {
337
+ return {
338
+ provider: this.id,
339
+ status: "none",
340
+ detail: "VAULT_ADDR not set",
341
+ refresh: this.refreshCommand,
342
+ secretHint: "VAULT_ADDR",
343
+ };
344
+ }
345
+
346
+ // 3. Run vault token lookup — read-only, returns token metadata (never the token itself)
347
+ // VAULT_TOKEN is read by the vault CLI from the environment; we never access it directly.
348
+ const result = await pi.exec("vault", ["token", "lookup", "-format=json"], { signal, timeout: 10_000 });
349
+
350
+ if (result.code === 0) {
351
+ try {
352
+ const data = JSON.parse(result.stdout.trim());
353
+ const tokenData = data?.data ?? {};
354
+
355
+ // Extract safe metadata — policies and expiry only, never the token value
356
+ const policies: string[] = tokenData.policies ?? [];
357
+ const displayName: string = tokenData.display_name ?? "";
358
+ const expireTime: string = tokenData.expire_time ?? "";
359
+
360
+ // Build a human-readable detail string
361
+ const parts: string[] = [];
362
+ if (displayName) parts.push(displayName);
363
+ if (policies.length > 0) parts.push(`policies: ${policies.filter(p => p !== "default").join(", ") || "default"}`);
364
+ if (expireTime) parts.push(`expires: ${expireTime.split("T")[0]}`);
365
+ else parts.push("no expiry");
366
+
367
+ return {
368
+ provider: this.id,
369
+ status: "ok",
370
+ detail: parts.join(" · ") || "authenticated",
371
+ refresh: this.refreshCommand,
372
+ };
373
+ } catch {
374
+ // JSON parse failed but command succeeded — still authenticated
375
+ return { provider: this.id, status: "ok", detail: "authenticated", refresh: this.refreshCommand };
376
+ }
377
+ }
378
+
379
+ // 4. Diagnose failure — truncate to 300 chars, never log token values
380
+ const output = (result.stdout + "\n" + result.stderr).trim();
381
+ const diag = diagnoseError(output);
382
+ return {
383
+ provider: this.id,
384
+ status: diag.status,
385
+ detail: `${addr} — ${diag.reason}`,
386
+ error: output.slice(0, 300),
387
+ refresh: this.refreshCommand,
388
+ secretHint: "VAULT_TOKEN",
389
+ };
390
+ },
391
+ };
392
+
320
393
  // ─── Provider Registry ───────────────────────────────────────────
321
394
 
322
395
  /** All providers, ordered by typical check priority. */
@@ -327,6 +400,7 @@ export const ALL_PROVIDERS: AuthProvider[] = [
327
400
  awsProvider,
328
401
  kubernetesProvider,
329
402
  ociProvider,
403
+ vaultProvider,
330
404
  ];
331
405
 
332
406
  export function findProvider(idOrName: string): AuthProvider | undefined {
@@ -31,6 +31,12 @@ export interface Dep {
31
31
  url?: string;
32
32
  /** Dep IDs that must be installed first */
33
33
  requires?: string[];
34
+ /**
35
+ * Optional preflight check. If it returns a string, that string is a
36
+ * blocking message shown to the operator explaining what they need to do
37
+ * manually before this dep can be installed. Return undefined if ready.
38
+ */
39
+ preflight?: () => string | undefined;
34
40
  }
35
41
 
36
42
  export interface InstallOption {
@@ -53,6 +59,12 @@ function ensureToolPaths(): void {
53
59
  "/nix/var/nix/profiles/default/bin",
54
60
  join(home, ".nix-profile", "bin"),
55
61
  join(home, ".cargo", "bin"),
62
+ // Linuxbrew
63
+ "/home/linuxbrew/.linuxbrew/bin",
64
+ join(home, ".linuxbrew", "bin"),
65
+ // macOS Homebrew
66
+ "/opt/homebrew/bin",
67
+ "/usr/local/bin",
56
68
  ];
57
69
  const current = process.env.PATH ?? "";
58
70
  const parts = current.split(":");
@@ -63,6 +75,52 @@ function ensureToolPaths(): void {
63
75
  }
64
76
  ensureToolPaths();
65
77
 
78
+ /** Detect ostree-based immutable Linux (Bazzite, Silverblue, Kinoite, Bluefin, etc.) */
79
+ function isOstree(): boolean {
80
+ if (process.platform !== "linux") return false;
81
+ try {
82
+ execSync("which rpm-ostree", { stdio: "ignore" });
83
+ return true;
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * On Fedora 42+ ostree systems, `/` is read-only by default (composefs).
91
+ * Nix needs to create `/nix` which requires `root.transient = true` in the
92
+ * ostree prepare-root config. Returns blocking instructions if not ready.
93
+ */
94
+ function checkOstreeReadyForNix(): string | undefined {
95
+ // If /nix already exists, we're good (previous install or already configured)
96
+ if (existsSync("/nix")) return undefined;
97
+
98
+ // Check if root.transient is enabled
99
+ try {
100
+ const conf = execSync("cat /etc/ostree/prepare-root.conf 2>/dev/null", { encoding: "utf-8" });
101
+ if (/transient\s*=\s*true/i.test(conf)) return undefined;
102
+ } catch { /* file doesn't exist */ }
103
+
104
+ return [
105
+ "⚠️ Your system has a read-only root filesystem (ostree/composefs).",
106
+ "Nix needs `/nix` to exist, which requires enabling root.transient.",
107
+ "",
108
+ "Run these commands in your terminal, then reboot and run /bootstrap again:",
109
+ "",
110
+ "```",
111
+ "sudo tee /etc/ostree/prepare-root.conf <<'EOL'",
112
+ "[composefs]",
113
+ "enabled = yes",
114
+ "[root]",
115
+ "transient = true",
116
+ "EOL",
117
+ "",
118
+ "sudo rpm-ostree initramfs-etc --track=/etc/ostree/prepare-root.conf",
119
+ "systemctl reboot",
120
+ "```",
121
+ ].join("\n");
122
+ }
123
+
66
124
  function hasCmd(cmd: string): boolean {
67
125
  try {
68
126
  execSync(`which ${cmd}`, { stdio: "ignore" });
@@ -105,10 +163,19 @@ export const DEPS: Dep[] = [
105
163
  tier: "core",
106
164
  check: () => hasCmd("nix"),
107
165
  install: [
108
- // --no-confirm: headless (stdin is closed)
109
- { platform: "any", cmd: "curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install --no-confirm" },
166
+ // Immutable ostree-based Linux (Bazzite, Silverblue, Bluefin, etc.)
167
+ // needs root.transient enabled and --persistence=/var/lib/nix so the
168
+ // nix store lives on a writable partition. The upstream installer uses
169
+ // the ostree planner automatically when it detects ostree.
170
+ { platform: "linux", cmd: isOstree()
171
+ ? "curl -sSfL https://install.determinate.systems/nix | sh -s -- install --no-confirm --persistence=/var/lib/nix"
172
+ : "curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install --no-confirm" },
173
+ { platform: "darwin", cmd: "curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install --no-confirm" },
110
174
  ],
111
175
  url: "https://zero-to-nix.com",
176
+ // On ostree systems, root.transient must be enabled first for /nix to be created.
177
+ // preflight returns instructions if the system isn't ready.
178
+ preflight: isOstree() ? checkOstreeReadyForNix : undefined,
112
179
  },
113
180
  {
114
181
  id: "ollama",
@@ -138,6 +205,19 @@ export const DEPS: Dep[] = [
138
205
  },
139
206
 
140
207
  // --- Recommended: common workflows ---
208
+ {
209
+ id: "vault",
210
+ name: "Vault CLI",
211
+ purpose: "HashiCorp Vault authentication status checking and secret management",
212
+ usedBy: ["01-auth"],
213
+ tier: "optional",
214
+ check: () => hasCmd("vault"),
215
+ requires: ["nix"],
216
+ install: [
217
+ { platform: "any", cmd: "nix profile install nixpkgs#vault" },
218
+ ],
219
+ url: "https://developer.hashicorp.com/vault/install",
220
+ },
141
221
  {
142
222
  id: "gh",
143
223
  name: "GitHub CLI",
@@ -1075,6 +1075,15 @@ async function installDeps(ctx: CommandContext, deps: DepStatus[]): Promise<void
1075
1075
  const { dep } = sorted[i];
1076
1076
  const step = `[${i + 1}/${total}]`;
1077
1077
 
1078
+ // Preflight check — some deps need manual system prep before install
1079
+ if (dep.preflight) {
1080
+ const blocker = dep.preflight();
1081
+ if (blocker) {
1082
+ ctx.ui.notify(`\n${step} 🛑 ${dep.name} — manual setup required:\n\n${blocker}`);
1083
+ continue;
1084
+ }
1085
+ }
1086
+
1078
1087
  // Check prerequisites — re-verify availability live (not from stale array)
1079
1088
  if (dep.requires?.length) {
1080
1089
  const unmet = dep.requires.filter((reqId) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omegon",
3
- "version": "0.6.19",
3
+ "version": "0.6.21",
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",