@hwip/cli 0.0.1 → 0.1.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.
Files changed (245) hide show
  1. package/LICENSE +190 -0
  2. package/NOTICE +11 -0
  3. package/README.md +1979 -0
  4. package/dist/agent-list.d.ts +76 -0
  5. package/dist/agent-list.js +102 -0
  6. package/dist/agent-read.d.ts +105 -0
  7. package/dist/agent-read.js +309 -0
  8. package/dist/agent-rules-fetch.d.ts +51 -0
  9. package/dist/agent-rules-fetch.js +275 -0
  10. package/dist/agent-rules.default.d.ts +30 -0
  11. package/dist/agent-rules.default.js +292 -0
  12. package/dist/agent-session.d.ts +23 -0
  13. package/dist/agent-session.js +44 -0
  14. package/dist/agent-state.d.ts +84 -0
  15. package/dist/agent-state.js +188 -0
  16. package/dist/agent-target.d.ts +60 -0
  17. package/dist/agent-target.js +81 -0
  18. package/dist/agents.d.ts +31 -0
  19. package/dist/agents.js +75 -0
  20. package/dist/api.d.ts +28 -0
  21. package/dist/api.js +76 -0
  22. package/dist/args.d.ts +74 -0
  23. package/dist/args.js +254 -0
  24. package/dist/ask-context-attach.d.ts +73 -0
  25. package/dist/ask-context-attach.js +151 -0
  26. package/dist/ask-context-screen.d.ts +21 -0
  27. package/dist/ask-context-screen.js +49 -0
  28. package/dist/ask-context-switch.d.ts +31 -0
  29. package/dist/ask-context-switch.js +74 -0
  30. package/dist/ask-context.d.ts +84 -0
  31. package/dist/ask-context.js +172 -0
  32. package/dist/ask-pending.d.ts +47 -0
  33. package/dist/ask-pending.js +221 -0
  34. package/dist/ask-seal.d.ts +120 -0
  35. package/dist/ask-seal.js +179 -0
  36. package/dist/ask.d.ts +107 -0
  37. package/dist/ask.js +551 -0
  38. package/dist/blocked-prompt-seal.d.ts +35 -0
  39. package/dist/blocked-prompt-seal.js +89 -0
  40. package/dist/blocked-prompt.d.ts +57 -0
  41. package/dist/blocked-prompt.js +181 -0
  42. package/dist/channel.d.ts +67 -0
  43. package/dist/channel.js +136 -0
  44. package/dist/check-update.d.ts +28 -0
  45. package/dist/check-update.js +153 -0
  46. package/dist/cli.d.ts +3 -0
  47. package/dist/cli.js +813 -0
  48. package/dist/codex-config.d.ts +24 -0
  49. package/dist/codex-config.js +169 -0
  50. package/dist/codex-transcript.d.ts +59 -0
  51. package/dist/codex-transcript.js +373 -0
  52. package/dist/command-refusal.d.ts +56 -0
  53. package/dist/command-refusal.js +127 -0
  54. package/dist/command-scan.d.ts +46 -0
  55. package/dist/command-scan.js +154 -0
  56. package/dist/command-seal.d.ts +57 -0
  57. package/dist/command-seal.js +88 -0
  58. package/dist/command-seq.d.ts +30 -0
  59. package/dist/command-seq.js +150 -0
  60. package/dist/config.d.ts +156 -0
  61. package/dist/config.js +392 -0
  62. package/dist/core-sections.d.ts +18 -0
  63. package/dist/core-sections.js +28 -0
  64. package/dist/crypto.d.ts +278 -0
  65. package/dist/crypto.js +698 -0
  66. package/dist/desktop-notify.d.ts +33 -0
  67. package/dist/desktop-notify.js +49 -0
  68. package/dist/device-directory.d.ts +36 -0
  69. package/dist/device-directory.js +97 -0
  70. package/dist/device-presence.d.ts +17 -0
  71. package/dist/device-presence.js +62 -0
  72. package/dist/downloads.d.ts +29 -0
  73. package/dist/downloads.js +106 -0
  74. package/dist/errors.d.ts +12 -0
  75. package/dist/errors.js +28 -0
  76. package/dist/file-crypto.d.ts +94 -0
  77. package/dist/file-crypto.js +272 -0
  78. package/dist/file-view-budget.d.ts +15 -0
  79. package/dist/file-view-budget.js +34 -0
  80. package/dist/file-view-switch.d.ts +28 -0
  81. package/dist/file-view-switch.js +122 -0
  82. package/dist/file-view.d.ts +108 -0
  83. package/dist/file-view.js +327 -0
  84. package/dist/gate.d.ts +141 -0
  85. package/dist/gate.js +336 -0
  86. package/dist/gemini-transcript.d.ts +32 -0
  87. package/dist/gemini-transcript.js +189 -0
  88. package/dist/http-failure.d.ts +98 -0
  89. package/dist/http-failure.js +349 -0
  90. package/dist/hwip-core.generated.d.ts +7 -0
  91. package/dist/hwip-core.generated.js +14 -0
  92. package/dist/hwip-hook.d.ts +77 -0
  93. package/dist/hwip-hook.js +340 -0
  94. package/dist/index.d.ts +7 -0
  95. package/dist/index.js +14 -0
  96. package/dist/input-sequencer.d.ts +65 -0
  97. package/dist/input-sequencer.js +142 -0
  98. package/dist/insecure-url.d.ts +16 -0
  99. package/dist/insecure-url.js +76 -0
  100. package/dist/installer.d.ts +96 -0
  101. package/dist/installer.js +850 -0
  102. package/dist/inventory-offload.d.ts +49 -0
  103. package/dist/inventory-offload.js +209 -0
  104. package/dist/inventory-worker.d.ts +2 -0
  105. package/dist/inventory-worker.js +27 -0
  106. package/dist/jsonc-doc.d.ts +106 -0
  107. package/dist/jsonc-doc.js +354 -0
  108. package/dist/keep-awake.d.ts +32 -0
  109. package/dist/keep-awake.js +46 -0
  110. package/dist/key-changes.d.ts +48 -0
  111. package/dist/key-changes.js +140 -0
  112. package/dist/lan-auth.d.ts +140 -0
  113. package/dist/lan-auth.js +217 -0
  114. package/dist/lan-endpoint.d.ts +41 -0
  115. package/dist/lan-endpoint.js +115 -0
  116. package/dist/lan-receiver.d.ts +122 -0
  117. package/dist/lan-receiver.js +510 -0
  118. package/dist/lan-sender.d.ts +76 -0
  119. package/dist/lan-sender.js +184 -0
  120. package/dist/lan-transfer.d.ts +36 -0
  121. package/dist/lan-transfer.js +54 -0
  122. package/dist/listener-device-id.d.ts +20 -0
  123. package/dist/listener-device-id.js +133 -0
  124. package/dist/listener-log.d.ts +4 -0
  125. package/dist/listener-log.js +23 -0
  126. package/dist/listener-process.d.ts +200 -0
  127. package/dist/listener-process.js +470 -0
  128. package/dist/listener-service.d.ts +225 -0
  129. package/dist/listener-service.js +682 -0
  130. package/dist/listener-worktree-cleanup.d.ts +27 -0
  131. package/dist/listener-worktree-cleanup.js +62 -0
  132. package/dist/listener.d.ts +1325 -0
  133. package/dist/listener.js +5680 -0
  134. package/dist/login-address.d.ts +25 -0
  135. package/dist/login-address.js +97 -0
  136. package/dist/login-page.d.ts +29 -0
  137. package/dist/login-page.js +93 -0
  138. package/dist/login.d.ts +98 -0
  139. package/dist/login.js +388 -0
  140. package/dist/mcp-command.d.ts +45 -0
  141. package/dist/mcp-command.js +50 -0
  142. package/dist/mcp.d.ts +19 -0
  143. package/dist/mcp.js +29 -0
  144. package/dist/node-support.d.ts +19 -0
  145. package/dist/node-support.js +38 -0
  146. package/dist/notify-failure.d.ts +57 -0
  147. package/dist/notify-failure.js +150 -0
  148. package/dist/opencode-transcript.d.ts +139 -0
  149. package/dist/opencode-transcript.js +390 -0
  150. package/dist/optional-features.d.ts +6 -0
  151. package/dist/optional-features.js +19 -0
  152. package/dist/pane-fit.d.ts +40 -0
  153. package/dist/pane-fit.js +119 -0
  154. package/dist/peer-key.d.ts +33 -0
  155. package/dist/peer-key.js +34 -0
  156. package/dist/pi-transcript.d.ts +26 -0
  157. package/dist/pi-transcript.js +255 -0
  158. package/dist/presence.d.ts +14 -0
  159. package/dist/presence.js +124 -0
  160. package/dist/private-files.d.ts +51 -0
  161. package/dist/private-files.js +218 -0
  162. package/dist/project-switch.d.ts +19 -0
  163. package/dist/project-switch.js +60 -0
  164. package/dist/redact.d.ts +10 -0
  165. package/dist/redact.js +21 -0
  166. package/dist/remote-agents.d.ts +410 -0
  167. package/dist/remote-agents.js +1218 -0
  168. package/dist/remote-hook.d.ts +12 -0
  169. package/dist/remote-hook.js +196 -0
  170. package/dist/replace-file.d.ts +50 -0
  171. package/dist/replace-file.js +328 -0
  172. package/dist/schedule-command.d.ts +35 -0
  173. package/dist/schedule-command.js +260 -0
  174. package/dist/schedule-launch.d.ts +15 -0
  175. package/dist/schedule-launch.js +24 -0
  176. package/dist/schedule-ready.d.ts +44 -0
  177. package/dist/schedule-ready.js +71 -0
  178. package/dist/schedule-remote.d.ts +77 -0
  179. package/dist/schedule-remote.js +167 -0
  180. package/dist/schedule-runner.d.ts +89 -0
  181. package/dist/schedule-runner.js +452 -0
  182. package/dist/schedule.d.ts +149 -0
  183. package/dist/schedule.js +428 -0
  184. package/dist/secret-mask.d.ts +29 -0
  185. package/dist/secret-mask.js +245 -0
  186. package/dist/self-register.d.ts +45 -0
  187. package/dist/self-register.js +67 -0
  188. package/dist/send.d.ts +56 -0
  189. package/dist/send.js +104 -0
  190. package/dist/session-diff.d.ts +161 -0
  191. package/dist/session-diff.js +445 -0
  192. package/dist/session-registry.d.ts +143 -0
  193. package/dist/session-registry.js +216 -0
  194. package/dist/sessions-report.d.ts +59 -0
  195. package/dist/sessions-report.js +99 -0
  196. package/dist/shell-audit.d.ts +34 -0
  197. package/dist/shell-audit.js +47 -0
  198. package/dist/subagent-transcripts.d.ts +167 -0
  199. package/dist/subagent-transcripts.js +437 -0
  200. package/dist/subscriber-trust.d.ts +128 -0
  201. package/dist/subscriber-trust.js +397 -0
  202. package/dist/templates.d.ts +105 -0
  203. package/dist/templates.js +871 -0
  204. package/dist/tmux-install.d.ts +44 -0
  205. package/dist/tmux-install.js +65 -0
  206. package/dist/tmux-liveness.d.ts +45 -0
  207. package/dist/tmux-liveness.js +71 -0
  208. package/dist/tmux-pane.d.ts +55 -0
  209. package/dist/tmux-pane.js +126 -0
  210. package/dist/tmux-socket.d.ts +20 -0
  211. package/dist/tmux-socket.js +292 -0
  212. package/dist/todo-fold.d.ts +31 -0
  213. package/dist/todo-fold.js +102 -0
  214. package/dist/transcript-tail.d.ts +312 -0
  215. package/dist/transcript-tail.js +718 -0
  216. package/dist/trust-command.d.ts +49 -0
  217. package/dist/trust-command.js +200 -0
  218. package/dist/trust-lock.d.ts +26 -0
  219. package/dist/trust-lock.js +94 -0
  220. package/dist/trust-pins.d.ts +37 -0
  221. package/dist/trust-pins.js +82 -0
  222. package/dist/turn-recorder-wire.d.ts +70 -0
  223. package/dist/turn-recorder-wire.js +165 -0
  224. package/dist/turn-recorder.d.ts +190 -0
  225. package/dist/turn-recorder.js +695 -0
  226. package/dist/turn-ring.d.ts +93 -0
  227. package/dist/turn-ring.js +418 -0
  228. package/dist/turn-watch.d.ts +177 -0
  229. package/dist/turn-watch.js +630 -0
  230. package/dist/types.d.ts +63 -0
  231. package/dist/types.js +2 -0
  232. package/dist/uninstall.d.ts +55 -0
  233. package/dist/uninstall.js +507 -0
  234. package/dist/usage.d.ts +17 -0
  235. package/dist/usage.js +421 -0
  236. package/dist/verify.d.ts +47 -0
  237. package/dist/verify.js +396 -0
  238. package/dist/worktree.d.ts +144 -0
  239. package/dist/worktree.js +310 -0
  240. package/dist/wrapper.d.ts +254 -0
  241. package/dist/wrapper.js +704 -0
  242. package/dist/ws-url.d.ts +109 -0
  243. package/dist/ws-url.js +202 -0
  244. package/package.json +78 -10
  245. package/bin/hwip.js +0 -3
@@ -0,0 +1,470 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.awaitServiceListener = exports.LISTENER_SERVICE_START_MS = exports.startListenerDetached = exports.listenerLogSize = exports.startFailure = exports.LISTENER_START_GRACE_MS = exports.rotateListenerLogIfLarge = exports.resolveCliPath = exports.LISTENER_LAUNCH_ENV = exports.stopListener = exports.otherListenerPid = exports.isListenerPid = exports.otherListenerNote = exports.checkListenerPid = exports.isHwipListenerCommand = exports.commandOfPid = exports.clearStaleListenerRuntime = exports.clearListenerRuntime = exports.runningListenerWsUrl = exports.writeListenerRuntime = exports.runningListenerVersion = exports.runningListenerPid = exports.sleep = exports.LISTENER_LOG_FILE = exports.LISTENER_WS_URL_FILE = exports.LISTENER_VERSION_FILE = exports.LISTENER_PID_FILE = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_os_1 = require("node:os");
7
+ const node_path_1 = require("node:path");
8
+ const private_files_js_1 = require("./private-files.js");
9
+ /**
10
+ * Process-level facts about the resident `hwip listener` daemon: where its
11
+ * PID/version stamps live, whether one is alive, how to stop it and how to
12
+ * respawn it detached.
13
+ *
14
+ * This lives OUTSIDE listener.ts on purpose. `wrapper.ts` (the `hwip cc` hot
15
+ * path) needs all of it, and importing listener.ts there would drag `ws` +
16
+ * the crypto stack into every agent launch. Same split as
17
+ * listener-device-id.ts — one small module both sides can read.
18
+ *
19
+ * Why a version stamp at all: `npm i -g @hwip/cli` replaces the package on
20
+ * disk but NOT the daemon already running from the old dist. That daemon
21
+ * keeps answering pushes (so chat looks fine) while silently ignoring every
22
+ * subtype added since it started — which is exactly how a pre-1.24 listener
23
+ * left the phone's live terminal spinning until the machine was rebooted.
24
+ * Stamping the version next to the PID makes that drift detectable.
25
+ */
26
+ const HWIP_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), '.hwip');
27
+ exports.LISTENER_PID_FILE = (0, node_path_1.join)(HWIP_DIR, 'listener.pid');
28
+ /** CLI version the running daemon booted from. Absent ⇒ pre-stamp build. */
29
+ exports.LISTENER_VERSION_FILE = (0, node_path_1.join)(HWIP_DIR, 'listener.version');
30
+ /**
31
+ * The socket the running daemon actually connected to. A stamp rather than a
32
+ * line in listener.log, because the log rotates at 5MB and a long-lived daemon
33
+ * — exactly the one whose config edit has been outlived — loses its own startup
34
+ * line to that rotation, which would silently switch the staleness check off.
35
+ */
36
+ exports.LISTENER_WS_URL_FILE = (0, node_path_1.join)(HWIP_DIR, 'listener.wsurl');
37
+ exports.LISTENER_LOG_FILE = (0, node_path_1.join)(HWIP_DIR, 'listener.log');
38
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
39
+ exports.sleep = sleep;
40
+ /**
41
+ * PID of the listener on record, or null when the file is missing or stale.
42
+ * Stale PID files (process gone) read as "no listener" so a crashed daemon
43
+ * never blocks the next autostart.
44
+ */
45
+ const runningListenerPid = () => {
46
+ try {
47
+ const pid = Number((0, node_fs_1.readFileSync)(exports.LISTENER_PID_FILE, 'utf-8').trim());
48
+ if (!Number.isFinite(pid) || pid <= 0)
49
+ return null;
50
+ process.kill(pid, 0); // signal 0 = existence check, throws when gone
51
+ return pid;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ };
57
+ exports.runningListenerPid = runningListenerPid;
58
+ /**
59
+ * CLI version the running daemon started from, or null when unknown.
60
+ * null is meaningful: every build before the stamp existed predates the
61
+ * live-terminal protocol, so callers treat "unknown" as "stale".
62
+ */
63
+ const runningListenerVersion = () => {
64
+ try {
65
+ return (0, node_fs_1.readFileSync)(exports.LISTENER_VERSION_FILE, 'utf-8').trim() || null;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ };
71
+ exports.runningListenerVersion = runningListenerVersion;
72
+ /** Claim the singleton slot for THIS process and stamp the version with it. */
73
+ const writeListenerRuntime = (version, wsUrl) => {
74
+ (0, private_files_js_1.ensurePrivateDir)(HWIP_DIR);
75
+ (0, node_fs_1.writeFileSync)(exports.LISTENER_PID_FILE, String(process.pid));
76
+ (0, node_fs_1.writeFileSync)(exports.LISTENER_VERSION_FILE, version);
77
+ if (wsUrl)
78
+ (0, node_fs_1.writeFileSync)(exports.LISTENER_WS_URL_FILE, wsUrl);
79
+ for (const file of [exports.LISTENER_PID_FILE, exports.LISTENER_VERSION_FILE, ...(wsUrl ? [exports.LISTENER_WS_URL_FILE] : [])])
80
+ (0, private_files_js_1.handBackToSudoUser)(file);
81
+ };
82
+ exports.writeListenerRuntime = writeListenerRuntime;
83
+ /** The socket the running daemon connected to, or null when it is unknown. */
84
+ const runningListenerWsUrl = () => {
85
+ if ((0, exports.runningListenerPid)() === null)
86
+ return null;
87
+ try {
88
+ return (0, node_fs_1.readFileSync)(exports.LISTENER_WS_URL_FILE, 'utf-8').trim() || null;
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ };
94
+ exports.runningListenerWsUrl = runningListenerWsUrl;
95
+ /**
96
+ * Drop the PID/version stamps, but only when they still name `pid` — a
97
+ * successor that already claimed the slot must not be trampled by a
98
+ * predecessor's exit handler.
99
+ */
100
+ const clearListenerRuntime = (pid) => {
101
+ try {
102
+ if (!(0, node_fs_1.existsSync)(exports.LISTENER_PID_FILE))
103
+ return;
104
+ if (Number((0, node_fs_1.readFileSync)(exports.LISTENER_PID_FILE, 'utf-8').trim()) !== pid)
105
+ return;
106
+ (0, node_fs_1.unlinkSync)(exports.LISTENER_PID_FILE);
107
+ for (const stamp of [exports.LISTENER_VERSION_FILE, exports.LISTENER_WS_URL_FILE]) {
108
+ try {
109
+ (0, node_fs_1.unlinkSync)(stamp);
110
+ }
111
+ catch { /* best-effort — a missing stamp is already the desired state */ }
112
+ }
113
+ }
114
+ catch { /* best-effort */ }
115
+ };
116
+ exports.clearListenerRuntime = clearListenerRuntime;
117
+ /**
118
+ * Drop stamps left behind by a daemon that died without running its exit
119
+ * handler (crash, SIGKILL, reboot-era leftovers). No-op while one is alive.
120
+ */
121
+ const clearStaleListenerRuntime = () => {
122
+ if ((0, exports.runningListenerPid)() !== null)
123
+ return;
124
+ try {
125
+ (0, node_fs_1.unlinkSync)(exports.LISTENER_PID_FILE);
126
+ }
127
+ catch { /* nothing to clear */ }
128
+ try {
129
+ (0, node_fs_1.unlinkSync)(exports.LISTENER_VERSION_FILE);
130
+ }
131
+ catch { /* nothing to clear */ }
132
+ };
133
+ exports.clearStaleListenerRuntime = clearStaleListenerRuntime;
134
+ const commandOfPid = (pid) => {
135
+ const result = (0, node_child_process_1.spawnSync)('ps', ['-o', 'command=', '-p', String(pid)], { encoding: 'utf-8', timeout: 2_000 });
136
+ if (result.error)
137
+ return { kind: 'unchecked', reason: result.error.message };
138
+ const command = result.stdout.trim();
139
+ if (result.status === 0 && command)
140
+ return { kind: 'running', command };
141
+ // A missing pid is a bare non-zero exit; anything on stderr is ps itself
142
+ // failing ("process id too large"), which says nothing about the pid.
143
+ const complaint = result.stderr.trim();
144
+ return complaint ? { kind: 'unchecked', reason: complaint } : { kind: 'gone' };
145
+ };
146
+ exports.commandOfPid = commandOfPid;
147
+ /**
148
+ * True when a command line is hwip's listener daemon: the word before
149
+ * `listener` is hwip's own script — the bin shim (`…/hwip`), the published
150
+ * package's entry (`…/@hwip/cli/dist/cli.js`: global install, npx), or this
151
+ * CLI's own file (a dev checkout). Any other `…/cli.js listener` (Zeph's runs
152
+ * on the same machines), or a file that only sits under a `hwip*` directory,
153
+ * does not pass. Exported for tests, which pass `ownCliPath`.
154
+ */
155
+ const isHwipListenerCommand = (command, ownCliPath = (0, exports.resolveCliPath)()) => {
156
+ const words = command.trim().split(/\s+/);
157
+ const at = words.indexOf('listener');
158
+ if (at < 1)
159
+ return false;
160
+ const script = words[at - 1];
161
+ // A shebang script shows up by its full path, so a bare `hwip` word is
162
+ // some other command (`grep hwip listener`), not the shim.
163
+ return script.endsWith('/hwip') || script.endsWith('/@hwip/cli/dist/cli.js') || script === ownCliPath;
164
+ };
165
+ exports.isHwipListenerCommand = isHwipListenerCommand;
166
+ /**
167
+ * The pid file can outlive a crashed daemon and its pid be reused by any
168
+ * process, so a pid is only ours while its command line says so.
169
+ */
170
+ const checkListenerPid = (pid, commandOf = exports.commandOfPid, isHwipScript = isHwipCliScript) => {
171
+ const found = commandOf(pid);
172
+ if (found.kind !== 'running')
173
+ return found;
174
+ if ((0, exports.isHwipListenerCommand)(found.command))
175
+ return { kind: 'ours' };
176
+ const script = listenerScriptOf(found.command);
177
+ if (script !== null && isHwipScript(script))
178
+ return { kind: 'other-listener', script };
179
+ return { kind: 'foreign', program: (0, node_path_1.basename)(found.command.split(/\s+/)[0]) };
180
+ };
181
+ exports.checkListenerPid = checkListenerPid;
182
+ /**
183
+ * Whether a script belongs to a checkout of this CLI: the nearest
184
+ * package.json above it names `@hwip/cli`. That is what tells another hwip
185
+ * worktree's `dist/cli.js` from Zeph's, which has the same shape.
186
+ */
187
+ const isHwipCliScript = (script) => {
188
+ for (let dir = (0, node_path_1.dirname)(script), up = 0; up < 3; dir = (0, node_path_1.dirname)(dir), up++) {
189
+ try {
190
+ return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, 'package.json'), 'utf-8')).name === '@hwip/cli';
191
+ }
192
+ catch { /* no package.json here: look one level up */ }
193
+ }
194
+ return false;
195
+ };
196
+ /** What every caller says about an `other-listener`. */
197
+ const otherListenerNote = (pid, script) => `pid ${pid} is a hwip listener from another checkout (${script}) — left it running; stop it from there`;
198
+ exports.otherListenerNote = otherListenerNote;
199
+ /**
200
+ * Any `<script>.js listener` command line: hwip's own, or one from another
201
+ * checkout or worktree of it (`…/cli-wt-x/dist/cli.js listener`), which
202
+ * `isHwipListenerCommand` cannot vouch for. Zeph's listener has the same
203
+ * shape — deferring to it costs one start, a second hwip daemon costs every
204
+ * push answered twice.
205
+ */
206
+ const listenerScriptOf = (command) => {
207
+ const words = command.trim().split(/\s+/);
208
+ const at = words.indexOf('listener');
209
+ return at >= 1 && /\.[cm]?[jt]s$/.test(words[at - 1]) ? words[at - 1] : null;
210
+ };
211
+ /**
212
+ * Whether the pid on record should be treated as a live listener: hwip's own
213
+ * or listener-shaped command line, or one `ps` could not check — two daemons
214
+ * answering the same pushes is the worse failure. `hwip verify` and the
215
+ * startup guard ask the same question.
216
+ */
217
+ const isListenerPid = (pid, commandOf = exports.commandOfPid) => {
218
+ const found = commandOf(pid);
219
+ if (found.kind !== 'running')
220
+ return found.kind === 'unchecked';
221
+ return (0, exports.isHwipListenerCommand)(found.command) || listenerScriptOf(found.command) !== null;
222
+ };
223
+ exports.isListenerPid = isListenerPid;
224
+ /**
225
+ * The pid of another hwip listener holding the singleton slot, or null. A pid
226
+ * the OS handed to an unrelated process is not a listener: deferring to it
227
+ * would end launchd's or `hwip cc`'s daemon with exit 0 and leave no listener
228
+ * at all, so its stamps are cleared and the slot is free.
229
+ */
230
+ const otherListenerPid = (selfPid = process.pid, commandOf = exports.commandOfPid) => {
231
+ const pid = (0, exports.runningListenerPid)();
232
+ if (pid === null || pid === selfPid)
233
+ return null;
234
+ if ((0, exports.isListenerPid)(pid, commandOf))
235
+ return pid;
236
+ (0, exports.clearListenerRuntime)(pid);
237
+ return null;
238
+ };
239
+ exports.otherListenerPid = otherListenerPid;
240
+ /**
241
+ * Stop the daemon and wait for the process to actually go away — but only a
242
+ * process that is still hwip's listener (`checkListenerPid`); anything else is
243
+ * left alone and reported, and a pid file pointing at a stranger is cleared.
244
+ * SIGTERM first so its exit handler clears the stamps, SIGKILL once the grace
245
+ * window expires.
246
+ *
247
+ * Waiting matters: the caller's next move is spawning a replacement, and two
248
+ * live daemons on one machine both answer the same pushes.
249
+ */
250
+ const stopListener = async (pid, commandOf = exports.commandOfPid, timeoutMs = 3_000) => {
251
+ const check = (0, exports.checkListenerPid)(pid, commandOf);
252
+ // The stamps name a daemon that is gone; left in place they would keep
253
+ // the next daemon's singleton guard deferring to a stranger.
254
+ if (check.kind === 'foreign')
255
+ (0, exports.clearListenerRuntime)(pid);
256
+ if (check.kind !== 'ours')
257
+ return check;
258
+ try {
259
+ process.kill(pid, 'SIGTERM');
260
+ }
261
+ catch {
262
+ return { kind: 'gone' };
263
+ }
264
+ const deadline = Date.now() + timeoutMs;
265
+ while (Date.now() < deadline) {
266
+ await (0, exports.sleep)(50);
267
+ try {
268
+ process.kill(pid, 0);
269
+ }
270
+ catch {
271
+ (0, exports.clearListenerRuntime)(pid);
272
+ return { kind: 'stopped' };
273
+ }
274
+ }
275
+ try {
276
+ process.kill(pid, 'SIGKILL');
277
+ }
278
+ catch { /* raced us to exit */ }
279
+ (0, exports.clearListenerRuntime)(pid);
280
+ return { kind: 'killed' };
281
+ };
282
+ exports.stopListener = stopListener;
283
+ /**
284
+ * Set by the listener on the `hwip <agent> --detach` it runs for a schedule. That child is the listener's own: when its
285
+ * build is newer than the running listener's (after an upgrade) `ensureListenerRunning` would restart the listener, and
286
+ * the restart would end the launch's own parent. With this set it leaves the listener alone, and removes the variable from
287
+ * its environment so that nothing the launch starts after it (the tmux server, the agent) inherits it.
288
+ */
289
+ exports.LISTENER_LAUNCH_ENV = 'HWIP_LISTENER_LAUNCH';
290
+ /**
291
+ * Path to the running cli.js entry. wrapper.js/listener.js sit next to cli.js
292
+ * in dist/, so __dirname resolves it directly — independent of how the user
293
+ * invoked us.
294
+ *
295
+ * `process.argv[1]` is unreliable here: when `hwip` runs via the npm-installed
296
+ * bin shim (`/usr/local/bin/hwip` → cli.js via a wrapper script), argv[1] is
297
+ * the shim path (`.../bin/hwip`), NOT `cli.js`. That made the original
298
+ * `/cli\.(js|ts|mjs|cjs)$/` check silently reject the entry and the autospawn
299
+ * never fired — the bug only surfaced once the user switched to the global
300
+ * npm install.
301
+ *
302
+ * Fall back to argv[1] only when the __dirname-relative file doesn't exist
303
+ * (some packaging where dist layout differs).
304
+ */
305
+ const resolveCliPath = () => {
306
+ const local = (0, node_path_1.join)(__dirname, 'cli.js');
307
+ if ((0, node_fs_1.existsSync)(local))
308
+ return local;
309
+ const entry = process.argv[1];
310
+ if (entry && /cli\.(js|ts|mjs|cjs)$/.test(entry))
311
+ return entry;
312
+ return null;
313
+ };
314
+ exports.resolveCliPath = resolveCliPath;
315
+ /**
316
+ * Rotate the listener log once it grows past 5 MB. The daemon runs for days
317
+ * and writes 2-3 lines per 5-s cycle, so without rotation the file climbs
318
+ * into the tens of megabytes. Keep the previous window under `.old`.
319
+ *
320
+ * Copy-truncate rather than rename, because the writer is not always ours.
321
+ * Under the login-time LaunchAgent, launchd opens `StandardOutPath` itself
322
+ * and holds that fd for the life of the job: a rename moves the directory
323
+ * entry and leaves launchd appending to the very same inode, now called
324
+ * `.old`. The log the user tails would never shrink. Emptying the file in
325
+ * place keeps the inode — and the fd — valid.
326
+ */
327
+ const LISTENER_LOG_MAX_BYTES = 5 * 1024 * 1024;
328
+ const rotateListenerLogIfLarge = () => {
329
+ try {
330
+ if (!(0, node_fs_1.existsSync)(exports.LISTENER_LOG_FILE))
331
+ return;
332
+ if ((0, node_fs_1.statSync)(exports.LISTENER_LOG_FILE).size <= LISTENER_LOG_MAX_BYTES)
333
+ return;
334
+ (0, node_fs_1.copyFileSync)(exports.LISTENER_LOG_FILE, exports.LISTENER_LOG_FILE + '.old');
335
+ (0, node_fs_1.truncateSync)(exports.LISTENER_LOG_FILE, 0);
336
+ }
337
+ catch { /* best-effort */ }
338
+ };
339
+ exports.rotateListenerLogIfLarge = rotateListenerLogIfLarge;
340
+ /**
341
+ * Launch `hwip listener` detached, with output appended to the listener log
342
+ * so nothing is lost once we let go of it. Null when the cli entry couldn't
343
+ * be resolved or the spawn failed.
344
+ */
345
+ const spawnDetachedChild = () => {
346
+ const cliPath = (0, exports.resolveCliPath)();
347
+ if (!cliPath)
348
+ return null;
349
+ try {
350
+ // The log can quote what the listener was handed; only its owner reads it.
351
+ (0, private_files_js_1.ensurePrivateDir)(HWIP_DIR);
352
+ (0, private_files_js_1.ensurePrivateFile)(exports.LISTENER_LOG_FILE);
353
+ const out = (0, node_fs_1.openSync)(exports.LISTENER_LOG_FILE, 'a');
354
+ const child = (0, node_child_process_1.spawn)(process.execPath, [cliPath, 'listener'], {
355
+ detached: true,
356
+ stdio: ['ignore', out, out],
357
+ env: { ...process.env, HWIP_LISTENER_AUTOSTART: '1' },
358
+ });
359
+ // An async spawn failure (EACCES, EAGAIN) arrives as 'error' with no
360
+ // 'exit'; unhandled, it would crash `hwip cc` mid-wait.
361
+ child.once('error', () => { });
362
+ child.unref();
363
+ return child;
364
+ }
365
+ catch {
366
+ return null;
367
+ }
368
+ };
369
+ /**
370
+ * How long a daemon `hwip cc` just spawned gets to fail. The fast deaths — no
371
+ * key, no tmux, no socket URL — come before the daemon writes its pid file;
372
+ * a rejected key closes the first socket within about a second.
373
+ */
374
+ exports.LISTENER_START_GRACE_MS = 1_500;
375
+ /** Why a start failed, in one line for the user; null when a listener is up. */
376
+ const startFailure = (started) => {
377
+ switch (started.kind) {
378
+ case 'running':
379
+ case 'deferred':
380
+ return null;
381
+ case 'not-spawned':
382
+ return 'it could not be launched';
383
+ case 'died':
384
+ return started.line ?? (started.code === null ? 'it was killed by a signal' : `it exited with code ${started.code}`);
385
+ case 'not-up':
386
+ return started.line ?? 'it is not running and logged nothing';
387
+ }
388
+ };
389
+ exports.startFailure = startFailure;
390
+ /** Where the log ends now: what a start writes after this is that start's. */
391
+ const listenerLogSize = () => {
392
+ try {
393
+ return (0, node_fs_1.statSync)(exports.LISTENER_LOG_FILE).size;
394
+ }
395
+ catch {
396
+ return 0;
397
+ }
398
+ };
399
+ exports.listenerLogSize = listenerLogSize;
400
+ /** The last non-blank line written to the log past `offset` (from the start if it shrank). */
401
+ const lastLogLineSince = (offset) => {
402
+ try {
403
+ const log = (0, node_fs_1.readFileSync)(exports.LISTENER_LOG_FILE);
404
+ const lines = log.subarray(log.length < offset ? 0 : offset).toString('utf-8').split('\n');
405
+ return lines.map((line) => line.trim()).filter(Boolean).pop() ?? null;
406
+ }
407
+ catch {
408
+ return null;
409
+ }
410
+ };
411
+ /**
412
+ * Spawn the daemon and watch it through the grace window. A daemon that dies
413
+ * on a missing key, a missing tmux or a rejected key only says so in the log,
414
+ * so `hwip cc` used to print "listener autostarted" over a dead one and the
415
+ * phone silently lost control.
416
+ */
417
+ const startListenerDetached = async (spawnChild = spawnDetachedChild, graceMs = exports.LISTENER_START_GRACE_MS) => {
418
+ const offset = (0, exports.listenerLogSize)();
419
+ const child = spawnChild();
420
+ if (!child)
421
+ return { kind: 'not-spawned' };
422
+ const code = await new Promise((settle) => {
423
+ if (child.exitCode !== null)
424
+ return settle(child.exitCode);
425
+ const timer = setTimeout(() => settle(undefined), graceMs);
426
+ child.once('exit', (exitCode) => {
427
+ clearTimeout(timer);
428
+ settle(exitCode);
429
+ });
430
+ // A failed spawn (missing node, EACCES) emits 'error' and never 'exit'.
431
+ child.once('error', () => {
432
+ clearTimeout(timer);
433
+ settle('spawn-error');
434
+ });
435
+ });
436
+ if (code === 'spawn-error')
437
+ return { kind: 'not-spawned' };
438
+ if (code === undefined)
439
+ return { kind: 'running' };
440
+ if (code === 0)
441
+ return { kind: 'deferred' };
442
+ return { kind: 'died', code, line: lastLogLineSince(offset) };
443
+ };
444
+ exports.startListenerDetached = startListenerDetached;
445
+ /** How long launchd gets to bring a kickstarted daemon as far as its pid file. */
446
+ exports.LISTENER_SERVICE_START_MS = 5_000;
447
+ const SERVICE_POLL_MS = 250;
448
+ /**
449
+ * After `launchctl kickstart`: launchd starts the daemon, so there is no child
450
+ * to watch — only the pid file it writes once its key, tmux and socket URL
451
+ * check out, and the log it says why in. A daemon that dies there is restarted
452
+ * by launchd on a loop, and `hwip cc` used to report it "started". Waits for a
453
+ * pid other than `previousPid` (the daemon `kickstart -k` replaced may still
454
+ * be shutting down), then for the grace window a rejected key needs.
455
+ */
456
+ const awaitServiceListener = async (logOffset, previousPid, deps = { sleep: exports.sleep, runningPid: exports.runningListenerPid }, graceMs = exports.LISTENER_START_GRACE_MS, startMs = exports.LISTENER_SERVICE_START_MS) => {
457
+ const up = () => {
458
+ const pid = deps.runningPid();
459
+ return pid !== null && pid !== previousPid;
460
+ };
461
+ const notUp = () => ({ kind: 'not-up', line: lastLogLineSince(logOffset) });
462
+ for (let waited = 0; !up(); waited += SERVICE_POLL_MS) {
463
+ if (waited >= startMs)
464
+ return notUp();
465
+ await deps.sleep(SERVICE_POLL_MS);
466
+ }
467
+ await deps.sleep(graceMs);
468
+ return up() ? { kind: 'running' } : notUp();
469
+ };
470
+ exports.awaitServiceListener = awaitServiceListener;