muonroi-cli 1.7.0 → 1.7.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.
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/index.js +88 -44
- package/dist/src/storage/sessions.d.ts +6 -0
- package/dist/src/storage/sessions.js +11 -4
- package/dist/src/ui/app.d.ts +1 -1
- package/dist/src/ui/app.js +27 -7
- package/dist/src/ui/modals/session-picker-modal.js +14 -9
- package/dist/src/ui/modals/update-modal.js +2 -1
- package/dist/src/ui/types.d.ts +7 -0
- package/dist/src/ui/utils/relaunch.d.ts +17 -0
- package/dist/src/ui/utils/relaunch.js +27 -3
- package/dist/src/ui/utils/relaunch.test.js +50 -0
- package/dist/src/utils/install-manager.d.ts +29 -0
- package/dist/src/utils/install-manager.js +106 -0
- package/dist/src/utils/update-checker.js +2 -2
- package/dist/src/utils/update-checker.test.js +17 -4
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const PACKAGE_VERSION = "1.7.
|
|
1
|
+
export declare const PACKAGE_VERSION = "1.7.2";
|
|
2
2
|
export declare const PACKAGE_DESCRIPTION = "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// AUTO-GENERATED by scripts/sync-version.cjs. DO NOT EDIT BY HAND.
|
|
2
2
|
// Sourced from package.json at build time so it survives bun --compile bundling.
|
|
3
|
-
export const PACKAGE_VERSION = "1.7.
|
|
3
|
+
export const PACKAGE_VERSION = "1.7.2";
|
|
4
4
|
export const PACKAGE_DESCRIPTION = "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.";
|
|
5
5
|
//# sourceMappingURL=version.js.map
|
package/dist/src/index.js
CHANGED
|
@@ -391,6 +391,7 @@ async function startInteractive(apiKey, baseURL, model, maxToolRounds, batchApi,
|
|
|
391
391
|
const { createRoot } = await import("@opentui/react");
|
|
392
392
|
const { createElement } = await import("react");
|
|
393
393
|
const { App } = await import("./ui/app.js");
|
|
394
|
+
const { relaunchWithSession } = await import("./ui/utils/relaunch.js");
|
|
394
395
|
const renderer = await createCliRenderer({
|
|
395
396
|
exitOnCtrlC: false,
|
|
396
397
|
// We manage SIGINT ourselves (orchestrator abort → agent cleanup → renderer destroy).
|
|
@@ -441,64 +442,106 @@ async function startInteractive(apiKey, baseURL, model, maxToolRounds, batchApi,
|
|
|
441
442
|
process.env.TERM_PROGRAM === "WezTerm" ||
|
|
442
443
|
process.env.TERM_PROGRAM === "wezterm" ||
|
|
443
444
|
process.env.MUONROI_FORCE_SHELL_HOLD === "1";
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
/* fall back to async */
|
|
460
|
-
try {
|
|
461
|
-
process.stdout.write(seq);
|
|
462
|
-
}
|
|
463
|
-
catch {
|
|
464
|
-
/* noop */
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
};
|
|
445
|
+
// Restore the terminal to a clean main-screen state: undo Kitty keyboard,
|
|
446
|
+
// raw mode, mouse tracking, bracketed paste, and alt-screen. Shared by the
|
|
447
|
+
// /exit path AND the session-resume relaunch — a relaunch that skips this
|
|
448
|
+
// leaves the child (and, once the parent exits, the shell) with a terminal
|
|
449
|
+
// still in mouse-tracking/alt-screen mode, so stray escape bytes get parsed
|
|
450
|
+
// by the shell as a bogus command ("'…' is not recognized … operable
|
|
451
|
+
// program or batch file").
|
|
452
|
+
function restoreTerminalForHandoff() {
|
|
453
|
+
// Restore terminal state from JS before the native destroyRenderer runs
|
|
454
|
+
restoreTerminalSync();
|
|
455
|
+
renderer.destroy();
|
|
456
|
+
// Use synchronous writes to stdout's underlying fd so escape codes flush
|
|
457
|
+
// before the child inherits stdin — async process.stdout.write() can buffer
|
|
458
|
+
// past the spawn() boundary, leaving the terminal in mouse-tracking mode.
|
|
459
|
+
const writeSync = (seq) => {
|
|
468
460
|
try {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
475
|
-
catch {
|
|
476
|
-
/* noop */
|
|
477
|
-
}
|
|
478
|
-
}
|
|
461
|
+
const fs = require("node:fs");
|
|
462
|
+
fs.writeSync(1, seq);
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
/* fall back to async */
|
|
479
466
|
try {
|
|
480
|
-
process.
|
|
467
|
+
process.stdout.write(seq);
|
|
481
468
|
}
|
|
482
469
|
catch {
|
|
483
470
|
/* noop */
|
|
484
471
|
}
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
try {
|
|
475
|
+
// 1. Take stdin out of raw mode + pause it so no buffered keystrokes
|
|
476
|
+
// (or in-flight mouse-event bytes) hit the child.
|
|
477
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
485
478
|
try {
|
|
486
|
-
process.stdin.
|
|
479
|
+
process.stdin.setRawMode(false);
|
|
487
480
|
}
|
|
488
481
|
catch {
|
|
489
482
|
/* noop */
|
|
490
483
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
process.stdin.pause();
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
/* noop */
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
process.stdin.unref?.();
|
|
498
493
|
}
|
|
499
494
|
catch {
|
|
500
|
-
|
|
495
|
+
/* noop */
|
|
501
496
|
}
|
|
497
|
+
// 2. Disable extended-coords first (1006/1015/1005), then the basic
|
|
498
|
+
// tracking modes (1003→1002→1000). Wrong order leaves the terminal
|
|
499
|
+
// emitting SGR-formatted events after the base modes are off.
|
|
500
|
+
writeSync("\x1B[?1006l\x1B[?1015l\x1B[?1005l");
|
|
501
|
+
writeSync("\x1B[?1003l\x1B[?1002l\x1B[?1000l");
|
|
502
|
+
writeSync("\x1B[?2004l\x1B[?25h"); // bracketed paste off, cursor on
|
|
503
|
+
writeSync("\x1B[?1049l\x1B[0m\x1B[!p"); // exit alt-screen, reset SGR, soft reset
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
// best-effort
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Resume a different session by restarting the CLI bound to --session <id>.
|
|
510
|
+
// Routes through the SAME teardown as /exit, then supervises the child so the
|
|
511
|
+
// shell never reclaims the foreground mid-restart (which corrupted the
|
|
512
|
+
// terminal and dropped the user back to a broken prompt).
|
|
513
|
+
const onRelaunch = (sessionId) => {
|
|
514
|
+
void agent.cleanup().finally(() => {
|
|
515
|
+
restoreTerminalForHandoff();
|
|
516
|
+
// Let the terminal process the restore sequences before the child takes
|
|
517
|
+
// over stdin (mirrors the /exit shell-hold timing).
|
|
518
|
+
setTimeout(() => {
|
|
519
|
+
try {
|
|
520
|
+
relaunchWithSession(sessionId, { supervise: true });
|
|
521
|
+
}
|
|
522
|
+
catch (err) {
|
|
523
|
+
console.error(`[relaunch] failed: ${err?.message ?? err}`);
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
}, 80);
|
|
527
|
+
});
|
|
528
|
+
};
|
|
529
|
+
const onExit = () => {
|
|
530
|
+
void agent.cleanup().finally(() => {
|
|
531
|
+
restoreTerminalForHandoff();
|
|
532
|
+
const writeSync = (seq) => {
|
|
533
|
+
try {
|
|
534
|
+
require("node:fs").writeSync(1, seq);
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
try {
|
|
538
|
+
process.stdout.write(seq);
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
/* noop */
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
};
|
|
502
545
|
// WezTerm (and similar single-pane terminals) closes the window when
|
|
503
546
|
// the foreground process exits. To keep the pane usable after `/exit`,
|
|
504
547
|
// we hand control over to an interactive shell instead of exiting.
|
|
@@ -547,6 +590,7 @@ async function startInteractive(apiKey, baseURL, model, maxToolRounds, batchApi,
|
|
|
547
590
|
},
|
|
548
591
|
initialMessage,
|
|
549
592
|
onExit,
|
|
593
|
+
onRelaunch,
|
|
550
594
|
}));
|
|
551
595
|
}
|
|
552
596
|
async function runHeadless(prompt, apiKey, baseURL, model, maxToolRounds, batchApi, sandboxMode, sandboxSettings, format, session, permissionMode = "safe", councilAutoAnswer) {
|
|
@@ -8,6 +8,12 @@ export declare class SessionStore {
|
|
|
8
8
|
/**
|
|
9
9
|
* List recent sessions in this workspace (most recently updated first).
|
|
10
10
|
* Used by the `/sessions` slash command for resume picking.
|
|
11
|
+
*
|
|
12
|
+
* Only sessions that actually have a persisted message are returned. Every
|
|
13
|
+
* CLI launch WITHOUT `--session` opens a fresh empty session, and test /
|
|
14
|
+
* harness runs leave behind title-only stubs — without this filter the picker
|
|
15
|
+
* fills with empty rows that resume to a blank screen, so a user picking one
|
|
16
|
+
* thinks resume is broken.
|
|
11
17
|
*/
|
|
12
18
|
listRecentSessions(limit?: number): SessionInfo[];
|
|
13
19
|
getLatestSession(): SessionInfo | null;
|
|
@@ -64,14 +64,21 @@ export class SessionStore {
|
|
|
64
64
|
/**
|
|
65
65
|
* List recent sessions in this workspace (most recently updated first).
|
|
66
66
|
* Used by the `/sessions` slash command for resume picking.
|
|
67
|
+
*
|
|
68
|
+
* Only sessions that actually have a persisted message are returned. Every
|
|
69
|
+
* CLI launch WITHOUT `--session` opens a fresh empty session, and test /
|
|
70
|
+
* harness runs leave behind title-only stubs — without this filter the picker
|
|
71
|
+
* fills with empty rows that resume to a blank screen, so a user picking one
|
|
72
|
+
* thinks resume is broken.
|
|
67
73
|
*/
|
|
68
74
|
listRecentSessions(limit = 20) {
|
|
69
75
|
const rows = getDatabase()
|
|
70
76
|
.prepare(`
|
|
71
|
-
SELECT id, workspace_id, title, model, mode, cwd_at_start, cwd_last, status, created_at, updated_at
|
|
72
|
-
FROM sessions
|
|
73
|
-
WHERE workspace_id = ?
|
|
74
|
-
|
|
77
|
+
SELECT s.id, s.workspace_id, s.title, s.model, s.mode, s.cwd_at_start, s.cwd_last, s.status, s.created_at, s.updated_at
|
|
78
|
+
FROM sessions s
|
|
79
|
+
WHERE s.workspace_id = ?
|
|
80
|
+
AND EXISTS (SELECT 1 FROM messages m WHERE m.session_id = s.id)
|
|
81
|
+
ORDER BY s.updated_at DESC
|
|
75
82
|
LIMIT ?
|
|
76
83
|
`)
|
|
77
84
|
.all(this.workspace.id, limit);
|
package/dist/src/ui/app.d.ts
CHANGED
|
@@ -16,6 +16,6 @@ import "./slash/export.js";
|
|
|
16
16
|
import "./slash/status.js";
|
|
17
17
|
import type { AppProps } from "./types.js";
|
|
18
18
|
export type { AppStartupConfig } from "./types.js";
|
|
19
|
-
export declare function App({ agent, startupConfig, initialMessage, onExit }: AppProps): import("react").ReactNode;
|
|
19
|
+
export declare function App({ agent, startupConfig, initialMessage, onExit, onRelaunch }: AppProps): import("react").ReactNode;
|
|
20
20
|
export type { McpRunInfo } from "./components/message-view.js";
|
|
21
21
|
export { computeMcpRunInfo } from "./components/message-view.js";
|
package/dist/src/ui/app.js
CHANGED
|
@@ -408,7 +408,7 @@ ${prompt}`;
|
|
|
408
408
|
// user cannot enable a provider we are not actively maintaining UX for.
|
|
409
409
|
// Hoisted to module-level so React useEffect deps stay stable across renders.
|
|
410
410
|
const SPLASH_PROVIDERS = ["deepseek", "siliconflow"];
|
|
411
|
-
export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
411
|
+
export function App({ agent, startupConfig, initialMessage, onExit, onRelaunch }) {
|
|
412
412
|
const t = dark;
|
|
413
413
|
const renderer = useRenderer();
|
|
414
414
|
// Set initial status bar values synchronously before first render
|
|
@@ -3929,9 +3929,15 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
|
3929
3929
|
case "update":
|
|
3930
3930
|
setIsUpdating(true);
|
|
3931
3931
|
setUpdateOutput(null);
|
|
3932
|
+
// Always echo into the message log — the home-screen `updateOutput`
|
|
3933
|
+
// banner only renders on the splash screen, so an /update run inside an
|
|
3934
|
+
// active chat would otherwise produce no visible output at all.
|
|
3935
|
+
setMessages((prev) => [...prev, buildAssistantEntry("⟳ Checking for updates…")]);
|
|
3932
3936
|
runUpdate(startupConfig.version).then((result) => {
|
|
3933
3937
|
setIsUpdating(false);
|
|
3934
|
-
|
|
3938
|
+
const text = result.success ? result.output : `Update failed: ${result.output}`;
|
|
3939
|
+
setUpdateOutput(text);
|
|
3940
|
+
setMessages((prev) => [...prev, buildAssistantEntry(text)]);
|
|
3935
3941
|
});
|
|
3936
3942
|
break;
|
|
3937
3943
|
case "clear":
|
|
@@ -4830,9 +4836,14 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
|
4830
4836
|
if (key.name === "return") {
|
|
4831
4837
|
setIsUpdating(true);
|
|
4832
4838
|
setShowUpdateModal(false);
|
|
4839
|
+
// Echo into the message log too — same reason as the /update command:
|
|
4840
|
+
// the updateOutput banner only renders on the home/splash screen.
|
|
4841
|
+
setMessages((prev) => [...prev, buildAssistantEntry("⟳ Checking for updates…")]);
|
|
4833
4842
|
runUpdate(startupConfig.version).then((result) => {
|
|
4834
4843
|
setIsUpdating(false);
|
|
4835
|
-
|
|
4844
|
+
const text = result.success ? result.output : `Update failed: ${result.output}`;
|
|
4845
|
+
setUpdateOutput(text);
|
|
4846
|
+
setMessages((prev) => [...prev, buildAssistantEntry(text)]);
|
|
4836
4847
|
});
|
|
4837
4848
|
return;
|
|
4838
4849
|
}
|
|
@@ -5228,12 +5239,21 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
|
5228
5239
|
}
|
|
5229
5240
|
// Close the modal first so the toast renders before the spawn.
|
|
5230
5241
|
setShowSessionPicker(false);
|
|
5231
|
-
pushToast("info", `Resuming session ${picked.id
|
|
5232
|
-
// Defer to the next tick so OpenTUI flushes the toast frame
|
|
5233
|
-
//
|
|
5242
|
+
pushToast("info", `Resuming session ${picked.id}… restarting CLI`);
|
|
5243
|
+
// Defer to the next tick so OpenTUI flushes the toast frame, then hand
|
|
5244
|
+
// off to onRelaunch — which tears down the renderer + restores the
|
|
5245
|
+
// terminal (raw mode / mouse tracking / alt-screen) and supervises the
|
|
5246
|
+
// child. Doing the spawn here directly (the old relaunchWithSession
|
|
5247
|
+
// call) skipped that teardown and dropped the user to a corrupted
|
|
5248
|
+
// shell prompt. Fall back to the legacy path only if no handler wired.
|
|
5234
5249
|
setTimeout(() => {
|
|
5235
5250
|
try {
|
|
5236
|
-
|
|
5251
|
+
if (onRelaunch) {
|
|
5252
|
+
onRelaunch(picked.id);
|
|
5253
|
+
}
|
|
5254
|
+
else {
|
|
5255
|
+
relaunchWithSession(picked.id);
|
|
5256
|
+
}
|
|
5237
5257
|
}
|
|
5238
5258
|
catch (err) {
|
|
5239
5259
|
console.error(`[session-picker] relaunch failed: ${err?.message ?? err}`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { Semantic } from "@muonroi/agent-harness-opentui";
|
|
2
3
|
import { bottomAlignedModalTop } from "../utils/modal.js";
|
|
3
4
|
/**
|
|
4
5
|
* Recent-sessions picker. Opened by `/sessions` or `/session`. Selecting a
|
|
@@ -14,15 +15,19 @@ export function SessionPickerModal({ t, sessions, focusIndex, width, height, })
|
|
|
14
15
|
const panelHeight = Math.min(contentHeight, maxH);
|
|
15
16
|
const top = bottomAlignedModalTop(height, panelHeight);
|
|
16
17
|
const overlayBg = "#000000cc";
|
|
17
|
-
return (_jsx("box", { position: "absolute", left: 0, top: 0, width: width, height: height, alignItems: "center", paddingTop: top, backgroundColor: overlayBg, children: _jsxs("box", { width: panelWidth, height: panelHeight, backgroundColor: t.backgroundPanel, paddingTop: 1, paddingBottom: 1, flexDirection: "column", children: [_jsxs("box", { flexShrink: 0, flexDirection: "row", justifyContent: "space-between", paddingLeft: 2, paddingRight: 2, children: [_jsx("text", { fg: t.primary, children: _jsx("b", { children: "Resume session" }) }), _jsx("text", { fg: t.textMuted, children: "esc" })] }), _jsx("scrollbox", { flexGrow: 1, minHeight: 0, children: sessions.length === 0 ? (_jsx("box", { paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("text", { fg: t.textMuted, children: "No prior sessions in this workspace." }) })) : (sessions.map((s, idx) => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
return (_jsx(Semantic, { id: "session-picker", role: "dialog", name: "Resume session", isModal: true, children: _jsx("box", { position: "absolute", left: 0, top: 0, width: width, height: height, alignItems: "center", paddingTop: top, backgroundColor: overlayBg, children: _jsxs("box", { width: panelWidth, height: panelHeight, backgroundColor: t.backgroundPanel, paddingTop: 1, paddingBottom: 1, flexDirection: "column", children: [_jsxs("box", { flexShrink: 0, flexDirection: "row", justifyContent: "space-between", paddingLeft: 2, paddingRight: 2, children: [_jsx("text", { fg: t.primary, children: _jsx("b", { children: "Resume session" }) }), _jsx("text", { fg: t.textMuted, children: "esc" })] }), _jsx("scrollbox", { flexGrow: 1, minHeight: 0, children: sessions.length === 0 ? (_jsx("box", { paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("text", { fg: t.textMuted, children: "No prior sessions in this workspace." }) })) : (sessions.map((s, idx) => {
|
|
19
|
+
const focused = idx === focusIndex;
|
|
20
|
+
const ts = formatTimestamp(s.updatedAt);
|
|
21
|
+
const titleRaw = s.title?.trim() || "(untitled)";
|
|
22
|
+
const titleMax = Math.max(8, panelWidth - 38);
|
|
23
|
+
const title = titleRaw.length > titleMax ? `${titleRaw.slice(0, titleMax - 1)}…` : titleRaw;
|
|
24
|
+
// Show the FULL session id (not slice(-8)). The resumed-session
|
|
25
|
+
// header displays the full id, so a truncated id here made users
|
|
26
|
+
// think they had resumed a "different" session when it was the
|
|
27
|
+
// same one shown in two formats.
|
|
28
|
+
const idLabel = s.id;
|
|
29
|
+
return (_jsx(Semantic, { id: `session-item-${idx}`, role: "listitem", name: `${title} ${idLabel}`, value: s.id, selected: focused || undefined, children: _jsxs("box", { backgroundColor: focused ? t.selectedBg : undefined, paddingLeft: 2, paddingRight: 2, width: "100%", flexDirection: "row", justifyContent: "space-between", children: [_jsx("text", { fg: focused ? t.selected : t.text, children: `${ts} ${title}` }), _jsx("text", { fg: focused ? t.primary : t.textMuted, children: `${s.model} ${idLabel}` })] }) }, s.id));
|
|
30
|
+
})) }), _jsx("box", { flexShrink: 0, paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("text", { fg: t.textMuted, children: "↑↓ navigate · enter resume (restarts CLI) · esc cancel" }) })] }) }) }));
|
|
26
31
|
}
|
|
27
32
|
/**
|
|
28
33
|
* Compact MM-DD HH:MM timestamp for the picker rows. Trades the year for
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { Semantic } from "@muonroi/agent-harness-opentui";
|
|
2
3
|
import { bottomAlignedModalTop } from "../utils/modal.js";
|
|
3
4
|
export function UpdateModal({ t, width, height, currentVersion, latestVersion, }) {
|
|
4
5
|
const overlayBg = "#000000cc";
|
|
5
6
|
const panelWidth = Math.min(60, width - 6);
|
|
6
7
|
const panelHeight = 9;
|
|
7
8
|
const top = bottomAlignedModalTop(height, panelHeight);
|
|
8
|
-
return (_jsx("box", { position: "absolute", left: 0, top: 0, width: width, height: height, alignItems: "center", paddingTop: top, backgroundColor: overlayBg, children: _jsxs("box", { width: panelWidth, height: panelHeight, backgroundColor: t.backgroundPanel, paddingTop: 1, paddingBottom: 1, flexDirection: "column", children: [_jsxs("box", { flexShrink: 0, flexDirection: "row", justifyContent: "space-between", paddingLeft: 2, paddingRight: 2, children: [_jsx("text", { fg: "#f59e0b", children: _jsx("b", { children: "Update Available" }) }), _jsx("text", { fg: t.textMuted, children: "esc to dismiss" })] }), _jsx("box", { flexShrink: 0, paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsxs("text", { fg: t.text, children: ["A new version of muonroi-cli is available: ", _jsxs("span", { style: { fg: t.textMuted }, children: ["v", currentVersion] }), " → ", _jsxs("span", { style: { fg: "#22c55e" }, children: ["v", latestVersion] })] }) }), _jsx("box", { flexShrink: 0, paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("text", { fg: t.textMuted, children: "Press enter to update now, or esc to dismiss" }) })] }) }));
|
|
9
|
+
return (_jsx(Semantic, { id: "update-modal", role: "dialog", name: "Update Available", isModal: true, children: _jsx("box", { position: "absolute", left: 0, top: 0, width: width, height: height, alignItems: "center", paddingTop: top, backgroundColor: overlayBg, children: _jsxs("box", { width: panelWidth, height: panelHeight, backgroundColor: t.backgroundPanel, paddingTop: 1, paddingBottom: 1, flexDirection: "column", children: [_jsxs("box", { flexShrink: 0, flexDirection: "row", justifyContent: "space-between", paddingLeft: 2, paddingRight: 2, children: [_jsx("text", { fg: "#f59e0b", children: _jsx("b", { children: "Update Available" }) }), _jsx("text", { fg: t.textMuted, children: "esc to dismiss" })] }), _jsx("box", { flexShrink: 0, paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsxs("text", { fg: t.text, children: ["A new version of muonroi-cli is available: ", _jsxs("span", { style: { fg: t.textMuted }, children: ["v", currentVersion] }), " → ", _jsxs("span", { style: { fg: "#22c55e" }, children: ["v", latestVersion] })] }) }), _jsx("box", { flexShrink: 0, paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("text", { fg: t.textMuted, children: "Press enter to update now, or esc to dismiss" }) })] }) }) }));
|
|
9
10
|
}
|
|
10
11
|
//# sourceMappingURL=update-modal.js.map
|
package/dist/src/ui/types.d.ts
CHANGED
|
@@ -69,6 +69,13 @@ export interface AppProps {
|
|
|
69
69
|
startupConfig: AppStartupConfig;
|
|
70
70
|
initialMessage?: string;
|
|
71
71
|
onExit?: () => void;
|
|
72
|
+
/**
|
|
73
|
+
* Restart the CLI bound to a different session id (used by the /sessions
|
|
74
|
+
* picker). Routes through the same terminal teardown as onExit and supervises
|
|
75
|
+
* the child process, so resuming never strands the user at a corrupted shell
|
|
76
|
+
* prompt.
|
|
77
|
+
*/
|
|
78
|
+
onRelaunch?: (sessionId: string) => void;
|
|
72
79
|
}
|
|
73
80
|
export interface ActiveTurnState {
|
|
74
81
|
kind: "local" | "telegram";
|
|
@@ -27,6 +27,23 @@ export interface RelaunchOptions {
|
|
|
27
27
|
onExit?: (code: number) => void;
|
|
28
28
|
/** Injected spawn for tests. Defaults to the real node:child_process spawn. */
|
|
29
29
|
spawnFn?: typeof spawn;
|
|
30
|
+
/**
|
|
31
|
+
* Run immediately before the spawn — used to tear down the current TUI
|
|
32
|
+
* (renderer.destroy, restore raw mode / mouse tracking / alt-screen) so the
|
|
33
|
+
* child inherits a CLEAN terminal. Without this the child (and the shell, if
|
|
34
|
+
* the parent exits) inherit a terminal still in mouse-tracking/alt-screen
|
|
35
|
+
* mode, and stray escape bytes get parsed by the shell as a command
|
|
36
|
+
* ("'…' is not recognized as an internal or external command").
|
|
37
|
+
*/
|
|
38
|
+
beforeSpawn?: () => void;
|
|
39
|
+
/**
|
|
40
|
+
* When true, the parent stays alive and supervises the child: it exits with
|
|
41
|
+
* the child's exit code instead of exiting the instant the child spawns.
|
|
42
|
+
* This keeps any intermediate launcher (a `bun run` wrapper, the bun bin
|
|
43
|
+
* shim) alive so the shell does NOT regain the terminal foreground while the
|
|
44
|
+
* child is running. Required for a correct in-terminal restart on Windows.
|
|
45
|
+
*/
|
|
46
|
+
supervise?: boolean;
|
|
30
47
|
}
|
|
31
48
|
/**
|
|
32
49
|
* Spawn a fresh CLI process bound to {sessionId} and exit the current one.
|
|
@@ -26,16 +26,22 @@ export function sanitizeArgvForResume(args, sessionId) {
|
|
|
26
26
|
const out = [];
|
|
27
27
|
for (let i = 0; i < args.length; i++) {
|
|
28
28
|
const a = args[i];
|
|
29
|
-
if (a === "-s" || a === "--session") {
|
|
29
|
+
if (a === "-s" || a === "--session" || a === "--mock-llm") {
|
|
30
30
|
// skip the flag AND its value (if present and not another flag)
|
|
31
31
|
const next = args[i + 1];
|
|
32
32
|
if (next !== undefined && !next.startsWith("-"))
|
|
33
33
|
i++;
|
|
34
34
|
continue;
|
|
35
35
|
}
|
|
36
|
-
if (a.startsWith("--session=")) {
|
|
36
|
+
if (a.startsWith("--session=") || a.startsWith("--mock-llm=")) {
|
|
37
37
|
continue; // skip the combined form
|
|
38
38
|
}
|
|
39
|
+
// Transient launch-mode flags must NOT survive a resume — re-entering
|
|
40
|
+
// agent-mode (named-pipe transport) on a user-driven resume strands the
|
|
41
|
+
// child with no harness server. Drop them.
|
|
42
|
+
if (a === "--agent-mode") {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
39
45
|
out.push(a);
|
|
40
46
|
}
|
|
41
47
|
out.push("--session", sessionId);
|
|
@@ -60,12 +66,30 @@ export function relaunchWithSession(sessionId, opts = {}) {
|
|
|
60
66
|
const exit = opts.onExit ?? ((code) => process.exit(code));
|
|
61
67
|
const spawnImpl = opts.spawnFn ?? spawn;
|
|
62
68
|
const args = sanitizeArgvForResume(argv.slice(1), sessionId);
|
|
69
|
+
// Tear down the current TUI and restore the terminal BEFORE spawning so the
|
|
70
|
+
// child inherits a clean TTY (no raw mode / mouse tracking / alt-screen).
|
|
71
|
+
if (opts.beforeSpawn) {
|
|
72
|
+
try {
|
|
73
|
+
opts.beforeSpawn();
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
console.error(`[relaunch] beforeSpawn teardown failed: ${err?.message ?? err}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
63
79
|
const child = spawnImpl(exec, args, { stdio: "inherit", detached: false });
|
|
64
80
|
child.once("error", (err) => {
|
|
65
81
|
console.error(`[relaunch] spawn failed: ${err?.message ?? err}`);
|
|
66
82
|
exit(1);
|
|
67
83
|
});
|
|
68
|
-
|
|
84
|
+
if (opts.supervise) {
|
|
85
|
+
// Stay alive until the child exits so any intermediate launcher (bun run
|
|
86
|
+
// wrapper / bin shim) keeps the terminal foreground for the child instead
|
|
87
|
+
// of letting the shell reclaim it mid-restart.
|
|
88
|
+
child.once("exit", (code) => exit(code ?? 0));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Legacy fast-handoff: exit the instant the child spawns. Correct only when
|
|
92
|
+
// there is no intervening launcher waiting on this process.
|
|
69
93
|
child.once("spawn", () => exit(0));
|
|
70
94
|
}
|
|
71
95
|
//# sourceMappingURL=relaunch.js.map
|
|
@@ -40,6 +40,24 @@ describe("sanitizeArgvForResume", () => {
|
|
|
40
40
|
expect(() => sanitizeArgvForResume([], "")).toThrow(/sessionId is required/);
|
|
41
41
|
expect(() => sanitizeArgvForResume([], " ")).toThrow(/sessionId is required/);
|
|
42
42
|
});
|
|
43
|
+
it("strips transient launch-mode flags (--agent-mode) so resume does not re-enter agent-mode", () => {
|
|
44
|
+
expect(sanitizeArgvForResume(["--agent-mode", "-m", "grok-build-0.1"], "new-id")).toEqual([
|
|
45
|
+
"-m",
|
|
46
|
+
"grok-build-0.1",
|
|
47
|
+
"--session",
|
|
48
|
+
"new-id",
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
it("strips `--mock-llm <dir>` and the combined form", () => {
|
|
52
|
+
expect(sanitizeArgvForResume(["--mock-llm", "/fix/dir", "-y"], "new-id")).toEqual(["-y", "--session", "new-id"]);
|
|
53
|
+
expect(sanitizeArgvForResume(["--mock-llm=/fix/dir", "-y"], "new-id")).toEqual(["-y", "--session", "new-id"]);
|
|
54
|
+
});
|
|
55
|
+
it("strips agent-mode + mock-llm together (harness-launched resume → clean user CLI)", () => {
|
|
56
|
+
expect(sanitizeArgvForResume(["--agent-mode", "--mock-llm", "/fx", "--session", "old"], "new")).toEqual([
|
|
57
|
+
"--session",
|
|
58
|
+
"new",
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
43
61
|
});
|
|
44
62
|
describe("relaunchWithSession", () => {
|
|
45
63
|
it("spawns the same executable with the sanitized argv + session id, then exits 0", () => {
|
|
@@ -72,6 +90,38 @@ describe("relaunchWithSession", () => {
|
|
|
72
90
|
expect(errMock).toHaveBeenCalled();
|
|
73
91
|
errMock.mockRestore();
|
|
74
92
|
});
|
|
93
|
+
it("runs beforeSpawn teardown before spawning", () => {
|
|
94
|
+
const calls = [];
|
|
95
|
+
const child = new EventEmitter();
|
|
96
|
+
const spawnMock = vi.fn(() => {
|
|
97
|
+
calls.push("spawn");
|
|
98
|
+
return child;
|
|
99
|
+
});
|
|
100
|
+
relaunchWithSession("sess", {
|
|
101
|
+
argv: ["/bin/bun", "src/index.ts"],
|
|
102
|
+
onExit: () => { },
|
|
103
|
+
spawnFn: spawnMock,
|
|
104
|
+
beforeSpawn: () => calls.push("teardown"),
|
|
105
|
+
});
|
|
106
|
+
expect(calls).toEqual(["teardown", "spawn"]);
|
|
107
|
+
});
|
|
108
|
+
it("supervise: stays alive until the child exits, then propagates the exit code", () => {
|
|
109
|
+
const exitMock = vi.fn();
|
|
110
|
+
const child = new EventEmitter();
|
|
111
|
+
const spawnMock = vi.fn(() => child);
|
|
112
|
+
relaunchWithSession("sess", {
|
|
113
|
+
argv: ["/bin/bun", "src/index.ts"],
|
|
114
|
+
onExit: exitMock,
|
|
115
|
+
spawnFn: spawnMock,
|
|
116
|
+
supervise: true,
|
|
117
|
+
});
|
|
118
|
+
// Does NOT exit on spawn when supervising.
|
|
119
|
+
child.emit("spawn");
|
|
120
|
+
expect(exitMock).not.toHaveBeenCalled();
|
|
121
|
+
// Exits with the child's code when the child finally exits.
|
|
122
|
+
child.emit("exit", 3);
|
|
123
|
+
expect(exitMock).toHaveBeenCalledWith(3);
|
|
124
|
+
});
|
|
75
125
|
it("throws when argv[0] is missing (cannot relaunch without an executable)", () => {
|
|
76
126
|
expect(() => relaunchWithSession("sess", {
|
|
77
127
|
argv: [],
|
|
@@ -51,6 +51,35 @@ export declare function saveScriptInstallMetadata(metadata: ScriptInstallMetadat
|
|
|
51
51
|
export declare function getScriptInstallContext(homeDir?: string): ScriptInstallContext | null;
|
|
52
52
|
export declare function fetchLatestReleaseVersion(): Promise<string | null>;
|
|
53
53
|
export declare function parseChecksumsFile(contents: string): Map<string, string>;
|
|
54
|
+
/**
|
|
55
|
+
* How this muonroi-cli was installed. Drives which update path the built-in
|
|
56
|
+
* `/update` flow takes:
|
|
57
|
+
* - "script" → install.sh-managed; runScriptManagedUpdate replaces the binary.
|
|
58
|
+
* - "bun-global" → `bun add -g muonroi-cli`; update via bun.
|
|
59
|
+
* - "npm-global" → `npm install -g muonroi-cli`; update via npm.
|
|
60
|
+
* - "compiled" → standalone single-file binary; re-download / rebuild.
|
|
61
|
+
* - "dev-link" → linked/source build run from a git checkout (bun link,
|
|
62
|
+
* symlinked global bin, or `bun run src/index.ts`); rebuild dist.
|
|
63
|
+
* - "unknown" → can't tell; fall back to generic guidance.
|
|
64
|
+
*/
|
|
65
|
+
export type InstallMethod = "script" | "bun-global" | "npm-global" | "compiled" | "dev-link" | "unknown";
|
|
66
|
+
/**
|
|
67
|
+
* Detect how the running muonroi-cli was installed by inspecting the location
|
|
68
|
+
* of this module on disk plus the runtime. Pure path inspection — no I/O beyond
|
|
69
|
+
* the install.json check, so it is safe to call on the hot path.
|
|
70
|
+
*/
|
|
71
|
+
export declare function detectInstallMethod(homeDir?: string): InstallMethod;
|
|
72
|
+
/** Package-manager command that updates muonroi-cli for the given method, or null. */
|
|
73
|
+
export declare function getUpdateCommandForMethod(method: InstallMethod): string | null;
|
|
74
|
+
/**
|
|
75
|
+
* Top-level update entry point. Routes to the script-managed updater for
|
|
76
|
+
* install.sh installs, and returns the correct package-manager command for
|
|
77
|
+
* bun/npm global installs (rather than the misleading "reinstall via install.sh"
|
|
78
|
+
* dead-end). We do NOT auto-spawn the package manager: on Windows overwriting the
|
|
79
|
+
* files of the live process is unreliable, so we hand the user an exact command
|
|
80
|
+
* to run from a fresh terminal.
|
|
81
|
+
*/
|
|
82
|
+
export declare function runManagedUpdate(currentVersion: string): Promise<ScriptUpdateRunResult>;
|
|
54
83
|
export declare function runScriptManagedUpdate(currentVersion: string): Promise<ScriptUpdateRunResult>;
|
|
55
84
|
export declare function buildScriptUninstallPlan(options?: ScriptUninstallOptions, homeDir?: string): ScriptUninstallPlan | null;
|
|
56
85
|
export declare function runScriptManagedUninstall(options?: ScriptUninstallOptions): Promise<ScriptUpdateRunResult>;
|
|
@@ -6,6 +6,7 @@ import path from "path";
|
|
|
6
6
|
import readline from "readline";
|
|
7
7
|
import semverGt from "semver/functions/gt.js";
|
|
8
8
|
import semverValid from "semver/functions/valid.js";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
9
10
|
export const GITHUB_REPO = "muonroi/muonroi-cli";
|
|
10
11
|
export const RELEASES_API = `https://api.github.com/repos/${GITHUB_REPO}/releases`;
|
|
11
12
|
export const SCRIPT_INSTALL_METHOD = "script";
|
|
@@ -99,6 +100,111 @@ export function parseChecksumsFile(contents) {
|
|
|
99
100
|
}
|
|
100
101
|
return result;
|
|
101
102
|
}
|
|
103
|
+
/** Absolute filesystem path of THIS module, normalized to forward slashes. */
|
|
104
|
+
function runningModulePath() {
|
|
105
|
+
try {
|
|
106
|
+
return fileURLToPath(import.meta.url).replace(/\\/g, "/");
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return (process.argv[1] ?? "").replace(/\\/g, "/");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Walk up from `startDir` looking for a `.git` entry; return the repo root or null. */
|
|
113
|
+
function findGitRoot(startDir) {
|
|
114
|
+
let dir = startDir;
|
|
115
|
+
for (let i = 0; i < 30 && dir; i++) {
|
|
116
|
+
if (fs.existsSync(path.join(dir, ".git")))
|
|
117
|
+
return dir;
|
|
118
|
+
const parent = path.dirname(dir);
|
|
119
|
+
if (parent === dir)
|
|
120
|
+
break;
|
|
121
|
+
dir = parent;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Detect how the running muonroi-cli was installed by inspecting the location
|
|
127
|
+
* of this module on disk plus the runtime. Pure path inspection — no I/O beyond
|
|
128
|
+
* the install.json check, so it is safe to call on the hot path.
|
|
129
|
+
*/
|
|
130
|
+
export function detectInstallMethod(homeDir = os.homedir()) {
|
|
131
|
+
if (loadScriptInstallMetadata(homeDir))
|
|
132
|
+
return "script";
|
|
133
|
+
const modPath = runningModulePath();
|
|
134
|
+
const importUrl = (import.meta.url || "").replace(/\\/g, "/");
|
|
135
|
+
// Bun single-file compiled executable embeds modules under a virtual root
|
|
136
|
+
// (e.g. "/$bunfs/" or "/~BUN/"), so the module path is not a real fs path.
|
|
137
|
+
if (importUrl.includes("/$bunfs/") || importUrl.includes("/~BUN/") || modPath.includes("/$bunfs/")) {
|
|
138
|
+
return "compiled";
|
|
139
|
+
}
|
|
140
|
+
// Bun's global install root: ~/.bun/install/global/node_modules/muonroi-cli/...
|
|
141
|
+
if (modPath.includes("/.bun/install/global/"))
|
|
142
|
+
return "bun-global";
|
|
143
|
+
if (/\/node_modules\/muonroi-cli\//.test(modPath)) {
|
|
144
|
+
return modPath.includes("/.bun/") ? "bun-global" : "npm-global";
|
|
145
|
+
}
|
|
146
|
+
// Not under node_modules and not launched by node/bun → standalone binary.
|
|
147
|
+
const exeBase = ((process.execPath || "").replace(/\\/g, "/").split("/").pop() ?? "").toLowerCase();
|
|
148
|
+
if (!modPath.includes("/node_modules/") && !/^(node|bun)(\.exe)?$/.test(exeBase)) {
|
|
149
|
+
return "compiled";
|
|
150
|
+
}
|
|
151
|
+
// Linked/source build run from a git checkout (e.g. `bun link`, a symlinked
|
|
152
|
+
// global bin pointing at the repo, or `bun run src/index.ts`). The "update"
|
|
153
|
+
// here is a rebuild, not a package-manager swap.
|
|
154
|
+
if (modPath && !modPath.includes("/node_modules/") && findGitRoot(path.dirname(modPath))) {
|
|
155
|
+
return "dev-link";
|
|
156
|
+
}
|
|
157
|
+
return "unknown";
|
|
158
|
+
}
|
|
159
|
+
/** Package-manager command that updates muonroi-cli for the given method, or null. */
|
|
160
|
+
export function getUpdateCommandForMethod(method) {
|
|
161
|
+
switch (method) {
|
|
162
|
+
case "bun-global":
|
|
163
|
+
return "bun add -g muonroi-cli@latest";
|
|
164
|
+
case "npm-global":
|
|
165
|
+
return "npm install -g muonroi-cli@latest";
|
|
166
|
+
default:
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Top-level update entry point. Routes to the script-managed updater for
|
|
172
|
+
* install.sh installs, and returns the correct package-manager command for
|
|
173
|
+
* bun/npm global installs (rather than the misleading "reinstall via install.sh"
|
|
174
|
+
* dead-end). We do NOT auto-spawn the package manager: on Windows overwriting the
|
|
175
|
+
* files of the live process is unreliable, so we hand the user an exact command
|
|
176
|
+
* to run from a fresh terminal.
|
|
177
|
+
*/
|
|
178
|
+
export async function runManagedUpdate(currentVersion) {
|
|
179
|
+
const method = detectInstallMethod();
|
|
180
|
+
if (method === "script")
|
|
181
|
+
return runScriptManagedUpdate(currentVersion);
|
|
182
|
+
const cmd = getUpdateCommandForMethod(method);
|
|
183
|
+
if (cmd) {
|
|
184
|
+
const pm = method === "bun-global" ? "bun" : "npm";
|
|
185
|
+
return {
|
|
186
|
+
success: true,
|
|
187
|
+
output: `Installed via ${pm} (global package). To update, run this in a fresh terminal:\n\n ${cmd}\n\nThen restart muonroi-cli.`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (method === "compiled") {
|
|
191
|
+
const target = getReleaseTargetForPlatform();
|
|
192
|
+
const asset = target?.assetName ?? "the release asset for your platform";
|
|
193
|
+
return {
|
|
194
|
+
success: true,
|
|
195
|
+
output: `Standalone binary install. Download the latest ${asset} from https://github.com/${GITHUB_REPO}/releases/latest and replace the current binary, or rebuild from source.`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (method === "dev-link") {
|
|
199
|
+
const root = findGitRoot(path.dirname(runningModulePath()));
|
|
200
|
+
const target = root ?? "the muonroi-cli checkout";
|
|
201
|
+
return {
|
|
202
|
+
success: true,
|
|
203
|
+
output: `Running a linked/source build from ${target}. To update, rebuild it:\n\n git -C "${target}" pull && bun install && bun run build\n\nThen restart muonroi-cli. (If you also use the compiled muonroi-cli-dev binary, rebuild that separately.)`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return notScriptManaged("update");
|
|
207
|
+
}
|
|
102
208
|
export async function runScriptManagedUpdate(currentVersion) {
|
|
103
209
|
const context = getScriptInstallContext();
|
|
104
210
|
if (!context)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import semverGt from "semver/functions/gt.js";
|
|
2
2
|
import semverValid from "semver/functions/valid.js";
|
|
3
|
-
import { fetchLatestReleaseVersion,
|
|
3
|
+
import { fetchLatestReleaseVersion, runManagedUpdate } from "./install-manager.js";
|
|
4
4
|
export async function checkForUpdate(currentVersion) {
|
|
5
5
|
try {
|
|
6
6
|
const latestVersion = await fetchLatestReleaseVersion();
|
|
@@ -17,6 +17,6 @@ export async function checkForUpdate(currentVersion) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
export function runUpdate(currentVersion) {
|
|
20
|
-
return
|
|
20
|
+
return runManagedUpdate(currentVersion);
|
|
21
21
|
}
|
|
22
22
|
//# sourceMappingURL=update-checker.js.map
|
|
@@ -95,12 +95,12 @@ describe("checkForUpdate", () => {
|
|
|
95
95
|
});
|
|
96
96
|
});
|
|
97
97
|
describe("runUpdate", () => {
|
|
98
|
-
it("returns success when the
|
|
98
|
+
it("returns success when the managed updater succeeds", async () => {
|
|
99
99
|
vi.doMock("./install-manager", async () => {
|
|
100
100
|
const actual = await vi.importActual("./install-manager");
|
|
101
101
|
return {
|
|
102
102
|
...actual,
|
|
103
|
-
|
|
103
|
+
runManagedUpdate: vi.fn().mockResolvedValue({ success: true, output: "Updated to muonroi-cli 2.0.0." }),
|
|
104
104
|
};
|
|
105
105
|
});
|
|
106
106
|
const { runUpdate } = await importModule();
|
|
@@ -108,12 +108,12 @@ describe("runUpdate", () => {
|
|
|
108
108
|
expect(result.success).toBe(true);
|
|
109
109
|
expect(result.output).toContain("Updated");
|
|
110
110
|
});
|
|
111
|
-
it("returns failure when the
|
|
111
|
+
it("returns failure when the managed updater fails", async () => {
|
|
112
112
|
vi.doMock("./install-manager", async () => {
|
|
113
113
|
const actual = await vi.importActual("./install-manager");
|
|
114
114
|
return {
|
|
115
115
|
...actual,
|
|
116
|
-
|
|
116
|
+
runManagedUpdate: vi.fn().mockResolvedValue({ success: false, output: "permission denied" }),
|
|
117
117
|
};
|
|
118
118
|
});
|
|
119
119
|
const { runUpdate } = await importModule();
|
|
@@ -122,4 +122,17 @@ describe("runUpdate", () => {
|
|
|
122
122
|
expect(result.output).toContain("permission denied");
|
|
123
123
|
});
|
|
124
124
|
});
|
|
125
|
+
describe("getUpdateCommandForMethod", () => {
|
|
126
|
+
it("maps bun/npm global installs to the right update command", async () => {
|
|
127
|
+
const { getUpdateCommandForMethod } = await import("./install-manager.js");
|
|
128
|
+
expect(getUpdateCommandForMethod("bun-global")).toBe("bun add -g muonroi-cli@latest");
|
|
129
|
+
expect(getUpdateCommandForMethod("npm-global")).toBe("npm install -g muonroi-cli@latest");
|
|
130
|
+
});
|
|
131
|
+
it("returns null for methods without a package-manager command", async () => {
|
|
132
|
+
const { getUpdateCommandForMethod } = await import("./install-manager.js");
|
|
133
|
+
expect(getUpdateCommandForMethod("script")).toBeNull();
|
|
134
|
+
expect(getUpdateCommandForMethod("compiled")).toBeNull();
|
|
135
|
+
expect(getUpdateCommandForMethod("unknown")).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
125
138
|
//# sourceMappingURL=update-checker.test.js.map
|