clauderipple 0.9.5 → 0.11.0
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/CHANGELOG.md +118 -0
- package/README.es.md +45 -0
- package/README.ja.md +17 -0
- package/README.ko.md +31 -0
- package/README.md +70 -0
- package/README.pt-BR.md +45 -0
- package/README.zh-CN.md +17 -0
- package/dist/app/assets/icon.icns +0 -0
- package/dist/app/assets/icon.ico +0 -0
- package/dist/app/assets/icon.png +0 -0
- package/dist/app/assets/trayDirectTemplate.png +0 -0
- package/dist/app/assets/trayDirectTemplate@2x.png +0 -0
- package/dist/app/assets/trayDownTemplate.png +0 -0
- package/dist/app/assets/trayDownTemplate@2x.png +0 -0
- package/dist/app/assets/trayTemplate.png +0 -0
- package/dist/app/assets/trayTemplate@2x.png +0 -0
- package/dist/app/assets/trayWarnTemplate.png +0 -0
- package/dist/app/assets/trayWarnTemplate@2x.png +0 -0
- package/dist/app/assets/trayWin.png +0 -0
- package/dist/app/assets/trayWin@2x.png +0 -0
- package/dist/app/assets/trayWinDirect.png +0 -0
- package/dist/app/assets/trayWinDirect@2x.png +0 -0
- package/dist/app/assets/trayWinDown.png +0 -0
- package/dist/app/assets/trayWinDown@2x.png +0 -0
- package/dist/app/assets/trayWinWarn.png +0 -0
- package/dist/app/assets/trayWinWarn@2x.png +0 -0
- package/dist/app/dist/claude-desktop.js +67 -0
- package/dist/app/dist/main.js +423 -110
- package/dist/app/dist/preload.js +21 -0
- package/dist/app/dist/protocol.js +121 -0
- package/dist/app/dist/status-model.js +87 -0
- package/dist/app/dist/window.js +133 -0
- package/dist/app/package.json +1 -1
- package/dist/cli/src/awake-setup.js +314 -0
- package/dist/cli/src/bundle.js +9 -5
- package/dist/cli/src/direct.js +159 -0
- package/dist/cli/src/doctor.js +205 -0
- package/dist/cli/src/hooks/agent-title.js +7 -4
- package/dist/cli/src/index.js +227 -31
- package/dist/cli/src/launchd.js +29 -20
- package/dist/cli/src/launcher.js +215 -0
- package/dist/cli/src/power.js +182 -0
- package/dist/cli/src/registration.js +157 -0
- package/dist/cli/src/remote.js +263 -0
- package/dist/cli/src/schtasks.js +50 -40
- package/dist/cli/src/settings.js +26 -1
- package/dist/cli/src/tray.js +5 -2
- package/dist/mod/hooks/i18n.ts +473 -227
- package/dist/mod/hooks/judge.ts +35 -17
- package/dist/mod/hooks/modes.ts +56 -1
- package/dist/mod/hooks/register.tsx +237 -32
- package/dist/mod/hooks/session.ts +102 -1
- package/dist/mod/hooks/settings.js +7 -3
- package/dist/mod/hooks/settings.ts +9 -4
- package/dist/mod/hooks/stats.ts +25 -1
- package/dist/mod/hooks/ui/agents.tsx +903 -79
- package/dist/mod/hooks/ui/alerts.tsx +20 -17
- package/dist/mod/hooks/ui/art.ts +33 -7
- package/dist/mod/hooks/ui/band.tsx +262 -81
- package/dist/mod/hooks/ui/flat.ts +265 -0
- package/dist/mod/hooks/ui/kit.tsx +70 -55
- package/dist/mod/hooks/ui/look.ts +129 -0
- package/dist/mod/hooks/ui/settings.tsx +71 -46
- package/dist/mod/hooks/ui/setup.tsx +15 -13
- package/dist/mod/hooks/ui/theme.ts +3 -0
- package/dist/mod/hooks/ui/tools.ts +124 -0
- package/dist/mod/hooks/ui/view.ts +120 -4
- package/dist/mod/hooks/workers.ts +33 -6
- package/dist/mod/tests/register.test.tsx +155 -11
- package/dist/mod/tests/ui.test.tsx +408 -27
- package/dist/mod/types/index.d.ts +66 -2
- package/dist/remote/package.json +28 -0
- package/dist/remote/src/away.js +85 -0
- package/dist/remote/src/bridge-client.js +336 -0
- package/dist/remote/src/composer-menu.js +11 -0
- package/dist/remote/src/config.js +102 -0
- package/dist/remote/src/desktop.js +1018 -0
- package/dist/remote/src/e2ee.js +126 -0
- package/dist/remote/src/e2ee.ts +126 -0
- package/dist/remote/src/eventlog.js +298 -0
- package/dist/remote/src/expo-push.js +127 -0
- package/dist/remote/src/folders.js +79 -0
- package/dist/remote/src/fsutil.js +39 -0
- package/dist/remote/src/handoff.js +38 -0
- package/dist/remote/src/host-service.js +168 -0
- package/dist/remote/src/images.js +215 -0
- package/dist/remote/src/local-control.js +138 -0
- package/dist/remote/src/main.js +264 -0
- package/dist/remote/src/permissions.js +124 -0
- package/dist/remote/src/power.js +76 -0
- package/dist/remote/src/processes.js +24 -0
- package/dist/remote/src/protocol.js +32 -0
- package/dist/remote/src/protocol.ts +284 -0
- package/dist/remote/src/push-content.js +32 -0
- package/dist/remote/src/push.js +134 -0
- package/dist/remote/src/receipts.js +111 -0
- package/dist/remote/src/relay.js +438 -0
- package/dist/remote/src/router.js +208 -0
- package/dist/remote/src/sdk-launcher.js +18 -0
- package/dist/remote/src/server.js +706 -0
- package/dist/remote/src/service.js +157 -0
- package/dist/remote/src/sessions.js +1800 -0
- package/dist/remote/src/sidebar-storage.js +211 -0
- package/dist/remote/src/sidebar.js +173 -0
- package/dist/remote/src/tool-details.js +205 -0
- package/dist/remote/src/tool-details.ts +153 -0
- package/dist/remote/src/worktree.js +99 -0
- package/dist/remote/web/app.ts +746 -0
- package/dist/remote/web/conn.ts +193 -0
- package/dist/remote/web/demo.ts +134 -0
- package/dist/remote/web/dom.ts +124 -0
- package/dist/remote/web/esc.ts +4 -0
- package/dist/remote/web/i18n.ts +374 -0
- package/dist/remote/web/icon.svg +1 -0
- package/dist/remote/web/index.html +24 -0
- package/dist/remote/web/manifest.webmanifest +10 -0
- package/dist/remote/web/mascot.ts +60 -0
- package/dist/remote/web/md.ts +78 -0
- package/dist/remote/web/notify.ts +55 -0
- package/dist/remote/web/pushstate.ts +27 -0
- package/dist/remote/web/secure.ts +109 -0
- package/dist/remote/web/sidebar.ts +106 -0
- package/dist/remote/web/style.css +311 -0
- package/dist/remote/web/sw.ts +48 -0
- package/dist/remote/web/timeline.ts +224 -0
- package/dist/remote/web/views.ts +323 -0
- package/dist/router/src/admin.js +331 -5
- package/dist/router/src/awake-win.js +590 -0
- package/dist/router/src/awake.js +432 -0
- package/dist/router/src/config.js +94 -0
- package/dist/router/src/health.js +86 -3
- package/dist/router/src/hold.js +107 -0
- package/dist/router/src/index.js +108 -6
- package/dist/router/src/ingress/server.js +26 -4
- package/dist/router/src/ingress/translate.js +6 -2
- package/dist/router/src/phone.js +82 -0
- package/dist/router/src/power.js +107 -0
- package/dist/router/src/providers/anthropic-usage.js +33 -1
- package/dist/router/src/providers/google/antigravity.js +73 -10
- package/dist/router/src/providers/google/index.js +191 -14
- package/dist/router/src/providers/google/prompt-refusal.js +29 -0
- package/dist/router/src/providers/google/thoughtSignatures.js +13 -0
- package/dist/router/src/providers/google/transport.js +1 -1
- package/dist/router/src/providers/google/usage.js +302 -0
- package/dist/router/src/providers/grok/index.js +18 -0
- package/dist/router/src/providers/grok/usage.js +77 -0
- package/dist/router/src/providers/openai/translate.js +13 -2
- package/dist/router/src/providers/opencode-usage.js +116 -0
- package/dist/router/src/providers/retry.js +4 -1
- package/dist/router/src/proxy.js +371 -261
- package/dist/router/src/remote-host.js +320 -0
- package/dist/router/src/remote-tunnel.js +320 -0
- package/dist/router/src/requestlog.js +48 -39
- package/dist/router/src/tether-win.js +211 -0
- package/dist/router/src/tether.js +712 -0
- package/dist/router/src/usage-history.js +227 -0
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +1083 -59
- package/dist/ui/i18n.js +670 -69
- package/dist/ui/index.html +33 -6
- package/dist/ui/style.css +177 -0
- package/docs/ARCHITECTURE.md +402 -14
- package/package.json +8 -2
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// `clauderipple doctor` and `clauderipple repair`: what is wrong with the chain Claude depends on,
|
|
2
|
+
// and the idempotent fix. Both serve the menu-bar app, which has to work when the router — and
|
|
3
|
+
// with it the dashboard — is down: Claude Desktop and Claude Code send everything to 127.0.0.1
|
|
4
|
+
// and fail with ECONNREFUSED, and nothing the router serves can explain why (2026-10-20).
|
|
5
|
+
//
|
|
6
|
+
// Everything that reaches the machine is a dependency (`RecoveryDeps`), so tests run with a fake
|
|
7
|
+
// supervisor and temp files and never touch launchd, the user's settings or the live router.
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { ConfigStore, adminPort } from "../../router/src/config.js";
|
|
11
|
+
import { certPaths } from "./certs.js";
|
|
12
|
+
import { applyAppProxy, currentAppProxy } from "./picker.js";
|
|
13
|
+
import { applyProxyEnv, settingsEnv } from "./settings.js";
|
|
14
|
+
export async function fetchRouterStatus(adminUrl, timeoutMs) {
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(`${adminUrl}/api/status`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
17
|
+
if (!res.ok)
|
|
18
|
+
return null;
|
|
19
|
+
const body = (await res.json());
|
|
20
|
+
return { version: typeof body.version === "string" ? body.version : null };
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const sleepDefault = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
27
|
+
const LOOPBACK_PROXY = /^http:\/\/(127\.0\.0\.1|localhost|\[::1\]):\d+\/?$/;
|
|
28
|
+
export function proxyUrlFor(port) {
|
|
29
|
+
return `http://127.0.0.1:${port}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Claude Code's proxy, from settings.json env. `stale` is a proxy that is ours — our CA beside a
|
|
33
|
+
* loopback URL — on a port the config no longer listens on: the one case repair may rewrite.
|
|
34
|
+
*/
|
|
35
|
+
export function cliProxyState(env, proxyUrl, caPath) {
|
|
36
|
+
const url = env.HTTPS_PROXY || null;
|
|
37
|
+
if (!url)
|
|
38
|
+
return { proxy: "none", url: null, stale: false };
|
|
39
|
+
if (url === proxyUrl)
|
|
40
|
+
return { proxy: "ripple", url, stale: false };
|
|
41
|
+
return { proxy: "other", url, stale: env.NODE_EXTRA_CA_CERTS === caPath && LOOPBACK_PROXY.test(url) };
|
|
42
|
+
}
|
|
43
|
+
/** Claude Desktop's proxy, from the applied Config Library entry. An entry with no proxy is "none". */
|
|
44
|
+
export function desktopProxyState(ap, proxyUrl) {
|
|
45
|
+
const url = ap.egressProxyUrl;
|
|
46
|
+
if (!url)
|
|
47
|
+
return { proxy: "none", url: null, stale: false };
|
|
48
|
+
if (url === proxyUrl)
|
|
49
|
+
return { proxy: "ripple", url, stale: false };
|
|
50
|
+
return { proxy: "other", url, stale: ap.ours };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Claude Code always goes through the router in Ripple mode; Claude Desktop itself only when picker
|
|
54
|
+
* mode is on (the Code tab's sessions read settings.json either way). Nothing through us is
|
|
55
|
+
* "direct"; anything in between is "mixed".
|
|
56
|
+
*/
|
|
57
|
+
export function classifyMode(cli, desktop, pickerEnabled) {
|
|
58
|
+
const cliRipple = cli === "ripple";
|
|
59
|
+
const desktopRipple = desktop === "ripple";
|
|
60
|
+
if (cliRipple && (desktopRipple || !pickerEnabled))
|
|
61
|
+
return "ripple";
|
|
62
|
+
if (!cliRipple && !desktopRipple)
|
|
63
|
+
return "direct";
|
|
64
|
+
return "mixed";
|
|
65
|
+
}
|
|
66
|
+
function configFile(home) {
|
|
67
|
+
return path.join(home, "config.json");
|
|
68
|
+
}
|
|
69
|
+
async function snapshot(deps) {
|
|
70
|
+
const installed = fs.existsSync(configFile(deps.home));
|
|
71
|
+
const cfg = new ConfigStore(configFile(deps.home)).get();
|
|
72
|
+
const proxyUrl = proxyUrlFor(cfg.listen.port);
|
|
73
|
+
const adminUrl = `http://127.0.0.1:${adminPort(cfg)}`;
|
|
74
|
+
const caPath = certPaths(deps.home).caPem;
|
|
75
|
+
const status = await (deps.routerStatus ?? fetchRouterStatus)(adminUrl, 4_000);
|
|
76
|
+
const view = deps.supervisor.inspect();
|
|
77
|
+
const cli = cliProxyState(settingsEnv(), proxyUrl, caPath);
|
|
78
|
+
const desktop = desktopProxyState(currentAppProxy(), proxyUrl);
|
|
79
|
+
const pickerEnabled = cfg.picker?.enabled === true;
|
|
80
|
+
const mode = classifyMode(cli.proxy, desktop.proxy, pickerEnabled);
|
|
81
|
+
const problems = [];
|
|
82
|
+
const add = (code, message) => void problems.push({ code, message });
|
|
83
|
+
if (!installed)
|
|
84
|
+
add("not-installed", `ClaudeRipple is not installed here (no ${configFile(deps.home)}). Run: clauderipple install`);
|
|
85
|
+
if (!view.registered)
|
|
86
|
+
add("supervisor-missing", "The router is not registered to start at login, so nothing starts it. Repair registers it again.");
|
|
87
|
+
else if (!view.programExists)
|
|
88
|
+
add("supervisor-program-missing", `The router's startup entry points at a program that no longer exists (${view.program ?? "none"}). Repair points it at this installation.`);
|
|
89
|
+
else if (!view.current)
|
|
90
|
+
add("supervisor-other-install", `The router's startup entry belongs to another ClaudeRipple installation (${view.program}).`);
|
|
91
|
+
if (!status) {
|
|
92
|
+
add("router-down", `The router is not answering on ${adminUrl}. ${mode === "direct" ? "Claude is on its default connection, so it is not affected." : "Claude Desktop and Claude Code cannot connect while it is down."}`);
|
|
93
|
+
if (view.registered && view.state !== "running")
|
|
94
|
+
add("supervisor-not-running", "The router's background service is not running.");
|
|
95
|
+
}
|
|
96
|
+
else if (status.version && status.version !== deps.version) {
|
|
97
|
+
add("router-version-mismatch", `The running router is ${status.version}; this ClaudeRipple is ${deps.version}.`);
|
|
98
|
+
}
|
|
99
|
+
if (cli.stale)
|
|
100
|
+
add("cli-port-mismatch", `Claude Code is set to a ClaudeRipple port that is no longer used (${cli.url}; the router listens on ${proxyUrl}).`);
|
|
101
|
+
else if (cli.proxy === "other")
|
|
102
|
+
add("cli-foreign-proxy", `Claude Code goes through another proxy (${cli.url}), not ClaudeRipple.`);
|
|
103
|
+
if (desktop.stale)
|
|
104
|
+
add("desktop-port-mismatch", `Claude Desktop is set to a ClaudeRipple port that is no longer used (${desktop.url}; the router listens on ${proxyUrl}).`);
|
|
105
|
+
else if (desktop.proxy === "other")
|
|
106
|
+
add("desktop-foreign-proxy", `Claude Desktop goes through another proxy (${desktop.url}), not ClaudeRipple.`);
|
|
107
|
+
if (mode === "mixed")
|
|
108
|
+
add("mixed-mode", `Claude Code ${cli.proxy === "ripple" ? "goes" : "does not go"} through ClaudeRipple, Claude Desktop ${desktop.proxy === "ripple" ? "does" : "does not"}.`);
|
|
109
|
+
return {
|
|
110
|
+
report: {
|
|
111
|
+
mode,
|
|
112
|
+
router: { responding: status !== null, version: status?.version ?? null, pid: view.pid, url: proxyUrl, adminUrl },
|
|
113
|
+
supervisor: { registered: view.registered, program: view.program, programExists: view.programExists, state: view.state },
|
|
114
|
+
desktop: { proxy: desktop.proxy, url: desktop.url },
|
|
115
|
+
cli: { proxy: cli.proxy, url: cli.url },
|
|
116
|
+
problems,
|
|
117
|
+
},
|
|
118
|
+
installed,
|
|
119
|
+
cfg,
|
|
120
|
+
view,
|
|
121
|
+
caPath,
|
|
122
|
+
cliStale: cli.stale,
|
|
123
|
+
desktopStale: desktop.stale,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export async function doctor(deps) {
|
|
127
|
+
return (await snapshot(deps)).report;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Fixes what doctor finds, in order, and is safe to run again: re-register the supervisor for this
|
|
131
|
+
* installation, start it, wait for it to answer, and correct a Claude proxy setting that is ours but
|
|
132
|
+
* names an old port. It never switches between Ripple and direct mode (that is `direct on|off`) and
|
|
133
|
+
* never touches a proxy setting that is not ours.
|
|
134
|
+
*/
|
|
135
|
+
export async function repair(deps) {
|
|
136
|
+
const sleep = deps.sleep ?? sleepDefault;
|
|
137
|
+
const status = deps.routerStatus ?? fetchRouterStatus;
|
|
138
|
+
const steps = [];
|
|
139
|
+
const before = await snapshot(deps);
|
|
140
|
+
if (!before.installed) {
|
|
141
|
+
steps.push({ name: "config", ok: false, detail: `no ${configFile(deps.home)}; run \`clauderipple install\` first` });
|
|
142
|
+
return { ok: false, steps, doctor: before.report };
|
|
143
|
+
}
|
|
144
|
+
const responding = before.report.router.responding;
|
|
145
|
+
const view = before.view;
|
|
146
|
+
// A working router is never cut off for a definition that merely differs: re-registering unloads
|
|
147
|
+
// the running job, which drains it and drops every Claude connection (ARCHITECTURE §5).
|
|
148
|
+
const mustRegister = !view.registered || !view.programExists || (!view.current && !responding);
|
|
149
|
+
if (mustRegister) {
|
|
150
|
+
try {
|
|
151
|
+
const where = deps.supervisor.register();
|
|
152
|
+
steps.push({ name: "supervisor", ok: true, detail: `registered for this installation: ${where}` });
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
steps.push({ name: "supervisor", ok: false, detail: e.message });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
steps.push({
|
|
160
|
+
name: "supervisor",
|
|
161
|
+
ok: true,
|
|
162
|
+
detail: view.current ? "already registered for this installation" : `left as it is: the router answers, and its entry names another installation (${view.program}); run \`clauderipple install\` to move it`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (responding) {
|
|
166
|
+
steps.push({ name: "start", ok: true, detail: "the router is already answering" });
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const started = deps.supervisor.start();
|
|
170
|
+
steps.push({ name: "start", ok: started !== "failed", detail: started === "failed" ? "the supervisor could not start the router" : started === "already-running" ? "the supervisor reports it running" : "started" });
|
|
171
|
+
const adminUrl = before.report.router.adminUrl;
|
|
172
|
+
const budget = deps.healthBudgetMs ?? 20_000;
|
|
173
|
+
const t0 = Date.now();
|
|
174
|
+
let up = false;
|
|
175
|
+
while (!up) {
|
|
176
|
+
up = (await status(adminUrl, 3_000)) !== null;
|
|
177
|
+
if (up || Date.now() - t0 >= budget)
|
|
178
|
+
break;
|
|
179
|
+
await sleep(1_000);
|
|
180
|
+
}
|
|
181
|
+
steps.push({ name: "health", ok: up, detail: up ? `answering on ${adminUrl} after ${Math.round((Date.now() - t0) / 100) / 10}s` : `still not answering on ${adminUrl} after ${Math.round(budget / 1000)}s; see ${path.join(deps.home, "logs", "router.log")}` });
|
|
182
|
+
}
|
|
183
|
+
const proxyUrl = proxyUrlFor(before.cfg.listen.port);
|
|
184
|
+
if (before.cliStale) {
|
|
185
|
+
try {
|
|
186
|
+
// Ours by our CA beside a loopback URL (cliProxyState), so replacing it is not overriding anyone.
|
|
187
|
+
const edit = applyProxyEnv({ proxyUrl, caPath: before.caPath, force: true });
|
|
188
|
+
steps.push({ name: "cli-proxy", ok: true, detail: `Claude Code now points at ${proxyUrl}${edit.backup ? ` (backup: ${edit.backup})` : ""}` });
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
steps.push({ name: "cli-proxy", ok: false, detail: e.message });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (before.desktopStale) {
|
|
195
|
+
try {
|
|
196
|
+
applyAppProxy(proxyUrl);
|
|
197
|
+
steps.push({ name: "desktop-proxy", ok: true, detail: `Claude Desktop now points at ${proxyUrl} (quit and reopen it to apply)` });
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
steps.push({ name: "desktop-proxy", ok: false, detail: e.message });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const after = await doctor(deps);
|
|
204
|
+
return { ok: steps.every((s) => s.ok) && after.router.responding, steps, doctor: after };
|
|
205
|
+
}
|
|
@@ -25,13 +25,16 @@ import { fileURLToPath } from "node:url";
|
|
|
25
25
|
const MARKER = /^\s*\[\[\s*(?:ripple|gpt)\s*:\s*([A-Za-z0-9.\-]+)\s*(?:@\s*([A-Za-z]+))?\s*\]\]/;
|
|
26
26
|
const REMINDER = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
27
27
|
const OUR_PREFIX = /^[^\s·]+(?:·[a-z]+)? · /;
|
|
28
|
-
const CLAUDE_ID = /^claude-(opus|sonnet|haiku|fable)-(\d+(?:-\d+)*?)(?:-\d{8})?$/;
|
|
28
|
+
const CLAUDE_ID = /^claude-(opus|sonnet|haiku|fable|mythos)-(\d+(?:-\d+)*?)(?:-\d{8})?$/;
|
|
29
29
|
/** The router's default `effortClamp` (config.ts), for when config.json cannot be read. */
|
|
30
30
|
const DEFAULT_CLAMP = { ultra: "max" };
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* A model's name in a title: the last word of its display name ("GPT-6.1 Sol" → "Sol"), without
|
|
33
|
+
* the vendor path a hosted id carries ("deepseek-ai/deepseek-v4.1-flash-1017" → "deepseek-v4.1-flash-1017").
|
|
34
|
+
*/
|
|
32
35
|
export function shortName(name) {
|
|
33
36
|
const words = name.trim().split(/\s+/);
|
|
34
|
-
return words[words.length - 1];
|
|
37
|
+
return words[words.length - 1].replace(/^.*\//, "");
|
|
35
38
|
}
|
|
36
39
|
function titleConfig() {
|
|
37
40
|
const names = { "gpt-5.6-terra": "Terra", "gpt-5.6-sol": "Sol", "gpt-5.6-luna": "Luna", "gpt-6-astra": "Astra", "gpt-6-sol": "Sol", "gpt-6-luna": "Luna" };
|
|
@@ -84,7 +87,7 @@ export function fromAgentFile(subagentType, dirs) {
|
|
|
84
87
|
}
|
|
85
88
|
export function claudePretty(modelId) {
|
|
86
89
|
const v = (modelId ?? "").trim().toLowerCase();
|
|
87
|
-
if (["opus", "sonnet", "haiku", "fable"].includes(v))
|
|
90
|
+
if (["opus", "sonnet", "haiku", "fable", "mythos"].includes(v))
|
|
88
91
|
return v[0].toUpperCase() + v.slice(1);
|
|
89
92
|
const m = CLAUDE_ID.exec(v);
|
|
90
93
|
return m ? m[1][0].toUpperCase() + m[1].slice(1) + m[2].replace(/-/g, ".") : null;
|
package/dist/cli/src/index.js
CHANGED
|
@@ -15,7 +15,9 @@ import { adminPort } from "../../router/src/admin.js";
|
|
|
15
15
|
import { certsExist, certPaths, generateCerts } from "./certs.js";
|
|
16
16
|
import { applyProxyEnv, checkProxyEnv, currentProxyEnv, removeProxyEnv, settingsPath } from "./settings.js";
|
|
17
17
|
import { agentDefinitionPath, agentState, installAgent, isLinux, isSupported, isWindows, removeAgent, restartAgent, startAgent, stopAgent, supervisorName } from "./supervisor.js";
|
|
18
|
-
import {
|
|
18
|
+
import { removeBundle, writeBundle } from "./bundle.js";
|
|
19
|
+
import { nativeSupervisor, registration, writePaths } from "./registration.js";
|
|
20
|
+
import { installLauncher, uninstallLauncher } from "./launcher.js";
|
|
19
21
|
import { applyAppProxy, caTrusted, currentAppProxy, nssDb, removeAppProxy, trustCa, untrustCa } from "./picker.js";
|
|
20
22
|
import { runtime } from "./runtime.js";
|
|
21
23
|
import { codexOff, codexOn } from "./codex.js";
|
|
@@ -201,37 +203,17 @@ async function install() {
|
|
|
201
203
|
checkProxyEnv({ proxyUrl, force: flag("force") });
|
|
202
204
|
// Persist the single execution contract so the GUI, admin API, hooks, and supervisor all use
|
|
203
205
|
// this installation's runtime rather than any Node installation on the user's PATH.
|
|
204
|
-
|
|
205
|
-
node: installedRuntime.node,
|
|
206
|
-
env: installedRuntime.env,
|
|
207
|
-
cli: installedRuntime.cli,
|
|
208
|
-
router: installedRuntime.router,
|
|
209
|
-
hookScript: installedRuntime.hookScript,
|
|
210
|
-
repo: installedRuntime.repo,
|
|
211
|
-
}, null, 2) + "\n");
|
|
206
|
+
writePaths(installedRuntime, home);
|
|
212
207
|
// Windows and Linux register node + the router script directly (schtasks.ts writes its own .cmd
|
|
213
208
|
// launcher); a macOS source checkout gets a small .app so the agent shows a real name in Login Items.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
bundleId: "com.clauderipple.app",
|
|
219
|
-
home,
|
|
220
|
-
env: { ...installedRuntime.env, ...(process.env.CLAUDE_SETTINGS_PATH ? { CLAUDE_SETTINGS_PATH: process.env.CLAUDE_SETTINGS_PATH } : {}) },
|
|
221
|
-
});
|
|
222
|
-
console.log(`✓ ${supervisorName()} registered: ${plist}${isLinux ? "" : ' (shows as "ClaudeRipple" in Login Items)'}`);
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
const launcher = writeBundle({ home, node: installedRuntime.node, script: installedRuntime.router, version: VERSION });
|
|
209
|
+
// registration.ts holds the mapping, so `repair` registers exactly the same thing.
|
|
210
|
+
const reg = registration(installedRuntime, home, VERSION);
|
|
211
|
+
if (reg.bundle) {
|
|
212
|
+
const launcher = writeBundle({ home, node: reg.bundle.node, script: reg.bundle.script, version: reg.bundle.version });
|
|
226
213
|
console.log(`✓ background item bundle written: ${path.dirname(path.dirname(path.dirname(launcher)))} (shows as "ClaudeRipple" in Login Items)`);
|
|
227
|
-
const plist = installAgent({
|
|
228
|
-
program: launcher,
|
|
229
|
-
bundleId: BUNDLE_ID,
|
|
230
|
-
home,
|
|
231
|
-
...(process.env.CLAUDE_SETTINGS_PATH ? { env: { CLAUDE_SETTINGS_PATH: process.env.CLAUDE_SETTINGS_PATH } } : {}),
|
|
232
|
-
});
|
|
233
|
-
console.log(`✓ ${supervisorName()} registered: ${plist}`);
|
|
234
214
|
}
|
|
215
|
+
const plist = installAgent({ program: reg.program, args: reg.args, bundleId: reg.bundleId, home, env: reg.env });
|
|
216
|
+
console.log(`✓ ${supervisorName()} registered: ${plist}${isLinux || reg.bundle ? "" : ' (shows as "ClaudeRipple" in Login Items)'}`);
|
|
235
217
|
// launchd starts the agent the moment it is bootstrapped (RunAtLoad); a scheduled task waits for
|
|
236
218
|
// its logon trigger, so the router would only appear after the next sign-in. Start it either way.
|
|
237
219
|
const started = startAgent();
|
|
@@ -257,9 +239,21 @@ async function install() {
|
|
|
257
239
|
console.log(edit.changed ? `✓ ${settingsPath()} updated (backup: ${edit.backup ?? "none"})` : `✓ ${settingsPath()} already correct`);
|
|
258
240
|
for (const n of edit.notes)
|
|
259
241
|
console.log(` note: ${n}`);
|
|
242
|
+
// Something to double-click for the menu-bar app, which is what offers repair when the router is
|
|
243
|
+
// down. Never fatal: the router and Claude's settings are already in place.
|
|
244
|
+
try {
|
|
245
|
+
const app = installLauncher({ rt: installedRuntime, home, version: VERSION });
|
|
246
|
+
if (app.paths.length)
|
|
247
|
+
console.log(`✓ menu-bar app launcher: ${app.paths.join(", ")}`);
|
|
248
|
+
else if (!app.ok)
|
|
249
|
+
console.log(` menu-bar app launcher not written: ${app.detail}`);
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
console.log(` menu-bar app launcher not written: ${e.message}`);
|
|
253
|
+
}
|
|
260
254
|
console.log("\nDone. New Claude Desktop Code sessions go through ClaudeRipple. Existing sessions pick up settings.json env changes on their next request.");
|
|
261
255
|
}
|
|
262
|
-
function uninstall() {
|
|
256
|
+
async function uninstall() {
|
|
263
257
|
const home = homeDir();
|
|
264
258
|
const cfg = new ConfigStore(configPath()).get();
|
|
265
259
|
// Stop the router before unregistering it. On macOS unloading the agent takes the process with
|
|
@@ -268,9 +262,19 @@ function uninstall() {
|
|
|
268
262
|
// Windows 11 arm64 VM).
|
|
269
263
|
if (stopAgent())
|
|
270
264
|
console.log("✓ router stopped");
|
|
265
|
+
// Before --purge can take the journal that says whether the router left sleep disabled.
|
|
266
|
+
await releaseHeldSleep("uninstalled");
|
|
271
267
|
const removed = removeAgent();
|
|
272
268
|
console.log(removed ? `✓ ${supervisorName()} removed (${agentDefinitionPath()})` : `✓ no ${supervisorName()} registered`);
|
|
273
269
|
removeBundle(home);
|
|
270
|
+
try {
|
|
271
|
+
const app = uninstallLauncher({ rt: installedRuntime, home, version: VERSION });
|
|
272
|
+
if (app.paths.length)
|
|
273
|
+
console.log(`✓ menu-bar app launcher removed (${app.paths.join(", ")})`);
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
console.log(` could not remove the menu-bar app launcher: ${e.message}`);
|
|
277
|
+
}
|
|
274
278
|
const edit = removeProxyEnv({ proxyUrl: proxyUrlFor(cfg.listen.port), caPath: certPaths(home).caPem });
|
|
275
279
|
console.log(edit.changed ? `✓ ${settingsPath()} restored (backup: ${edit.backup ?? "none"})` : `✓ ${settingsPath()} had no ClaudeRipple keys`);
|
|
276
280
|
for (const n of edit.notes)
|
|
@@ -443,14 +447,134 @@ async function update() {
|
|
|
443
447
|
if (!result.ok)
|
|
444
448
|
process.exitCode = 1;
|
|
445
449
|
}
|
|
450
|
+
// ---- Recovery: doctor, repair, direct, app ------------------------------------------------
|
|
451
|
+
//
|
|
452
|
+
// The menu-bar app calls these when the router is down and the dashboard with it. With --json each
|
|
453
|
+
// prints one JSON object on stdout and exits 0 whatever the diagnosis; a non-zero exit means the
|
|
454
|
+
// command itself could not run.
|
|
455
|
+
/** This machine's supervisor, registering this installation's runtime and files. */
|
|
456
|
+
function recoveryDeps() {
|
|
457
|
+
const home = homeDir();
|
|
458
|
+
const ops = nativeSupervisor(registration(installedRuntime, home, VERSION), home);
|
|
459
|
+
return {
|
|
460
|
+
home,
|
|
461
|
+
version: VERSION,
|
|
462
|
+
supervisor: {
|
|
463
|
+
inspect: () => ops.inspect(),
|
|
464
|
+
// paths.json is part of what install registers: the GUI and hooks run what it names.
|
|
465
|
+
register: () => {
|
|
466
|
+
writePaths(installedRuntime, home);
|
|
467
|
+
return ops.register();
|
|
468
|
+
},
|
|
469
|
+
start: () => ops.start(),
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function printDoctor(d) {
|
|
474
|
+
const sup = d.supervisor;
|
|
475
|
+
const rows = [
|
|
476
|
+
["mode", d.mode === "ripple" ? "ClaudeRipple" : d.mode === "direct" ? "Claude's default connection (direct)" : "mixed"],
|
|
477
|
+
["router", d.router.responding ? `answering${d.router.version ? ` (${d.router.version}${d.router.pid ? `, pid ${d.router.pid}` : ""})` : ""} on ${d.router.url}` : `NOT answering (${d.router.adminUrl})`],
|
|
478
|
+
[supervisorName(), sup.registered ? `registered · ${sup.state} · ${sup.programExists ? "program ok" : "PROGRAM MISSING"} (${sup.program ?? "-"})` : "NOT registered"],
|
|
479
|
+
["claude code", `${d.cli.proxy}${d.cli.url ? ` (${d.cli.url})` : ""}`],
|
|
480
|
+
["claude desktop", `${d.desktop.proxy}${d.desktop.url ? ` (${d.desktop.url})` : ""}`],
|
|
481
|
+
];
|
|
482
|
+
const w = Math.max(...rows.map((r) => r[0].length));
|
|
483
|
+
for (const [k, v] of rows)
|
|
484
|
+
console.log(`${k.padEnd(w)} ${v}`);
|
|
485
|
+
if (d.problems.length === 0)
|
|
486
|
+
console.log("\n✓ no problems found");
|
|
487
|
+
else {
|
|
488
|
+
console.log("\nproblems:");
|
|
489
|
+
for (const p of d.problems)
|
|
490
|
+
console.log(` ✗ ${p.message} [${p.code}]`);
|
|
491
|
+
console.log("\nFix what can be fixed with: clauderipple repair");
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async function recovery() {
|
|
495
|
+
const json = flag("json");
|
|
496
|
+
const out = (value, text) => {
|
|
497
|
+
if (json)
|
|
498
|
+
console.log(JSON.stringify(value));
|
|
499
|
+
else {
|
|
500
|
+
text();
|
|
501
|
+
if ("ok" in value && value.ok === false)
|
|
502
|
+
process.exitCode = 1;
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
if (cmd === "doctor") {
|
|
506
|
+
const { doctor } = await import("./doctor.js");
|
|
507
|
+
const d = await doctor(recoveryDeps());
|
|
508
|
+
out(d, () => printDoctor(d));
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (cmd === "repair") {
|
|
512
|
+
const { repair } = await import("./doctor.js");
|
|
513
|
+
const r = await repair(recoveryDeps());
|
|
514
|
+
out(r, () => {
|
|
515
|
+
for (const s of r.steps)
|
|
516
|
+
console.log(`${s.ok ? "✓" : "✗"} ${s.name}: ${s.detail}`);
|
|
517
|
+
console.log("");
|
|
518
|
+
printDoctor(r.doctor);
|
|
519
|
+
});
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (cmd === "direct") {
|
|
523
|
+
const sub = args[1];
|
|
524
|
+
if (sub !== "on" && sub !== "off")
|
|
525
|
+
throw new Error("usage: clauderipple direct on|off [--force] [--json]");
|
|
526
|
+
const { directOn, directOff } = await import("./direct.js");
|
|
527
|
+
const r = sub === "on" ? directOn(homeDir()) : await directOff(recoveryDeps(), { force: flag("force") });
|
|
528
|
+
out(r, () => {
|
|
529
|
+
console.log(`${r.ok ? "✓" : "✗"} ${r.detail}`);
|
|
530
|
+
if (r.ok)
|
|
531
|
+
console.log("Quit and reopen Claude Desktop, and start new Claude Code sessions: both read this at start.");
|
|
532
|
+
});
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (cmd === "app") {
|
|
536
|
+
const install = flag("install");
|
|
537
|
+
if (install === flag("uninstall"))
|
|
538
|
+
throw new Error("usage: clauderipple app --install|--uninstall [--json]");
|
|
539
|
+
const o = { rt: installedRuntime, home: homeDir(), version: VERSION };
|
|
540
|
+
const r = install ? installLauncher(o) : uninstallLauncher(o);
|
|
541
|
+
out(r, () => console.log(`${r.ok ? "✓" : "✗"} ${r.detail}${r.paths.length ? `\n ${r.paths.join("\n ")}` : ""}`));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* A router that exits leaves the lid-closed sleep bit as it was, for its successor to reconcile
|
|
546
|
+
* (router awake.ts). When it is stopped for good there is no successor, so clear it here.
|
|
547
|
+
*/
|
|
548
|
+
async function releaseHeldSleep(reason) {
|
|
549
|
+
const { releaseAwake, releaseFailedHint } = await import("./awake-setup.js");
|
|
550
|
+
const { execRunner } = await import("../../router/src/awake.js");
|
|
551
|
+
const r = await releaseAwake(execRunner(), homeDir(), reason);
|
|
552
|
+
if (r === "released")
|
|
553
|
+
console.log(process.platform === "win32" ? "✓ the lid-close action is back to yours (lid-closed awake had set it to Do nothing)" : "✓ sleep re-enabled (lid-closed awake was holding the Mac awake)");
|
|
554
|
+
if (r === "failed")
|
|
555
|
+
console.log(`✗ could not re-enable sleep: ${releaseFailedHint()}`);
|
|
556
|
+
}
|
|
446
557
|
function help() {
|
|
447
558
|
console.log(`clauderipple <command>
|
|
448
559
|
|
|
449
560
|
install [--port N] [--force] [--max-context-tokens N]
|
|
450
561
|
uninstall [--purge]
|
|
451
562
|
status
|
|
563
|
+
doctor [--json] what is wrong with the router, its background service and Claude's connection settings
|
|
564
|
+
repair [--json] fix what doctor finds: re-register and start the router, wait for it to answer
|
|
565
|
+
direct on|off [--force] [--json]
|
|
566
|
+
on: use Claude's default connection (Claude Desktop and Claude Code skip ClaudeRipple;
|
|
567
|
+
the router stays installed); off: go through ClaudeRipple again. Restart Claude after either.
|
|
568
|
+
app --install|--uninstall [--json]
|
|
569
|
+
a double-clickable launcher for the menu-bar app (~/Applications, Start Menu, app menu)
|
|
452
570
|
start | stop | restart
|
|
453
571
|
logs [-n N] [-f]
|
|
572
|
+
remote [install|uninstall|status|pair] run or manage the phone host (macOS, Windows, Linux systemd)
|
|
573
|
+
remote add <ssh-host> [--port N] [--force]
|
|
574
|
+
route Claude Desktop sessions that run on a remote (SSH) host through ClaudeRipple:
|
|
575
|
+
sets up the host over your key-based ssh and keeps a reverse tunnel open (remote port, default 18791)
|
|
576
|
+
remote remove <ssh-host> undo "remote add" on that host and stop its tunnel
|
|
577
|
+
remote hosts list remote (SSH) hosts and their tunnels (also shown by "remote status")
|
|
454
578
|
config print the config file path
|
|
455
579
|
ui open the dashboard in your browser
|
|
456
580
|
tray [--install] start the menu-bar / tray app (--install fetches Electron, ~270MB, once)
|
|
@@ -468,20 +592,71 @@ function help() {
|
|
|
468
592
|
agent-title on|off|status
|
|
469
593
|
prefix subagent titles with the real model and thinking depth ("Terra·high · …") via a Claude Code hook
|
|
470
594
|
mod on|off|status show where each request went above the Claude Code prompt, and the request log in a pane (/ripple-log), via a Claude Code mod
|
|
595
|
+
awake setup [--ssid <hotspot name>] [--remove]
|
|
596
|
+
allow ClaudeRipple to keep the laptop awake with the lid closed (and join that phone hotspot).
|
|
597
|
+
macOS: asks for your password once in macOS's own dialog, and removes Insomnia if it is installed;
|
|
598
|
+
Windows: no administrator step, only checks (power plan, Wi-Fi adapter, saved hotspot profile)
|
|
599
|
+
awake on|off|working [--until HH:MM]
|
|
600
|
+
working: awake while Claude works and 10 min after; on: always (until HH:MM); off: never
|
|
601
|
+
tether on|off join the phone hotspot when Wi-Fi has no internet, and go back to Wi-Fi when a known one works
|
|
602
|
+
power [--json] lid-closed awake and hotspot state
|
|
471
603
|
|
|
472
604
|
Home directory: ${homeDir()} (override with CLAUDERIPPLE_HOME)`);
|
|
473
605
|
}
|
|
474
606
|
try {
|
|
475
607
|
switch (cmd) {
|
|
608
|
+
case "remote": {
|
|
609
|
+
const sub = args[1];
|
|
610
|
+
// Remote (SSH) hosts for Claude Desktop sessions; every other `remote` verb is the phone host's.
|
|
611
|
+
if (sub === "add" || sub === "remove" || sub === "hosts") {
|
|
612
|
+
const { remoteAdd, remoteRemove, remoteHosts, REMOTE_SSH_USAGE } = await import("./remote.js");
|
|
613
|
+
const host = args[2];
|
|
614
|
+
if (sub === "hosts")
|
|
615
|
+
await remoteHosts();
|
|
616
|
+
else if (!host || host.startsWith("--"))
|
|
617
|
+
throw new Error(REMOTE_SSH_USAGE);
|
|
618
|
+
else if (sub === "add") {
|
|
619
|
+
const port = opt("port");
|
|
620
|
+
await remoteAdd(host, { ...(port !== undefined ? { port: Number(port) } : {}), force: flag("force") });
|
|
621
|
+
}
|
|
622
|
+
else
|
|
623
|
+
await remoteRemove(host);
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
const script = path.resolve(here, "../../remote/src/main" + path.extname(fileURLToPath(import.meta.url)));
|
|
627
|
+
const { spawn } = await import("node:child_process");
|
|
628
|
+
const child = spawn(installedRuntime.node, [script, ...args.slice(1)], { stdio: "inherit", env: { ...process.env, ...installedRuntime.env } });
|
|
629
|
+
for (const signal of ["SIGTERM", "SIGINT"])
|
|
630
|
+
process.on(signal, () => child.kill(signal));
|
|
631
|
+
process.exitCode = await new Promise((resolve, reject) => {
|
|
632
|
+
child.once("error", reject);
|
|
633
|
+
child.once("exit", code => resolve(code ?? 1));
|
|
634
|
+
});
|
|
635
|
+
if (sub === "status") {
|
|
636
|
+
// Only when there are any, so the phone host's status reads exactly as before otherwise.
|
|
637
|
+
const { configuredHosts, remoteHosts } = await import("./remote.js");
|
|
638
|
+
if (configuredHosts(configPath()).length > 0) {
|
|
639
|
+
console.log("\nremote (SSH) hosts:");
|
|
640
|
+
await remoteHosts();
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
476
645
|
case "install":
|
|
477
646
|
await install();
|
|
478
647
|
break;
|
|
479
648
|
case "uninstall":
|
|
480
|
-
uninstall();
|
|
649
|
+
await uninstall();
|
|
481
650
|
break;
|
|
482
651
|
case "status":
|
|
483
652
|
await status();
|
|
484
653
|
break;
|
|
654
|
+
case "doctor":
|
|
655
|
+
case "repair":
|
|
656
|
+
case "direct":
|
|
657
|
+
case "app":
|
|
658
|
+
await recovery();
|
|
659
|
+
break;
|
|
485
660
|
case "start": {
|
|
486
661
|
const r = startAgent();
|
|
487
662
|
console.log(r === "already-running" ? "already running" : r === "started" ? "started" : "start failed (is it installed?)");
|
|
@@ -492,6 +667,7 @@ try {
|
|
|
492
667
|
break;
|
|
493
668
|
case "stop":
|
|
494
669
|
console.log(stopAgent() ? "stopped (run `clauderipple start` or `install` to bring it back)" : "stop failed (not loaded?)");
|
|
670
|
+
await releaseHeldSleep("router stopped");
|
|
495
671
|
break;
|
|
496
672
|
case "logs":
|
|
497
673
|
logs();
|
|
@@ -641,6 +817,24 @@ try {
|
|
|
641
817
|
case "image":
|
|
642
818
|
await image();
|
|
643
819
|
break;
|
|
820
|
+
case "power":
|
|
821
|
+
case "awake":
|
|
822
|
+
case "tether": {
|
|
823
|
+
const { powerCli, adminPowerCall } = await import("./power.js");
|
|
824
|
+
const { defaultSetupDeps } = await import("./awake-setup.js");
|
|
825
|
+
const { execRunner } = await import("../../router/src/awake.js");
|
|
826
|
+
const run = execRunner();
|
|
827
|
+
process.exitCode = await powerCli(args, {
|
|
828
|
+
call: adminPowerCall(adminPort(new ConfigStore(configPath()).get())),
|
|
829
|
+
configFile: configPath(),
|
|
830
|
+
rippleHome: homeDir(),
|
|
831
|
+
run,
|
|
832
|
+
setupDeps: () => defaultSetupDeps(run, homeDir()),
|
|
833
|
+
print: (line) => console.log(line),
|
|
834
|
+
now: () => new Date(),
|
|
835
|
+
});
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
644
838
|
case "ui":
|
|
645
839
|
ui();
|
|
646
840
|
break;
|
|
@@ -648,7 +842,9 @@ try {
|
|
|
648
842
|
await update();
|
|
649
843
|
break;
|
|
650
844
|
case "tray": {
|
|
651
|
-
|
|
845
|
+
// --ensure is what the app launcher runs: fetch Electron once if it is missing, then start.
|
|
846
|
+
const ensured = args.includes("--ensure") && !installedRuntime.packaged ? installTrayRuntime() : null;
|
|
847
|
+
const result = args.includes("--install") ? installTrayRuntime() : ensured && !ensured.ok ? ensured : await startTray();
|
|
652
848
|
console.log(result.message);
|
|
653
849
|
if (!result.ok)
|
|
654
850
|
process.exit(1);
|