handmux 0.23.0 → 0.25.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/README.md +31 -8
- package/README.zh-CN.md +31 -8
- package/bin/handmux.js +14 -688
- package/bin/node-version.js +20 -0
- package/dist/bin/handmux.js +1015 -0
- package/dist/connectors/bridgeClient.js +385 -0
- package/dist/connectors/claude/index.js +476 -0
- package/dist/connectors/pi/bridgeClient.js +8 -0
- package/dist/connectors/pi/index.js +851 -0
- package/dist/hooks/handmux-notify.sh +64 -0
- package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
- package/dist/hooks/handmux-write.cjs +264 -0
- package/dist/package.json +127 -0
- package/dist/public/assets/index-CTrqTqtT.css +32 -0
- package/dist/public/assets/index-DmjeMSQA.js +346 -0
- package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
- package/{public → dist/public}/index.html +5 -4
- package/dist/src/agent-runtime/adapter.js +204 -0
- package/dist/src/agent-runtime/bridge.js +1006 -0
- package/dist/src/agent-runtime/bridgeStore.js +25 -0
- package/dist/src/agent-runtime/bridgeTransport.js +619 -0
- package/dist/src/agent-runtime/bridgeTypes.js +13 -0
- package/dist/src/agent-runtime/builtinRuntime.js +193 -0
- package/dist/src/agent-runtime/conversation.js +2179 -0
- package/dist/src/agent-runtime/conversationActivation.js +75 -0
- package/dist/src/agent-runtime/conversationActivity.js +69 -0
- package/dist/src/agent-runtime/conversationControls.js +206 -0
- package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
- package/dist/src/agent-runtime/conversationStore.js +352 -0
- package/dist/src/agent-runtime/conversationTypes.js +1 -0
- package/dist/src/agent-runtime/conversationValidation.js +339 -0
- package/dist/src/agent-runtime/inbox.js +973 -0
- package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
- package/dist/src/agent-runtime/inboxStore.js +25 -0
- package/dist/src/agent-runtime/inboxTypes.js +1 -0
- package/dist/src/agent-runtime/interaction.js +829 -0
- package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
- package/dist/src/agent-runtime/interactionStore.js +18 -0
- package/dist/src/agent-runtime/interactionTypes.js +1 -0
- package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
- package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
- package/dist/src/agent-runtime/resourceTypes.js +1 -0
- package/dist/src/agent-runtime/resources.js +245 -0
- package/dist/src/agent-runtime/run.js +328 -0
- package/dist/src/agent-runtime/runtime.js +793 -0
- package/dist/src/agent-runtime/sessionControl.js +164 -0
- package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
- package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
- package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
- package/dist/src/agents/claude.js +203 -0
- package/dist/src/agents/claudeConversation.js +362 -0
- package/dist/src/agents/claudeInteraction.js +134 -0
- package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
- package/dist/src/agents/codex.js +262 -0
- package/dist/src/agents/codexConversation.js +1103 -0
- package/dist/src/agents/codexConversationActivation.js +121 -0
- package/dist/src/agents/codexConversationControls.js +256 -0
- package/dist/src/agents/codexInteraction.js +306 -0
- package/dist/src/agents/codexSessionControl.js +118 -0
- package/dist/src/agents/codexSubscriptionUsage.js +313 -0
- package/dist/src/agents/codexTerminalControl.js +23 -0
- package/dist/src/agents/conversationProjectionSafety.js +416 -0
- package/dist/src/agents/index.js +39 -0
- package/dist/src/agents/nativeInbox.js +264 -0
- package/dist/src/agents/nativeInboxSources.js +115 -0
- package/dist/src/agents/pi.js +28 -0
- package/dist/src/agents/piConversation.js +523 -0
- package/dist/src/agents/piConversationHistory.js +502 -0
- package/dist/src/agents/piInboxBridge.js +368 -0
- package/dist/src/agents/piSessionControl.js +32 -0
- package/dist/src/agents/processIdentity.js +94 -0
- package/dist/src/agents/scanUtils.js +262 -0
- package/dist/src/apiAccountProviders.js +194 -0
- package/dist/src/apiAccounts.js +592 -0
- package/dist/src/apiErrors.js +86 -0
- package/{src → dist/src}/appName.js +12 -13
- package/dist/src/asr/iflyConfig.js +7 -0
- package/dist/src/asr/iflySign.js +14 -0
- package/dist/src/auth.js +27 -0
- package/dist/src/browser/bootstrap.js +72 -0
- package/dist/src/browser/cookieProfiles.js +413 -0
- package/dist/src/browser/coordinator.js +51 -0
- package/dist/src/browser/hammerheadCompat.js +104 -0
- package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
- package/dist/src/browser/manager.js +560 -0
- package/dist/src/browser/originLabel.js +15 -0
- package/dist/src/browser/profilePersistence.js +235 -0
- package/dist/src/browser/publicProxy.js +176 -0
- package/dist/src/browser/routes.js +159 -0
- package/dist/src/browser/sessionStore.js +72 -0
- package/dist/src/browser/siteVersion.js +121 -0
- package/dist/src/browser/targetPolicy.js +163 -0
- package/{src → dist/src}/browser/worker.js +14 -18
- package/dist/src/browser/workerClient.js +286 -0
- package/dist/src/browser/workerServer.js +91 -0
- package/dist/src/captureBackground.js +95 -0
- package/dist/src/claudeEvents.js +525 -0
- package/dist/src/cli/agentIntegration.js +131 -0
- package/dist/src/cli/agentLauncher.js +26 -0
- package/dist/src/cli/claudeHooks.js +290 -0
- package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
- package/dist/src/cli/cloudflared.js +106 -0
- package/dist/src/cli/codexManaged.js +80 -0
- package/{src → dist/src}/cli/cpolarUrl.js +8 -9
- package/dist/src/cli/drivers.js +98 -0
- package/dist/src/cli/hookScaffold.js +39 -0
- package/dist/src/cli/i18n/en.js +392 -0
- package/{src → dist/src}/cli/i18n/index.js +27 -25
- package/dist/src/cli/i18n/zh.js +391 -0
- package/dist/src/cli/legacyCodexHooks.js +52 -0
- package/{src → dist/src}/cli/natappUrl.js +2 -3
- package/dist/src/cli/options.js +245 -0
- package/dist/src/cli/piExtension.js +155 -0
- package/dist/src/cli/probe.js +16 -0
- package/{src → dist/src}/cli/prompt.js +4 -6
- package/dist/src/cli/pushCmd.js +102 -0
- package/{src → dist/src}/cli/qr.js +19 -19
- package/dist/src/cli/service.js +154 -0
- package/dist/src/cli/setupModel.js +194 -0
- package/dist/src/cli/setupWizard.js +647 -0
- package/dist/src/cli/shortcutEditor.js +351 -0
- package/dist/src/cli/sshTunnel.js +18 -0
- package/dist/src/cli/state.js +117 -0
- package/dist/src/cli/statusLine.js +115 -0
- package/dist/src/cli/supervisor.js +278 -0
- package/dist/src/cli/supervisorHealth.js +21 -0
- package/dist/src/cli/supervisorLaunch.js +148 -0
- package/dist/src/cli/supervisorProcesses.js +62 -0
- package/dist/src/cli/supervisorState.js +62 -0
- package/dist/src/cli/tmuxVersion.js +53 -0
- package/{src → dist/src}/cli/tunlite.js +7 -10
- package/dist/src/cli/tunnelClients.js +72 -0
- package/{src → dist/src}/cli/updateCheck.js +98 -69
- package/{src → dist/src}/cli/urlHost.js +8 -4
- package/dist/src/cli/workspaceCmd.js +347 -0
- package/dist/src/codexAppServer.js +3436 -0
- package/dist/src/codexConversationProjection.js +199 -0
- package/dist/src/codexDiff.js +118 -0
- package/dist/src/codexMessageIdentity.js +18 -0
- package/dist/src/codexPlan.js +30 -0
- package/dist/src/codexQueueProtocol.js +137 -0
- package/dist/src/codexStreamProtocol.js +207 -0
- package/dist/src/codexToolProtocol.js +68 -0
- package/dist/src/codexTranscriptParse.js +835 -0
- package/dist/src/codexUsageSnapshot.js +228 -0
- package/dist/src/config.js +8 -0
- package/{src → dist/src}/docPath.js +22 -24
- package/dist/src/docs.js +337 -0
- package/dist/src/git.js +284 -0
- package/dist/src/healthProtocol.js +97 -0
- package/dist/src/httpApi.js +96 -0
- package/dist/src/jsonStore.js +15 -0
- package/{src → dist/src}/keyNames.js +3 -3
- package/dist/src/notifications.js +111 -0
- package/dist/src/orphans.js +167 -0
- package/dist/src/paneInput.js +42 -0
- package/dist/src/pendingPrompt.js +194 -0
- package/dist/src/previewServer.js +202 -0
- package/dist/src/previews.js +179 -0
- package/dist/src/privateStateStore.js +124 -0
- package/dist/src/projectTask/backup.js +224 -0
- package/dist/src/projectTask/lock.js +108 -0
- package/dist/src/projectTask/migrations.js +90 -0
- package/dist/src/projectTask/routes.js +155 -0
- package/dist/src/projectTask/runtime.js +182 -0
- package/dist/src/projectTask/schema.js +56 -0
- package/dist/src/projectTask/store.js +457 -0
- package/dist/src/push.js +224 -0
- package/dist/src/routes/agents.js +631 -0
- package/dist/src/routes/apiAccounts.js +82 -0
- package/dist/src/routes/files.js +230 -0
- package/dist/src/routes/git.js +89 -0
- package/dist/src/routes/health.js +16 -0
- package/dist/src/routes/notifications.js +16 -0
- package/dist/src/routes/previews.js +43 -0
- package/dist/src/routes/push.js +145 -0
- package/dist/src/routes/sessions.js +244 -0
- package/dist/src/routes/system.js +220 -0
- package/dist/src/routes/terminal.js +244 -0
- package/dist/src/routes/workspace.js +267 -0
- package/dist/src/server.js +315 -0
- package/dist/src/shortcutConfig.js +83 -0
- package/{src → dist/src}/staticCache.js +5 -3
- package/{src → dist/src}/staticCompression.js +5 -6
- package/dist/src/terminalStream.js +441 -0
- package/{src → dist/src}/tmux/commands.js +185 -172
- package/dist/src/tmux/format.js +47 -0
- package/dist/src/transcriptParse.js +261 -0
- package/dist/src/transcriptReader.js +209 -0
- package/{src → dist/src}/trimCapture.js +11 -11
- package/dist/src/uploadTypes.js +31 -0
- package/dist/src/urlPolicy.js +18 -0
- package/dist/src/usagePaths.js +15 -0
- package/dist/src/workspace/agentRunner.js +301 -0
- package/dist/src/workspace/atomicJson.js +31 -0
- package/dist/src/workspace/capture.js +139 -0
- package/dist/src/workspace/checkpointer.js +228 -0
- package/dist/src/workspace/environment.js +71 -0
- package/dist/src/workspace/lock.js +137 -0
- package/dist/src/workspace/mapping.js +128 -0
- package/dist/src/workspace/operations.js +271 -0
- package/dist/src/workspace/paths.js +15 -0
- package/dist/src/workspace/planner.js +292 -0
- package/dist/src/workspace/restore.js +363 -0
- package/dist/src/workspace/runtime.js +318 -0
- package/dist/src/workspace/schema.js +156 -0
- package/dist/src/workspace/store.js +464 -0
- package/dist/src/workspace/tmuxAdapter.js +704 -0
- package/dist/src/workspaceProtocol.js +158 -0
- package/package.json +30 -17
- package/hooks/handmux-notify.sh +0 -22
- package/hooks/handmux-write.cjs +0 -78
- package/public/assets/index-C3N7OkyP.css +0 -32
- package/public/assets/index-D1DWcC5u.js +0 -337
- package/src/.gitkeep +0 -0
- package/src/agents/claude.js +0 -159
- package/src/agents/codex.js +0 -168
- package/src/agents/index.js +0 -21
- package/src/agents/processIdentity.js +0 -72
- package/src/agents/scanUtils.js +0 -225
- package/src/asr/iflyConfig.js +0 -10
- package/src/asr/iflySign.js +0 -16
- package/src/auth.js +0 -30
- package/src/browser/bootstrap.js +0 -68
- package/src/browser/cookieProfiles.js +0 -403
- package/src/browser/coordinator.js +0 -47
- package/src/browser/hammerheadCompat.js +0 -86
- package/src/browser/hammerheadRedirectCompat.js +0 -38
- package/src/browser/manager.js +0 -556
- package/src/browser/originLabel.js +0 -17
- package/src/browser/profilePersistence.js +0 -241
- package/src/browser/publicProxy.js +0 -175
- package/src/browser/routes.js +0 -154
- package/src/browser/sessionStore.js +0 -78
- package/src/browser/siteVersion.js +0 -124
- package/src/browser/targetPolicy.js +0 -137
- package/src/browser/workerClient.js +0 -253
- package/src/browser/workerServer.js +0 -98
- package/src/captureBackground.js +0 -93
- package/src/claudeEvents.js +0 -399
- package/src/cli/claudeHooks.js +0 -206
- package/src/cli/cloudflared.js +0 -99
- package/src/cli/codexManaged.js +0 -77
- package/src/cli/drivers.js +0 -97
- package/src/cli/hookScaffold.js +0 -33
- package/src/cli/i18n/en.js +0 -385
- package/src/cli/i18n/zh.js +0 -384
- package/src/cli/legacyCodexHooks.js +0 -49
- package/src/cli/options.js +0 -211
- package/src/cli/probe.js +0 -16
- package/src/cli/pushCmd.js +0 -60
- package/src/cli/service.js +0 -126
- package/src/cli/setupModel.js +0 -154
- package/src/cli/setupWizard.js +0 -506
- package/src/cli/shortcutEditor.js +0 -288
- package/src/cli/sshTunnel.js +0 -12
- package/src/cli/state.js +0 -90
- package/src/cli/statusLine.js +0 -90
- package/src/cli/supervisor.js +0 -183
- package/src/cli/supervisorProcesses.js +0 -57
- package/src/cli/tmuxVersion.js +0 -49
- package/src/cli/tunnelClients.js +0 -62
- package/src/cli/workspaceCmd.js +0 -300
- package/src/codexAppServer.js +0 -1620
- package/src/codexDiff.js +0 -108
- package/src/codexTranscriptParse.js +0 -413
- package/src/codexUsageSnapshot.cjs +0 -129
- package/src/config.js +0 -9
- package/src/docs.js +0 -228
- package/src/git.js +0 -208
- package/src/httpApi.js +0 -72
- package/src/jsonStore.js +0 -24
- package/src/notifications.js +0 -68
- package/src/orphans.js +0 -152
- package/src/pendingPrompt.js +0 -163
- package/src/previewServer.js +0 -168
- package/src/previews.js +0 -116
- package/src/push.js +0 -183
- package/src/routes/codex.js +0 -473
- package/src/routes/files.js +0 -172
- package/src/routes/git.js +0 -57
- package/src/routes/notifications.js +0 -19
- package/src/routes/previews.js +0 -35
- package/src/routes/push.js +0 -119
- package/src/routes/sessions.js +0 -168
- package/src/routes/system.js +0 -132
- package/src/routes/terminal.js +0 -210
- package/src/routes/transcript.js +0 -171
- package/src/routes/workspace.js +0 -267
- package/src/server.js +0 -186
- package/src/shortcutConfig.js +0 -75
- package/src/terminalStream.js +0 -391
- package/src/tmux/format.js +0 -45
- package/src/transcriptParse.js +0 -180
- package/src/transcriptReader.js +0 -101
- package/src/uploadTypes.js +0 -30
- package/src/urlPolicy.js +0 -14
- package/src/usage.js +0 -260
- package/src/workspace/agentRunner.js +0 -188
- package/src/workspace/atomicJson.js +0 -32
- package/src/workspace/capture.js +0 -139
- package/src/workspace/checkpointer.js +0 -187
- package/src/workspace/environment.js +0 -75
- package/src/workspace/lock.js +0 -128
- package/src/workspace/mapping.js +0 -124
- package/src/workspace/operations.js +0 -252
- package/src/workspace/paths.js +0 -16
- package/src/workspace/planner.js +0 -247
- package/src/workspace/restore.js +0 -279
- package/src/workspace/runtime.js +0 -314
- package/src/workspace/schema.js +0 -136
- package/src/workspace/store.js +0 -382
- package/src/workspace/tmuxAdapter.js +0 -554
- /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
- /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
- /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
- /package/{public → dist/public}/icons/badge-96.png +0 -0
- /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-192.png +0 -0
- /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-512.png +0 -0
- /package/{public → dist/public}/icons/logo.svg +0 -0
- /package/{public → dist/public}/manifest.webmanifest +0 -0
- /package/{public → dist/public}/offline.html +0 -0
- /package/{public → dist/public}/sw.js +0 -0
- /package/{src → dist/src}/browser/protocol.js +0 -0
- /package/{src → dist/src}/cli/cfNamed.js +0 -0
package/src/cli/probe.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// Read-only reachability check: after the tunnel reports live, GET the public URL from THIS machine to
|
|
2
|
-
// confirm the whole chain (edge/reverse-proxy → tunnel → server) actually answers. Touches no remote
|
|
3
|
-
// config — it just fills the "tunnel connected but page won't load" blind spot.
|
|
4
|
-
export async function probe(url, { fetchImpl = globalThis.fetch, timeoutMs = 6000 } = {}) {
|
|
5
|
-
if (!url) return false;
|
|
6
|
-
const ac = new AbortController();
|
|
7
|
-
const t = setTimeout(() => ac.abort(), timeoutMs);
|
|
8
|
-
try {
|
|
9
|
-
await fetchImpl(url, { method: 'GET', redirect: 'manual', signal: ac.signal });
|
|
10
|
-
return true; // any HTTP response (even 401/404) means the chain is reachable
|
|
11
|
-
} catch {
|
|
12
|
-
return false;
|
|
13
|
-
} finally {
|
|
14
|
-
clearTimeout(t);
|
|
15
|
-
}
|
|
16
|
-
}
|
package/src/cli/pushCmd.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
// `handmux push <title> <body> [--session X]... [--device K]... [--tag T] [--url U]` — fire one
|
|
2
|
-
// notification to the phone through the already-running server (loopback + the server's own token).
|
|
3
|
-
// Scope is mutually exclusive: --device (by key) or --session, else all. Pure parse + injectable
|
|
4
|
-
// runner so it unit-tests without spawning or real fetch.
|
|
5
|
-
import { readState } from './state.js';
|
|
6
|
-
import { sanitizeNotificationUrl } from '../urlPolicy.js';
|
|
7
|
-
|
|
8
|
-
const collect = (acc, v) => acc.concat(String(v).split(',').map((s) => s.trim()).filter(Boolean));
|
|
9
|
-
|
|
10
|
-
// argv is process.argv.slice(2), i.e. ['push', title, body, ...flags]. The shared parseArgs() drops bare
|
|
11
|
-
// words after the command, so title/body are taken positionally; --session/--device may repeat.
|
|
12
|
-
export function parsePushArgs(argv) {
|
|
13
|
-
const rest = argv.slice(1);
|
|
14
|
-
const positional = [];
|
|
15
|
-
let sessions = [], devices = [], tag, url;
|
|
16
|
-
for (let i = 0; i < rest.length; i++) {
|
|
17
|
-
const a = rest[i];
|
|
18
|
-
if (a === '--session') { sessions = collect(sessions, rest[++i]); }
|
|
19
|
-
else if (a === '--device') { devices = collect(devices, rest[++i]); }
|
|
20
|
-
else if (a === '--tag') { tag = rest[++i]; }
|
|
21
|
-
else if (a === '--url') { url = rest[++i]; }
|
|
22
|
-
else if (!a.startsWith('--')) positional.push(a);
|
|
23
|
-
}
|
|
24
|
-
const [title, body] = positional;
|
|
25
|
-
if (!title || !body) return { error: 'usage: handmux push <title> <body> [--session X]... [--device K]... [--tag T] [--url U]' };
|
|
26
|
-
if (sessions.length && devices.length) return { error: 'use --session or --device, not both' };
|
|
27
|
-
const safeUrl = url == null ? null : sanitizeNotificationUrl(url);
|
|
28
|
-
if (url != null && !safeUrl) return { error: '--url must be an http(s) URL or a relative path' };
|
|
29
|
-
const out = { title, body };
|
|
30
|
-
if (sessions.length) out.sessions = sessions;
|
|
31
|
-
if (devices.length) out.devices = devices;
|
|
32
|
-
if (tag) out.tag = tag;
|
|
33
|
-
if (safeUrl) out.url = safeUrl;
|
|
34
|
-
return out;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function runPush({ argv, home, fetchImpl = globalThis.fetch, log = console.log, err = console.error }) {
|
|
38
|
-
const parsed = parsePushArgs(argv);
|
|
39
|
-
if (parsed.error) { err(parsed.error); return 1; }
|
|
40
|
-
const st = readState(home);
|
|
41
|
-
if (!st || !st.localUrl || !st.token) { err('handmux is not running — run `handmux start` first.'); return 1; }
|
|
42
|
-
try {
|
|
43
|
-
const res = await fetchImpl(`${st.localUrl}/api/push/send-local`, {
|
|
44
|
-
method: 'POST',
|
|
45
|
-
headers: { Authorization: `Bearer ${st.token}`, 'Content-Type': 'application/json' },
|
|
46
|
-
body: JSON.stringify(parsed),
|
|
47
|
-
});
|
|
48
|
-
if (!res.ok) { err(`push failed: ${res.status}`); return 1; }
|
|
49
|
-
const out = await res.json();
|
|
50
|
-
if (out.configured === false) { err('push is not configured (no VAPID keys) — run `handmux setup`.'); return 1; }
|
|
51
|
-
const sent = Number.isFinite(out.sent) ? out.sent : 0;
|
|
52
|
-
const failed = Number.isFinite(out.failed) ? out.failed : 0;
|
|
53
|
-
const gone = Number.isFinite(out.gone) ? out.gone : 0;
|
|
54
|
-
const counts = `sent: ${sent}, failed: ${failed}, gone: ${gone}`;
|
|
55
|
-
if (sent === 0) { err(`no notification delivered (${counts})`); return 1; }
|
|
56
|
-
if (failed > 0) { err(`push partially failed (${counts})`); return 1; }
|
|
57
|
-
log(`pushed (${counts})`);
|
|
58
|
-
return 0;
|
|
59
|
-
} catch (e) { err(`push failed: ${e.message}`); return 1; }
|
|
60
|
-
}
|
package/src/cli/service.js
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
// Boot/login autostart. The OS keeps the handmux SUPERVISOR (`__supervise`) alive; the supervisor
|
|
2
|
-
// keeps server + tunnel alive — same model as a foreground run, just parented by launchd/systemd.
|
|
3
|
-
// The intended config is baked into the service file as a base64 payload (self-contained: no reliance
|
|
4
|
-
// on config.json). Text generators are pure (unit-tested); the launchctl/systemctl calls inject `exec`.
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { spawnSync } from 'node:child_process';
|
|
8
|
-
import { pocketHome, logPath } from './state.js';
|
|
9
|
-
|
|
10
|
-
export const LABEL = 'com.handmux.agent';
|
|
11
|
-
export const UNIT = 'handmux.service';
|
|
12
|
-
|
|
13
|
-
export function plistPath(home) { return path.join(home, 'Library', 'LaunchAgents', `${LABEL}.plist`); }
|
|
14
|
-
export function unitPath(home) { return path.join(home, '.config', 'systemd', 'user', UNIT); }
|
|
15
|
-
|
|
16
|
-
export function isServiceInstalled(home, platform = process.platform) {
|
|
17
|
-
if (platform === 'darwin') return fs.existsSync(plistPath(home));
|
|
18
|
-
if (platform === 'linux') return fs.existsSync(unitPath(home));
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const xmlEscape = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
23
|
-
|
|
24
|
-
// launchd LaunchAgent. args = full argv for the process (node, script, __supervise, --payload, b64).
|
|
25
|
-
export function plistFor({ args, log, label = LABEL }) {
|
|
26
|
-
const items = args.map((a) => ` <string>${xmlEscape(a)}</string>`).join('\n');
|
|
27
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
28
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
29
|
-
<plist version="1.0">
|
|
30
|
-
<dict>
|
|
31
|
-
<key>Label</key><string>${label}</string>
|
|
32
|
-
<key>ProgramArguments</key>
|
|
33
|
-
<array>
|
|
34
|
-
${items}
|
|
35
|
-
</array>
|
|
36
|
-
<key>RunAtLoad</key><true/>
|
|
37
|
-
<key>KeepAlive</key><true/>
|
|
38
|
-
<key>StandardOutPath</key><string>${xmlEscape(log)}</string>
|
|
39
|
-
<key>StandardErrorPath</key><string>${xmlEscape(log)}</string>
|
|
40
|
-
</dict>
|
|
41
|
-
</plist>
|
|
42
|
-
`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// systemd --user unit. ExecStart needs a single command line; args are space-joined (our args have no
|
|
46
|
-
// spaces except an absolute path with none in practice — quote the script path defensively).
|
|
47
|
-
export function unitFor({ args }) {
|
|
48
|
-
const cmd = args.map((a) => (/\s/.test(a) ? JSON.stringify(a) : a)).join(' ');
|
|
49
|
-
return `[Unit]
|
|
50
|
-
Description=handmux — drive your tmux from your phone
|
|
51
|
-
After=network-online.target
|
|
52
|
-
|
|
53
|
-
[Service]
|
|
54
|
-
ExecStart=${cmd}
|
|
55
|
-
Restart=always
|
|
56
|
-
RestartSec=2
|
|
57
|
-
|
|
58
|
-
[Install]
|
|
59
|
-
WantedBy=default.target
|
|
60
|
-
`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function installService(args, { home, platform = process.platform, exec = spawnSync, log = console } = {}) {
|
|
64
|
-
if (platform === 'darwin') {
|
|
65
|
-
const p = plistPath(home);
|
|
66
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
67
|
-
fs.writeFileSync(p, plistFor({ args, log: logPath(home) }));
|
|
68
|
-
exec('launchctl', ['unload', p], { stdio: 'ignore' }); // best-effort: clear any prior load
|
|
69
|
-
const r = exec('launchctl', ['load', '-w', p], { encoding: 'utf8' });
|
|
70
|
-
if (r.status !== 0) throw new Error(`launchctl load failed: ${r.stderr || r.status}`);
|
|
71
|
-
log.log?.(`installed launchd agent: ${p}`);
|
|
72
|
-
return p;
|
|
73
|
-
}
|
|
74
|
-
if (platform === 'linux') {
|
|
75
|
-
const p = unitPath(home);
|
|
76
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
77
|
-
fs.writeFileSync(p, unitFor({ args }));
|
|
78
|
-
exec('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore' });
|
|
79
|
-
const enabled = exec('systemctl', ['--user', 'enable', UNIT], { encoding: 'utf8' });
|
|
80
|
-
if (enabled.status !== 0) throw new Error(`systemctl enable failed: ${enabled.stderr || enabled.status}`);
|
|
81
|
-
// `enable --now` does not restart an already-active unit. Always restart after rewriting ExecStart so
|
|
82
|
-
// an npm/brew upgrade (new CLI/Node path) and a changed baked config take effect immediately.
|
|
83
|
-
const restarted = exec('systemctl', ['--user', 'restart', UNIT], { encoding: 'utf8' });
|
|
84
|
-
if (restarted.status !== 0) throw new Error(`systemctl restart failed: ${restarted.stderr || restarted.status}`);
|
|
85
|
-
log.log?.(`installed systemd --user unit: ${p}`);
|
|
86
|
-
log.log?.('(for autostart before login: loginctl enable-linger "$USER")');
|
|
87
|
-
return p;
|
|
88
|
-
}
|
|
89
|
-
throw new Error(`autostart not supported on ${platform} yet`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Stop the currently-loaded service without removing/disable-ing its login registration. This is what the
|
|
93
|
-
// ordinary `handmux stop` command needs: stay stopped for this login session, then start normally again at
|
|
94
|
-
// the next login/boot. In particular, do NOT SIGTERM the supervisor directly while KeepAlive/Restart=always
|
|
95
|
-
// is active — the service manager would immediately resurrect it and race a second manual supervisor.
|
|
96
|
-
export function stopService({ home, platform = process.platform, exec = spawnSync } = {}) {
|
|
97
|
-
if (platform === 'darwin') {
|
|
98
|
-
const r = exec('launchctl', ['unload', plistPath(home)], { encoding: 'utf8' });
|
|
99
|
-
// unload returns non-zero when it was already unloaded; `stop` stays idempotent.
|
|
100
|
-
return r.status === 0;
|
|
101
|
-
}
|
|
102
|
-
if (platform === 'linux') {
|
|
103
|
-
const r = exec('systemctl', ['--user', 'stop', UNIT], { encoding: 'utf8' });
|
|
104
|
-
if (r.status !== 0) throw new Error(`systemctl stop failed: ${r.stderr || r.status}`);
|
|
105
|
-
return true;
|
|
106
|
-
}
|
|
107
|
-
throw new Error(`autostart not supported on ${platform} yet`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function uninstallService({ home, platform = process.platform, exec = spawnSync, log = console } = {}) {
|
|
111
|
-
if (platform === 'darwin') {
|
|
112
|
-
const p = plistPath(home);
|
|
113
|
-
exec('launchctl', ['unload', '-w', p], { stdio: 'ignore' });
|
|
114
|
-
try { fs.unlinkSync(p); } catch { /* already gone */ }
|
|
115
|
-
log.log?.(`removed launchd agent: ${p}`);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
if (platform === 'linux') {
|
|
119
|
-
exec('systemctl', ['--user', 'disable', '--now', UNIT], { stdio: 'ignore' });
|
|
120
|
-
try { fs.unlinkSync(unitPath(home)); } catch { /* already gone */ }
|
|
121
|
-
exec('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore' });
|
|
122
|
-
log.log?.(`removed systemd --user unit: ${unitPath(home)}`);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
throw new Error(`autostart not supported on ${platform} yet`);
|
|
126
|
-
}
|
package/src/cli/setupModel.js
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
// Pure model for `handmux setup`: the answer↔config mappers, the wizard-owned key sets, the connection
|
|
2
|
-
// summary, the cloudflared provisioning text helpers, and the clack validators. Extracted from the
|
|
3
|
-
// interactive shell (setupWizard.js) so the tested, side-effect-free logic stands on its own. Depends only
|
|
4
|
-
// on i18n (t/getLocale) for user-facing strings — otherwise deterministic given its inputs.
|
|
5
|
-
import { t, getLocale } from './i18n/index.js';
|
|
6
|
-
|
|
7
|
-
// ~/.cloudflared/config.yml for a named tunnel: route the hostname to the local handmux port.
|
|
8
|
-
export function cfConfigYaml({ tunnelName, credentialsFile, hostname, port }) {
|
|
9
|
-
return [
|
|
10
|
-
`tunnel: ${tunnelName}`,
|
|
11
|
-
`credentials-file: ${credentialsFile}`,
|
|
12
|
-
'ingress:',
|
|
13
|
-
` - hostname: ${hostname}`,
|
|
14
|
-
` service: http://localhost:${port}`,
|
|
15
|
-
' - service: http_status:404',
|
|
16
|
-
'',
|
|
17
|
-
].join('\n');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Extract the tunnel UUID + credentials path from `cloudflared tunnel create <name>` stdout.
|
|
21
|
-
export function parseTunnelCreate(out) {
|
|
22
|
-
const s = String(out || '');
|
|
23
|
-
const id = (s.match(/Created tunnel \S+ with id ([0-9a-fA-F-]+)/) || [])[1] || null;
|
|
24
|
-
const credentialsFile = (s.match(/credentials written to (\S+\.json)/i) || [])[1]?.replace(/\.$/, '') || null;
|
|
25
|
-
return { id, credentialsFile };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Find an existing named tunnel's UUID in `cloudflared tunnel list --output json`. A named tunnel is a
|
|
29
|
-
// PERSISTENT object in the Cloudflare account, so a second `setup` would hit `cloudflared tunnel create`'s
|
|
30
|
-
// "tunnel with name X already exists" error — which used to force a rename and pile up junk tunnels in the
|
|
31
|
-
// account. Looking it up first lets provisioning REUSE it idempotently. Tolerates non-JSON / errors → null.
|
|
32
|
-
export function findTunnelId(listJsonOut, name) {
|
|
33
|
-
let arr;
|
|
34
|
-
try { arr = JSON.parse(String(listJsonOut || '')); } catch { return null; }
|
|
35
|
-
if (!Array.isArray(arr)) return null;
|
|
36
|
-
const hit = arr.find((tn) => tn && tn.name === name);
|
|
37
|
-
return hit?.id || null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// The config keys the wizard owns: everything it can set. mergeConfig wipes these from the existing config
|
|
41
|
-
// before re-applying the answers, so switching a tunnel (or clearing an optional field) cleanly drops the
|
|
42
|
-
// old value instead of leaving a stale field behind. Anything NOT here (staticDir, uploadExts…) is
|
|
43
|
-
// preserved untouched. `token` IS owned so the Token row can pin one AND clear it back to auto — but it
|
|
44
|
-
// round-trips through answersFromConfig, so a re-run that never touches the row still writes it back.
|
|
45
|
-
const WIZARD_KEYS = [
|
|
46
|
-
'lang', 'name', 'port', 'tunnel', 'token', 'previewDomain',
|
|
47
|
-
'sshHost', 'remotePort', 'sshJump', 'cfHostname', 'cfTunnelName', 'publicUrl',
|
|
48
|
-
'authtoken', 'cpolarRegion',
|
|
49
|
-
'vapid', 'xfyun',
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
// The tunnel-specific keys, cleared whenever the tunnel changes so a switch never carries a stale field.
|
|
53
|
-
export const TUNNEL_KEYS = ['sshHost', 'remotePort', 'sshJump', 'cfHostname', 'cfTunnelName', 'publicUrl', 'authtoken', 'cpolarRegion'];
|
|
54
|
-
|
|
55
|
-
// Wizard answers → the config fragment the user actually set (omit empty optional fields).
|
|
56
|
-
export function configFromAnswers(a) {
|
|
57
|
-
const cfg = { tunnel: a.tunnel, port: a.port };
|
|
58
|
-
if (a.lang) cfg.lang = a.lang;
|
|
59
|
-
if (a.name) cfg.name = a.name;
|
|
60
|
-
if (a.token) cfg.token = a.token; // blank = don't pin one → the server mints a fresh token each start
|
|
61
|
-
if (a.previewDomain) cfg.previewDomain = a.previewDomain;
|
|
62
|
-
if (a.tunnel === 'ssh') {
|
|
63
|
-
cfg.sshHost = a.sshHost;
|
|
64
|
-
cfg.remotePort = a.remotePort;
|
|
65
|
-
if (a.publicUrl) cfg.publicUrl = a.publicUrl;
|
|
66
|
-
if (a.sshJump) cfg.sshJump = a.sshJump;
|
|
67
|
-
}
|
|
68
|
-
if (a.tunnel === 'cloudflare-named') {
|
|
69
|
-
cfg.cfHostname = a.cfHostname;
|
|
70
|
-
cfg.cfTunnelName = a.cfTunnelName;
|
|
71
|
-
}
|
|
72
|
-
if (a.tunnel === 'natapp' || a.tunnel === 'cpolar') {
|
|
73
|
-
if (a.authtoken) cfg.authtoken = a.authtoken;
|
|
74
|
-
if (a.publicUrl) cfg.publicUrl = a.publicUrl; // fixed/reserved domain (blank = temporary random)
|
|
75
|
-
if (a.cpolarRegion) cfg.cpolarRegion = a.cpolarRegion;
|
|
76
|
-
}
|
|
77
|
-
if (a.vapid) cfg.vapid = a.vapid;
|
|
78
|
-
if (a.xfyun) cfg.xfyun = a.xfyun;
|
|
79
|
-
return cfg;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Fold this run's answers into an existing config: preserve every non-wizard field, replace the wizard's
|
|
83
|
-
// own fields wholesale. This is why re-running `setup` to switch tunnels (or edit the name) never drops
|
|
84
|
-
// your token / push keys / static dir, yet also never leaves the previous tunnel's stale keys around.
|
|
85
|
-
export function mergeConfig(existing = {}, answers) {
|
|
86
|
-
const out = { ...existing };
|
|
87
|
-
for (const k of WIZARD_KEYS) delete out[k];
|
|
88
|
-
return { ...out, ...configFromAnswers(answers) };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Seed the working answers from an existing config so the hub shows current values and each edit starts
|
|
92
|
-
// from what's already there. A brand-new config yields safe defaults (none/LAN, port 19999).
|
|
93
|
-
export function answersFromConfig(cfg = {}) {
|
|
94
|
-
const a = {
|
|
95
|
-
lang: cfg.lang || getLocale(),
|
|
96
|
-
name: cfg.name || '',
|
|
97
|
-
token: cfg.token || '', // '' = not pinned (auto each start); seeded so an untouched re-run rewrites it
|
|
98
|
-
previewDomain: cfg.previewDomain || '',
|
|
99
|
-
tunnel: cfg.tunnel || 'none',
|
|
100
|
-
port: Number(cfg.port) || 19999,
|
|
101
|
-
};
|
|
102
|
-
for (const k of [...TUNNEL_KEYS, 'vapid', 'xfyun']) if (cfg[k] != null) a[k] = cfg[k];
|
|
103
|
-
return a;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// The dim one-line summary shown next to the Connection row (and reused nowhere else). Pure.
|
|
107
|
-
export function summarizeConnection(a) {
|
|
108
|
-
const bare = (u) => String(u || '').replace(/^https?:\/\//, '');
|
|
109
|
-
switch (a.tunnel) {
|
|
110
|
-
case 'cloudflare': return `cloudflare · ${t('setup.sumTemp')}`;
|
|
111
|
-
case 'cloudflare-named': return `cloudflare-named · ${a.cfHostname || '?'}`;
|
|
112
|
-
case 'ssh': return `ssh · ${a.sshHost || '?'}`;
|
|
113
|
-
case 'natapp':
|
|
114
|
-
case 'cpolar': return `${a.tunnel} · ${a.publicUrl ? `${t('setup.sumFixed')} ${bare(a.publicUrl)}` : t('setup.sumTemp')}`;
|
|
115
|
-
default: return `${t('setup.tunnelNone')} · ${t('setup.sumNoRelay')}`;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ---- validators (clack: return a string to reject + show inline, undefined to accept). Pure. ----
|
|
120
|
-
export function validatePort(v) {
|
|
121
|
-
const n = Number(v);
|
|
122
|
-
if (!Number.isInteger(n) || n < 1 || n > 65535) return t('setup.valPort');
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
125
|
-
export const validateNonEmpty = (label) => (v) => (String(v || '').trim() ? undefined : t('setup.valRequired', { label }));
|
|
126
|
-
export function validateHost(v) {
|
|
127
|
-
const s = String(v || '').trim().replace(/^https?:\/\//, '').replace(/\/.*$/, '');
|
|
128
|
-
return /^[a-z0-9-]+(\.[a-z0-9-]+)+$/i.test(s) ? undefined : t('setup.valHost');
|
|
129
|
-
}
|
|
130
|
-
// The built-in browser maps proxy origins below this domain. Blank deliberately leaves proxy mode disabled;
|
|
131
|
-
// a URL, wildcard, or port would produce an invalid host when Handmux adds its own stable subdomain.
|
|
132
|
-
export function validatePreviewDomain(v) {
|
|
133
|
-
const s = String(v || '').trim();
|
|
134
|
-
if (!s) return undefined;
|
|
135
|
-
return /^[a-z0-9-]+(\.[a-z0-9-]+)+$/i.test(s) ? undefined : t('setup.valPreviewDomain');
|
|
136
|
-
}
|
|
137
|
-
// VAPID subject: Apple (APNs) rejects a fake/.local domain with BadJwtToken, so require a real-looking
|
|
138
|
-
// mailto:you@host.tld or an https:// URL and reject the known-bad .local. Keeps push from silently
|
|
139
|
-
// failing on iOS. (Can't fully validate "real" client-side — this just catches the obvious footguns.)
|
|
140
|
-
export function validateContact(v) {
|
|
141
|
-
const s = String(v || '').trim();
|
|
142
|
-
const wellFormed = /^mailto:[^@\s]+@[a-z0-9-]+(\.[a-z0-9-]+)+$/i.test(s)
|
|
143
|
-
|| /^https:\/\/[a-z0-9-]+(\.[a-z0-9-]+)+/i.test(s);
|
|
144
|
-
if (!wellFormed || /\.local(?:[:/]|$)/i.test(s)) return t('setup.valContact');
|
|
145
|
-
return undefined;
|
|
146
|
-
}
|
|
147
|
-
// Access token: it rides in the phone's URL as ?token=…, so require something and reject whitespace (a space
|
|
148
|
-
// would break the link). Length/charset are otherwise up to the user — a pinned token is used verbatim.
|
|
149
|
-
export function validateToken(v) {
|
|
150
|
-
const s = String(v || '').trim();
|
|
151
|
-
if (!s) return t('setup.valToken');
|
|
152
|
-
if (/\s/.test(s)) return t('setup.valTokenSpace');
|
|
153
|
-
return undefined;
|
|
154
|
-
}
|