omegon 0.10.2 → 0.10.3
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/bin/omegon.mjs +61 -15
- package/extensions/bootstrap/index.ts +12 -95
- package/package.json +1 -1
package/bin/omegon.mjs
CHANGED
|
@@ -135,20 +135,31 @@ function purgeSelfReferentialPackages() {
|
|
|
135
135
|
}
|
|
136
136
|
purgeSelfReferentialPackages();
|
|
137
137
|
|
|
138
|
-
process.argv = injectBundledResourceArgs(process.argv);
|
|
139
|
-
|
|
140
138
|
// ---------------------------------------------------------------------------
|
|
141
|
-
//
|
|
142
|
-
//
|
|
139
|
+
// CLI launch — subprocess with restart-loop support.
|
|
140
|
+
//
|
|
141
|
+
// Instead of importing the CLI directly (which makes restart impossible since
|
|
142
|
+
// Node can't replace its own process image), we spawn it as a child process.
|
|
143
|
+
// If the child exits with code 75 (EX_TEMPFAIL), we re-spawn — this is the
|
|
144
|
+
// restart signal from /update and /restart commands.
|
|
145
|
+
//
|
|
146
|
+
// This keeps the wrapper as the foreground process group leader throughout,
|
|
147
|
+
// so the re-spawned CLI always owns the terminal and can receive input.
|
|
143
148
|
// ---------------------------------------------------------------------------
|
|
149
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
150
|
+
|
|
151
|
+
const RESTART_EXIT_CODE = 75;
|
|
152
|
+
|
|
153
|
+
const cliArgs = injectBundledResourceArgs(process.argv).slice(2);
|
|
154
|
+
|
|
144
155
|
const isInteractive = process.stdout.isTTY &&
|
|
145
156
|
!process.argv.includes("-p") &&
|
|
146
157
|
!process.argv.includes("--print") &&
|
|
147
158
|
!process.argv.includes("--help") &&
|
|
148
159
|
!process.argv.includes("-h");
|
|
149
160
|
|
|
150
|
-
|
|
151
|
-
if (isInteractive)
|
|
161
|
+
function showPreImportSpinner() {
|
|
162
|
+
if (!isInteractive) return undefined;
|
|
152
163
|
const PRIMARY = "\x1b[38;2;42;180;200m";
|
|
153
164
|
const DIM = "\x1b[38;2;64;88;112m";
|
|
154
165
|
const RST = "\x1b[0m";
|
|
@@ -157,9 +168,8 @@ if (isInteractive) {
|
|
|
157
168
|
const spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
158
169
|
let frame = 0;
|
|
159
170
|
|
|
160
|
-
// Safety net: restore cursor on any exit path (crash, SIGTERM, etc.)
|
|
161
171
|
const restoreCursor = () => { try { process.stdout.write(SHOW_CURSOR); } catch {} };
|
|
162
|
-
process.on(
|
|
172
|
+
process.on("exit", restoreCursor);
|
|
163
173
|
|
|
164
174
|
process.stdout.write(HIDE_CURSOR);
|
|
165
175
|
process.stdout.write(`\n ${PRIMARY}omegon${RST} ${DIM}loading…${RST}`);
|
|
@@ -170,16 +180,52 @@ if (isInteractive) {
|
|
|
170
180
|
frame++;
|
|
171
181
|
}, 80);
|
|
172
182
|
|
|
173
|
-
|
|
183
|
+
return () => {
|
|
174
184
|
clearInterval(spinTimer);
|
|
175
|
-
process.removeListener(
|
|
176
|
-
// Clear the loading line and restore cursor
|
|
185
|
+
process.removeListener("exit", restoreCursor);
|
|
177
186
|
process.stdout.write(`\r\x1b[2K${SHOW_CURSOR}`);
|
|
178
187
|
};
|
|
179
188
|
}
|
|
180
189
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
190
|
+
function launchCli() {
|
|
191
|
+
return new Promise((resolve) => {
|
|
192
|
+
const cleanup = showPreImportSpinner();
|
|
193
|
+
|
|
194
|
+
const child = nodeSpawn(process.execPath, [cli, ...cliArgs], {
|
|
195
|
+
stdio: "inherit",
|
|
196
|
+
env: process.env,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Let the child handle SIGINT (Ctrl+C) — the wrapper ignores it.
|
|
200
|
+
const ignoreInt = () => {};
|
|
201
|
+
process.on("SIGINT", ignoreInt);
|
|
202
|
+
// Forward SIGTERM so graceful shutdown works.
|
|
203
|
+
const fwdTerm = () => child.kill("SIGTERM");
|
|
204
|
+
process.on("SIGTERM", fwdTerm);
|
|
205
|
+
|
|
206
|
+
// Clean up spinner once the child's TUI takes over. The child will
|
|
207
|
+
// clear the screen on startup anyway, but a brief delay ensures the
|
|
208
|
+
// spinner doesn't flicker.
|
|
209
|
+
if (cleanup) {
|
|
210
|
+
setTimeout(() => cleanup(), 200);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
child.on("exit", (code, signal) => {
|
|
214
|
+
process.removeListener("SIGINT", ignoreInt);
|
|
215
|
+
process.removeListener("SIGTERM", fwdTerm);
|
|
216
|
+
if (signal) {
|
|
217
|
+
// Re-raise the signal so the wrapper exits with the right status
|
|
218
|
+
process.kill(process.pid, signal);
|
|
219
|
+
}
|
|
220
|
+
resolve(code ?? 1);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
185
223
|
}
|
|
224
|
+
|
|
225
|
+
// Main loop — restart on exit code 75
|
|
226
|
+
let exitCode;
|
|
227
|
+
do {
|
|
228
|
+
exitCode = await launchCli();
|
|
229
|
+
} while (exitCode === RESTART_EXIT_CODE);
|
|
230
|
+
|
|
231
|
+
process.exit(exitCode);
|
|
@@ -25,7 +25,7 @@ import { dirname, join } from "node:path";
|
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
import { homedir, tmpdir } from "node:os";
|
|
27
27
|
import type { ExtensionAPI } from "@styrene-lab/pi-coding-agent";
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
import { checkAllProviders, type AuthResult } from "../01-auth/auth.ts";
|
|
30
30
|
import { loadPiConfig } from "../lib/model-preferences.ts";
|
|
31
31
|
import {
|
|
@@ -436,101 +436,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
436
436
|
* exit and release the terminal), then exec's the new Omegon. This avoids
|
|
437
437
|
* two TUI processes fighting over the same terminal simultaneously.
|
|
438
438
|
*/
|
|
439
|
+
/**
|
|
440
|
+
* Restart Omegon by exiting with code 75.
|
|
441
|
+
*
|
|
442
|
+
* The bin/omegon.mjs wrapper runs the CLI in a subprocess loop. When it sees
|
|
443
|
+
* exit code 75 (EX_TEMPFAIL), it re-spawns a fresh CLI process. Because the
|
|
444
|
+
* wrapper stays as the foreground process group leader throughout, the new
|
|
445
|
+
* CLI always owns the terminal and can receive input — no detached spawn,
|
|
446
|
+
* no competing with the shell for stdin.
|
|
447
|
+
*/
|
|
439
448
|
function restartOmegon(): never {
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
!a.startsWith("--extensions-dir=") &&
|
|
443
|
-
!a.startsWith("--themes-dir=") &&
|
|
444
|
-
!a.startsWith("--skills-dir=") &&
|
|
445
|
-
!a.startsWith("--prompts-dir=") &&
|
|
446
|
-
!a.startsWith("--extension=") && !a.startsWith("--extension ") &&
|
|
447
|
-
!a.startsWith("--skill=") && !a.startsWith("--skill ") &&
|
|
448
|
-
!a.startsWith("--prompt-template=") && !a.startsWith("--prompt-template ") &&
|
|
449
|
-
!a.startsWith("--theme=") && !a.startsWith("--theme ") &&
|
|
450
|
-
!a.startsWith("--no-skills") &&
|
|
451
|
-
!a.startsWith("--no-prompt-templates") &&
|
|
452
|
-
!a.startsWith("--no-themes") &&
|
|
453
|
-
!a.startsWith("--no-extensions")
|
|
454
|
-
);
|
|
455
|
-
|
|
456
|
-
const parts = [command, ...argvPrefix, ...userArgs].map(shellEscape);
|
|
457
|
-
const script = join(tmpdir(), `omegon-restart-${process.pid}.sh`);
|
|
458
|
-
const oldPid = process.pid;
|
|
459
|
-
writeFileSync(script, [
|
|
460
|
-
"#!/bin/sh",
|
|
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.3",
|
|
474
|
-
// Hard terminal reset via /dev/tty — avoids stdout buffering that
|
|
475
|
-
// bleeds into the exec'd process. RIS clears all protocol state
|
|
476
|
-
// (kitty keyboard protocol, bracketed paste, mouse tracking, etc.).
|
|
477
|
-
// Do NOT use `reset` here — it outputs terminfo init strings to
|
|
478
|
-
// stdout which the new TUI interprets as input (causing stray
|
|
479
|
-
// characters and double renders).
|
|
480
|
-
"printf '\\033c' >/dev/tty 2>/dev/null",
|
|
481
|
-
"stty sane 2>/dev/null",
|
|
482
|
-
// Clean up this script
|
|
483
|
-
`rm -f "${script}"`,
|
|
484
|
-
// Replace this shell with new omegon
|
|
485
|
-
`exec ${parts.join(" ")}`,
|
|
486
|
-
].join("\n") + "\n", { mode: 0o755 });
|
|
487
|
-
|
|
488
|
-
// Reset terminal to cooked mode BEFORE exiting so the restart script
|
|
489
|
-
// (and the user) aren't stuck with raw-mode terminal if something goes wrong.
|
|
490
|
-
try {
|
|
491
|
-
// RIS (Reset to Initial State) — the only reliable way to ensure ALL
|
|
492
|
-
// terminal protocol state is cleared. Write directly to the TTY fd
|
|
493
|
-
// to bypass pi's TUI layer which intercepts process.stdout and would
|
|
494
|
-
// mangle the escape sequence into visible ANSI garbage.
|
|
495
|
-
const { openSync, writeSync, closeSync } = require("fs") as typeof import("fs");
|
|
496
|
-
let ttyFd = -1;
|
|
497
|
-
try {
|
|
498
|
-
ttyFd = openSync("/dev/tty", "w");
|
|
499
|
-
writeSync(ttyFd, "\x1bc");
|
|
500
|
-
closeSync(ttyFd);
|
|
501
|
-
} catch {
|
|
502
|
-
// Fallback if /dev/tty isn't available (shouldn't happen on macOS/Linux)
|
|
503
|
-
process.stdout.write("\x1bc");
|
|
504
|
-
}
|
|
505
|
-
// Pause stdin to prevent buffered input from being re-interpreted
|
|
506
|
-
// after raw mode is disabled (prevents Ctrl+D from closing parent shell).
|
|
507
|
-
process.stdin.pause();
|
|
508
|
-
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
509
|
-
process.stdin.setRawMode(false);
|
|
510
|
-
}
|
|
511
|
-
// stty sane resets line discipline to known-good state.
|
|
512
|
-
spawnSync("stty", ["sane"], { stdio: ["inherit", "ignore", "ignore"], timeout: 2000 });
|
|
513
|
-
} catch { /* best-effort */ }
|
|
514
|
-
|
|
515
|
-
// Spawn restart script detached (survives parent exit) but with SIGHUP
|
|
516
|
-
// ignored in the script so process-group teardown doesn't kill it.
|
|
517
|
-
// detached: true puts it in its own process group; the script's signal
|
|
518
|
-
// traps ensure Ctrl+C still works once the new omegon exec's.
|
|
519
|
-
const child = spawn("sh", [script], {
|
|
520
|
-
stdio: "inherit",
|
|
521
|
-
detached: true,
|
|
522
|
-
env: process.env,
|
|
523
|
-
});
|
|
524
|
-
child.unref();
|
|
525
|
-
|
|
526
|
-
// Clean exit — the script waits for us to die before starting new omegon
|
|
527
|
-
process.exit(0);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/** Escape a string for POSIX shell */
|
|
531
|
-
function shellEscape(s: string): string {
|
|
532
|
-
if (/^[a-zA-Z0-9_./:=-]+$/.test(s)) return s;
|
|
533
|
-
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
449
|
+
const RESTART_EXIT_CODE = 75;
|
|
450
|
+
process.exit(RESTART_EXIT_CODE);
|
|
534
451
|
}
|
|
535
452
|
|
|
536
453
|
/** Run a command, collect stdout+stderr, resolve with exit code. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omegon",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
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",
|