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
|
@@ -0,0 +1,154 @@
|
|
|
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 service definition receives only a private supervisor-config path. Secrets stay out of argv, plist,
|
|
4
|
+
// and unit text. 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
|
+
import { ensurePrivateDirectorySync } from '../privateStateStore.js';
|
|
10
|
+
export const LABEL = 'com.handmux.agent';
|
|
11
|
+
export const UNIT = 'handmux.service';
|
|
12
|
+
const defaultExec = (command, args, options) => {
|
|
13
|
+
const result = options.encoding
|
|
14
|
+
? spawnSync(command, [...args], { encoding: options.encoding })
|
|
15
|
+
: spawnSync(command, [...args], { stdio: options.stdio });
|
|
16
|
+
return { status: result.status, stderr: result.stderr };
|
|
17
|
+
};
|
|
18
|
+
const failureDetail = (result) => String(result.stderr || result.status);
|
|
19
|
+
export function plistPath(home) { return path.join(home, 'Library', 'LaunchAgents', `${LABEL}.plist`); }
|
|
20
|
+
export function unitPath(home) { return path.join(home, '.config', 'systemd', 'user', UNIT); }
|
|
21
|
+
export function isServiceInstalled(home, platform = process.platform) {
|
|
22
|
+
if (platform === 'darwin')
|
|
23
|
+
return fs.existsSync(plistPath(home));
|
|
24
|
+
if (platform === 'linux')
|
|
25
|
+
return fs.existsSync(unitPath(home));
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const xmlEscape = (value) => value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
29
|
+
// launchd does not inherit the interactive shell PATH. Persist the caller's PATH so the supervised Server
|
|
30
|
+
// resolves the same npm/Homebrew/nvm/volta Agent executables that `handmux start` could resolve.
|
|
31
|
+
// args = full argv for the process (node, script, __supervise, --payload-file, path).
|
|
32
|
+
export function plistFor({ args, log, label = LABEL, pathEnv }) {
|
|
33
|
+
const items = args.map((a) => ` <string>${xmlEscape(a)}</string>`).join('\n');
|
|
34
|
+
const environment = pathEnv ? `
|
|
35
|
+
<key>EnvironmentVariables</key>
|
|
36
|
+
<dict>
|
|
37
|
+
<key>PATH</key><string>${xmlEscape(pathEnv)}</string>
|
|
38
|
+
</dict>` : '';
|
|
39
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
40
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
41
|
+
<plist version="1.0">
|
|
42
|
+
<dict>
|
|
43
|
+
<key>Label</key><string>${label}</string>
|
|
44
|
+
<key>ProgramArguments</key>
|
|
45
|
+
<array>
|
|
46
|
+
${items}
|
|
47
|
+
</array>
|
|
48
|
+
${environment}
|
|
49
|
+
<key>RunAtLoad</key><true/>
|
|
50
|
+
<key>KeepAlive</key><true/>
|
|
51
|
+
<key>StandardOutPath</key><string>${xmlEscape(log)}</string>
|
|
52
|
+
<key>StandardErrorPath</key><string>${xmlEscape(log)}</string>
|
|
53
|
+
</dict>
|
|
54
|
+
</plist>
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
// systemd --user may likewise start with a manager PATH rather than the invoking shell's. ExecStart needs
|
|
58
|
+
// a single command line; args are space-joined (our args have no spaces except an absolute path with none
|
|
59
|
+
// in practice — quote the script path defensively).
|
|
60
|
+
export function unitFor({ args, pathEnv }) {
|
|
61
|
+
const cmd = args.map((a) => (/\s/.test(a) ? JSON.stringify(a) : a)).join(' ');
|
|
62
|
+
const environment = pathEnv ? `Environment=${JSON.stringify(`PATH=${pathEnv}`)}\n` : '';
|
|
63
|
+
return `[Unit]
|
|
64
|
+
Description=handmux — drive your tmux from your phone
|
|
65
|
+
After=network-online.target
|
|
66
|
+
|
|
67
|
+
[Service]
|
|
68
|
+
ExecStart=${cmd}
|
|
69
|
+
${environment}Restart=always
|
|
70
|
+
RestartSec=2
|
|
71
|
+
|
|
72
|
+
[Install]
|
|
73
|
+
WantedBy=default.target
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
export function installService(args, { home, platform = process.platform, exec = defaultExec, log = console, pathEnv = process.env.PATH, }) {
|
|
77
|
+
ensurePrivateDirectorySync(pocketHome(home));
|
|
78
|
+
const runtimeLog = logPath(home);
|
|
79
|
+
const descriptor = fs.openSync(runtimeLog, 'a', 0o600);
|
|
80
|
+
fs.closeSync(descriptor);
|
|
81
|
+
fs.chmodSync(runtimeLog, 0o600);
|
|
82
|
+
if (platform === 'darwin') {
|
|
83
|
+
const p = plistPath(home);
|
|
84
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
85
|
+
fs.writeFileSync(p, plistFor({ args, log: runtimeLog, ...(pathEnv ? { pathEnv } : {}) }), { mode: 0o600 });
|
|
86
|
+
fs.chmodSync(p, 0o600);
|
|
87
|
+
exec('launchctl', ['unload', p], { stdio: 'ignore' }); // best-effort: clear any prior load
|
|
88
|
+
const r = exec('launchctl', ['load', '-w', p], { encoding: 'utf8' });
|
|
89
|
+
if (r.status !== 0)
|
|
90
|
+
throw new Error(`launchctl load failed: ${failureDetail(r)}`);
|
|
91
|
+
log.log?.(`installed launchd agent: ${p}`);
|
|
92
|
+
return p;
|
|
93
|
+
}
|
|
94
|
+
if (platform === 'linux') {
|
|
95
|
+
const p = unitPath(home);
|
|
96
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
97
|
+
fs.writeFileSync(p, unitFor({ args, ...(pathEnv ? { pathEnv } : {}) }), { mode: 0o600 });
|
|
98
|
+
fs.chmodSync(p, 0o600);
|
|
99
|
+
exec('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore' });
|
|
100
|
+
const enabled = exec('systemctl', ['--user', 'enable', UNIT], { encoding: 'utf8' });
|
|
101
|
+
if (enabled.status !== 0)
|
|
102
|
+
throw new Error(`systemctl enable failed: ${failureDetail(enabled)}`);
|
|
103
|
+
// `enable --now` does not restart an already-active unit. Always restart after rewriting ExecStart so
|
|
104
|
+
// an npm/brew upgrade (new CLI/Node path) and a changed baked config take effect immediately.
|
|
105
|
+
const restarted = exec('systemctl', ['--user', 'restart', UNIT], { encoding: 'utf8' });
|
|
106
|
+
if (restarted.status !== 0)
|
|
107
|
+
throw new Error(`systemctl restart failed: ${failureDetail(restarted)}`);
|
|
108
|
+
log.log?.(`installed systemd --user unit: ${p}`);
|
|
109
|
+
log.log?.('(for autostart before login: loginctl enable-linger "$USER")');
|
|
110
|
+
return p;
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`autostart not supported on ${platform} yet`);
|
|
113
|
+
}
|
|
114
|
+
// Stop the currently-loaded service without removing/disable-ing its login registration. This is what the
|
|
115
|
+
// ordinary `handmux stop` command needs: stay stopped for this login session, then start normally again at
|
|
116
|
+
// the next login/boot. In particular, do NOT SIGTERM the supervisor directly while KeepAlive/Restart=always
|
|
117
|
+
// is active — the service manager would immediately resurrect it and race a second manual supervisor.
|
|
118
|
+
export function stopService({ home, platform = process.platform, exec = defaultExec }) {
|
|
119
|
+
if (platform === 'darwin') {
|
|
120
|
+
const r = exec('launchctl', ['unload', plistPath(home)], { encoding: 'utf8' });
|
|
121
|
+
// unload returns non-zero when it was already unloaded; `stop` stays idempotent.
|
|
122
|
+
return r.status === 0;
|
|
123
|
+
}
|
|
124
|
+
if (platform === 'linux') {
|
|
125
|
+
const r = exec('systemctl', ['--user', 'stop', UNIT], { encoding: 'utf8' });
|
|
126
|
+
if (r.status !== 0)
|
|
127
|
+
throw new Error(`systemctl stop failed: ${failureDetail(r)}`);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
throw new Error(`autostart not supported on ${platform} yet`);
|
|
131
|
+
}
|
|
132
|
+
export function uninstallService({ home, platform = process.platform, exec = defaultExec, log = console }) {
|
|
133
|
+
if (platform === 'darwin') {
|
|
134
|
+
const p = plistPath(home);
|
|
135
|
+
exec('launchctl', ['unload', '-w', p], { stdio: 'ignore' });
|
|
136
|
+
try {
|
|
137
|
+
fs.unlinkSync(p);
|
|
138
|
+
}
|
|
139
|
+
catch { /* already gone */ }
|
|
140
|
+
log.log?.(`removed launchd agent: ${p}`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (platform === 'linux') {
|
|
144
|
+
exec('systemctl', ['--user', 'disable', '--now', UNIT], { stdio: 'ignore' });
|
|
145
|
+
try {
|
|
146
|
+
fs.unlinkSync(unitPath(home));
|
|
147
|
+
}
|
|
148
|
+
catch { /* already gone */ }
|
|
149
|
+
exec('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore' });
|
|
150
|
+
log.log?.(`removed systemd --user unit: ${unitPath(home)}`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
throw new Error(`autostart not supported on ${platform} yet`);
|
|
154
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
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.ts) 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
|
+
import { TUNNELS } from './options.js';
|
|
7
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
const isTunnel = (value) => typeof value === 'string' && TUNNELS.includes(value);
|
|
9
|
+
const optionalString = (value) => typeof value === 'string' && value ? value : undefined;
|
|
10
|
+
const stringObject = (value, keys) => {
|
|
11
|
+
if (!isRecord(value))
|
|
12
|
+
return undefined;
|
|
13
|
+
const result = {};
|
|
14
|
+
for (const key of keys)
|
|
15
|
+
if (typeof value[key] === 'string')
|
|
16
|
+
result[key] = value[key];
|
|
17
|
+
return result;
|
|
18
|
+
};
|
|
19
|
+
// ~/.cloudflared/config.yml for a named tunnel: route the hostname to the local handmux port.
|
|
20
|
+
export function cfConfigYaml({ tunnelName, credentialsFile, hostname, port, }) {
|
|
21
|
+
return [
|
|
22
|
+
`tunnel: ${tunnelName}`,
|
|
23
|
+
`credentials-file: ${credentialsFile}`,
|
|
24
|
+
'ingress:',
|
|
25
|
+
` - hostname: ${hostname}`,
|
|
26
|
+
` service: http://localhost:${port}`,
|
|
27
|
+
' - service: http_status:404',
|
|
28
|
+
'',
|
|
29
|
+
].join('\n');
|
|
30
|
+
}
|
|
31
|
+
// Extract the tunnel UUID + credentials path from `cloudflared tunnel create <name>` stdout.
|
|
32
|
+
export function parseTunnelCreate(out) {
|
|
33
|
+
const s = String(out || '');
|
|
34
|
+
const id = (s.match(/Created tunnel \S+ with id ([0-9a-fA-F-]+)/) || [])[1] || null;
|
|
35
|
+
const credentialsFile = (s.match(/credentials written to (\S+\.json)/i) || [])[1]?.replace(/\.$/, '') || null;
|
|
36
|
+
return { id, credentialsFile };
|
|
37
|
+
}
|
|
38
|
+
// Find an existing named tunnel's UUID in `cloudflared tunnel list --output json`. A named tunnel is a
|
|
39
|
+
// PERSISTENT object in the Cloudflare account, so a second `setup` would hit `cloudflared tunnel create`'s
|
|
40
|
+
// "tunnel with name X already exists" error — which used to force a rename and pile up junk tunnels in the
|
|
41
|
+
// account. Looking it up first lets provisioning REUSE it idempotently. Tolerates non-JSON / errors → null.
|
|
42
|
+
export function findTunnelId(listJsonOut, name) {
|
|
43
|
+
let arr;
|
|
44
|
+
try {
|
|
45
|
+
arr = JSON.parse(String(listJsonOut || ''));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(arr))
|
|
51
|
+
return null;
|
|
52
|
+
const hit = arr.find((candidate) => isRecord(candidate) && candidate.name === name);
|
|
53
|
+
return isRecord(hit) && typeof hit.id === 'string' && hit.id ? hit.id : null;
|
|
54
|
+
}
|
|
55
|
+
// The config keys the wizard owns: everything it can set. mergeConfig wipes these from the existing config
|
|
56
|
+
// before re-applying the answers, so switching a tunnel (or clearing an optional field) cleanly drops the
|
|
57
|
+
// old value instead of leaving a stale field behind. Anything NOT here (staticDir, uploadExts…) is
|
|
58
|
+
// preserved untouched. `token` IS owned so the Token row can pin one AND clear it back to auto — but it
|
|
59
|
+
// round-trips through answersFromConfig, so a re-run that never touches the row still writes it back.
|
|
60
|
+
const WIZARD_KEYS = [
|
|
61
|
+
'lang', 'name', 'port', 'tunnel', 'token', 'previewDomain',
|
|
62
|
+
'sshHost', 'remotePort', 'sshJump', 'cfHostname', 'cfTunnelName', 'publicUrl',
|
|
63
|
+
'authtoken', 'cpolarRegion',
|
|
64
|
+
'vapid', 'xfyun',
|
|
65
|
+
];
|
|
66
|
+
// The tunnel-specific keys, cleared whenever the tunnel changes so a switch never carries a stale field.
|
|
67
|
+
export const TUNNEL_KEYS = ['sshHost', 'remotePort', 'sshJump', 'cfHostname', 'cfTunnelName', 'publicUrl', 'authtoken', 'cpolarRegion'];
|
|
68
|
+
// Wizard answers → the config fragment the user actually set (omit empty optional fields).
|
|
69
|
+
export function configFromAnswers(a) {
|
|
70
|
+
const cfg = { tunnel: a.tunnel, port: a.port };
|
|
71
|
+
if (a.lang)
|
|
72
|
+
cfg.lang = a.lang;
|
|
73
|
+
if (a.name)
|
|
74
|
+
cfg.name = a.name;
|
|
75
|
+
if (a.token)
|
|
76
|
+
cfg.token = a.token; // blank = don't pin one → the server mints a fresh token each start
|
|
77
|
+
if (a.previewDomain)
|
|
78
|
+
cfg.previewDomain = a.previewDomain;
|
|
79
|
+
if (a.tunnel === 'ssh') {
|
|
80
|
+
cfg.sshHost = a.sshHost;
|
|
81
|
+
cfg.remotePort = a.remotePort;
|
|
82
|
+
if (a.publicUrl)
|
|
83
|
+
cfg.publicUrl = a.publicUrl;
|
|
84
|
+
if (a.sshJump)
|
|
85
|
+
cfg.sshJump = a.sshJump;
|
|
86
|
+
}
|
|
87
|
+
if (a.tunnel === 'cloudflare-named') {
|
|
88
|
+
cfg.cfHostname = a.cfHostname;
|
|
89
|
+
cfg.cfTunnelName = a.cfTunnelName;
|
|
90
|
+
}
|
|
91
|
+
if (a.tunnel === 'natapp' || a.tunnel === 'cpolar') {
|
|
92
|
+
if (a.authtoken)
|
|
93
|
+
cfg.authtoken = a.authtoken;
|
|
94
|
+
if (a.publicUrl)
|
|
95
|
+
cfg.publicUrl = a.publicUrl; // fixed/reserved domain (blank = temporary random)
|
|
96
|
+
if (a.cpolarRegion)
|
|
97
|
+
cfg.cpolarRegion = a.cpolarRegion;
|
|
98
|
+
}
|
|
99
|
+
if (a.vapid)
|
|
100
|
+
cfg.vapid = a.vapid;
|
|
101
|
+
if (a.xfyun)
|
|
102
|
+
cfg.xfyun = a.xfyun;
|
|
103
|
+
return cfg;
|
|
104
|
+
}
|
|
105
|
+
// Fold this run's answers into an existing config: preserve every non-wizard field, replace the wizard's
|
|
106
|
+
// own fields wholesale. This is why re-running `setup` to switch tunnels (or edit the name) never drops
|
|
107
|
+
// your token / push keys / static dir, yet also never leaves the previous tunnel's stale keys around.
|
|
108
|
+
export function mergeConfig(existing = {}, answers) {
|
|
109
|
+
const out = isRecord(existing) ? { ...existing } : {};
|
|
110
|
+
for (const k of WIZARD_KEYS)
|
|
111
|
+
delete out[k];
|
|
112
|
+
return { ...out, ...configFromAnswers(answers) };
|
|
113
|
+
}
|
|
114
|
+
// Seed the working answers from an existing config so the hub shows current values and each edit starts
|
|
115
|
+
// from what's already there. A brand-new config yields safe defaults (none/LAN, port 19999).
|
|
116
|
+
export function answersFromConfig(config = {}) {
|
|
117
|
+
const cfg = isRecord(config) ? config : {};
|
|
118
|
+
const a = {
|
|
119
|
+
lang: optionalString(cfg.lang) || getLocale(),
|
|
120
|
+
name: optionalString(cfg.name) || '',
|
|
121
|
+
token: optionalString(cfg.token) || '', // '' = not pinned (auto each start); seeded so an untouched re-run rewrites it
|
|
122
|
+
previewDomain: optionalString(cfg.previewDomain) || '',
|
|
123
|
+
tunnel: isTunnel(cfg.tunnel) ? cfg.tunnel : 'none',
|
|
124
|
+
port: Number(cfg.port) || 19999,
|
|
125
|
+
};
|
|
126
|
+
const stringKeys = ['sshHost', 'sshJump', 'cfHostname', 'cfTunnelName', 'publicUrl', 'authtoken', 'cpolarRegion'];
|
|
127
|
+
for (const key of stringKeys) {
|
|
128
|
+
const value = optionalString(cfg[key]);
|
|
129
|
+
if (value)
|
|
130
|
+
a[key] = value;
|
|
131
|
+
}
|
|
132
|
+
if (typeof cfg.remotePort === 'number' && Number.isFinite(cfg.remotePort))
|
|
133
|
+
a.remotePort = cfg.remotePort;
|
|
134
|
+
const vapid = stringObject(cfg.vapid, ['public', 'private', 'subject']);
|
|
135
|
+
const xfyun = stringObject(cfg.xfyun, ['appId', 'apiKey', 'apiSecret']);
|
|
136
|
+
if (vapid)
|
|
137
|
+
a.vapid = vapid;
|
|
138
|
+
if (xfyun)
|
|
139
|
+
a.xfyun = xfyun;
|
|
140
|
+
return a;
|
|
141
|
+
}
|
|
142
|
+
// The dim one-line summary shown next to the Connection row (and reused nowhere else). Pure.
|
|
143
|
+
export function summarizeConnection(a) {
|
|
144
|
+
const bare = (url) => String(url || '').replace(/^https?:\/\//, '');
|
|
145
|
+
switch (a.tunnel) {
|
|
146
|
+
case 'cloudflare': return `cloudflare · ${t('setup.sumTemp')}`;
|
|
147
|
+
case 'cloudflare-named': return `cloudflare-named · ${a.cfHostname || '?'}`;
|
|
148
|
+
case 'ssh': return `ssh · ${a.sshHost || '?'}`;
|
|
149
|
+
case 'natapp':
|
|
150
|
+
case 'cpolar': return `${a.tunnel} · ${a.publicUrl ? `${t('setup.sumFixed')} ${bare(a.publicUrl)}` : t('setup.sumTemp')}`;
|
|
151
|
+
default: return `${t('setup.tunnelNone')} · ${t('setup.sumNoRelay')}`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// ---- validators (clack: return a string to reject + show inline, undefined to accept). Pure. ----
|
|
155
|
+
export function validatePort(v) {
|
|
156
|
+
const n = Number(v);
|
|
157
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535)
|
|
158
|
+
return t('setup.valPort');
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
export const validateNonEmpty = (label) => (value) => (String(value || '').trim() ? undefined : t('setup.valRequired', { label }));
|
|
162
|
+
export function validateHost(v) {
|
|
163
|
+
const s = String(v || '').trim().replace(/^https?:\/\//, '').replace(/\/.*$/, '');
|
|
164
|
+
return /^[a-z0-9-]+(\.[a-z0-9-]+)+$/i.test(s) ? undefined : t('setup.valHost');
|
|
165
|
+
}
|
|
166
|
+
// The built-in browser maps proxy origins below this domain. Blank deliberately leaves proxy mode disabled;
|
|
167
|
+
// a URL, wildcard, or port would produce an invalid host when Handmux adds its own stable subdomain.
|
|
168
|
+
export function validatePreviewDomain(v) {
|
|
169
|
+
const s = String(v || '').trim();
|
|
170
|
+
if (!s)
|
|
171
|
+
return undefined;
|
|
172
|
+
return /^[a-z0-9-]+(\.[a-z0-9-]+)+$/i.test(s) ? undefined : t('setup.valPreviewDomain');
|
|
173
|
+
}
|
|
174
|
+
// VAPID subject: Apple (APNs) rejects a fake/.local domain with BadJwtToken, so require a real-looking
|
|
175
|
+
// mailto:you@host.tld or an https:// URL and reject the known-bad .local. Keeps push from silently
|
|
176
|
+
// failing on iOS. (Can't fully validate "real" client-side — this just catches the obvious footguns.)
|
|
177
|
+
export function validateContact(v) {
|
|
178
|
+
const s = String(v || '').trim();
|
|
179
|
+
const wellFormed = /^mailto:[^@\s]+@[a-z0-9-]+(\.[a-z0-9-]+)+$/i.test(s)
|
|
180
|
+
|| /^https:\/\/[a-z0-9-]+(\.[a-z0-9-]+)+/i.test(s);
|
|
181
|
+
if (!wellFormed || /\.local(?:[:/]|$)/i.test(s))
|
|
182
|
+
return t('setup.valContact');
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
// Access token: it rides in the phone's URL as ?token=…, so require something and reject whitespace (a space
|
|
186
|
+
// would break the link). Length/charset are otherwise up to the user — a pinned token is used verbatim.
|
|
187
|
+
export function validateToken(v) {
|
|
188
|
+
const s = String(v || '').trim();
|
|
189
|
+
if (!s)
|
|
190
|
+
return t('setup.valToken');
|
|
191
|
+
if (/\s/.test(s))
|
|
192
|
+
return t('setup.valTokenSpace');
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|