@hwip/cli 0.0.1 → 0.1.1
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/LICENSE +190 -0
- package/NOTICE +11 -0
- package/README.md +1983 -0
- package/dist/agent-list.d.ts +76 -0
- package/dist/agent-list.js +102 -0
- package/dist/agent-read.d.ts +105 -0
- package/dist/agent-read.js +309 -0
- package/dist/agent-rules-fetch.d.ts +51 -0
- package/dist/agent-rules-fetch.js +275 -0
- package/dist/agent-rules.default.d.ts +30 -0
- package/dist/agent-rules.default.js +305 -0
- package/dist/agent-session.d.ts +23 -0
- package/dist/agent-session.js +44 -0
- package/dist/agent-state.d.ts +84 -0
- package/dist/agent-state.js +188 -0
- package/dist/agent-target.d.ts +60 -0
- package/dist/agent-target.js +81 -0
- package/dist/agents.d.ts +31 -0
- package/dist/agents.js +75 -0
- package/dist/api.d.ts +28 -0
- package/dist/api.js +76 -0
- package/dist/args.d.ts +74 -0
- package/dist/args.js +254 -0
- package/dist/ask-context-attach.d.ts +73 -0
- package/dist/ask-context-attach.js +151 -0
- package/dist/ask-context-screen.d.ts +21 -0
- package/dist/ask-context-screen.js +49 -0
- package/dist/ask-context-switch.d.ts +31 -0
- package/dist/ask-context-switch.js +74 -0
- package/dist/ask-context.d.ts +84 -0
- package/dist/ask-context.js +172 -0
- package/dist/ask-pending.d.ts +47 -0
- package/dist/ask-pending.js +221 -0
- package/dist/ask-seal.d.ts +120 -0
- package/dist/ask-seal.js +179 -0
- package/dist/ask.d.ts +107 -0
- package/dist/ask.js +551 -0
- package/dist/blocked-prompt-seal.d.ts +39 -0
- package/dist/blocked-prompt-seal.js +92 -0
- package/dist/blocked-prompt.d.ts +62 -0
- package/dist/blocked-prompt.js +518 -0
- package/dist/channel.d.ts +67 -0
- package/dist/channel.js +136 -0
- package/dist/check-update.d.ts +28 -0
- package/dist/check-update.js +153 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +813 -0
- package/dist/codex-config.d.ts +24 -0
- package/dist/codex-config.js +169 -0
- package/dist/codex-transcript.d.ts +59 -0
- package/dist/codex-transcript.js +373 -0
- package/dist/command-refusal.d.ts +56 -0
- package/dist/command-refusal.js +127 -0
- package/dist/command-scan.d.ts +46 -0
- package/dist/command-scan.js +154 -0
- package/dist/command-seal.d.ts +57 -0
- package/dist/command-seal.js +88 -0
- package/dist/command-seq.d.ts +30 -0
- package/dist/command-seq.js +150 -0
- package/dist/config.d.ts +156 -0
- package/dist/config.js +392 -0
- package/dist/core-sections.d.ts +18 -0
- package/dist/core-sections.js +28 -0
- package/dist/crypto.d.ts +278 -0
- package/dist/crypto.js +698 -0
- package/dist/desktop-notify.d.ts +33 -0
- package/dist/desktop-notify.js +49 -0
- package/dist/device-directory.d.ts +36 -0
- package/dist/device-directory.js +97 -0
- package/dist/device-presence.d.ts +17 -0
- package/dist/device-presence.js +62 -0
- package/dist/downloads.d.ts +29 -0
- package/dist/downloads.js +106 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +28 -0
- package/dist/file-crypto.d.ts +94 -0
- package/dist/file-crypto.js +278 -0
- package/dist/file-view-budget.d.ts +15 -0
- package/dist/file-view-budget.js +34 -0
- package/dist/file-view-switch.d.ts +28 -0
- package/dist/file-view-switch.js +122 -0
- package/dist/file-view.d.ts +108 -0
- package/dist/file-view.js +327 -0
- package/dist/gate.d.ts +141 -0
- package/dist/gate.js +336 -0
- package/dist/gemini-transcript.d.ts +32 -0
- package/dist/gemini-transcript.js +189 -0
- package/dist/http-failure.d.ts +98 -0
- package/dist/http-failure.js +349 -0
- package/dist/hwip-core.generated.d.ts +7 -0
- package/dist/hwip-core.generated.js +14 -0
- package/dist/hwip-hook.d.ts +77 -0
- package/dist/hwip-hook.js +340 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/input-sequencer.d.ts +65 -0
- package/dist/input-sequencer.js +142 -0
- package/dist/insecure-url.d.ts +16 -0
- package/dist/insecure-url.js +76 -0
- package/dist/installer.d.ts +96 -0
- package/dist/installer.js +850 -0
- package/dist/inventory-offload.d.ts +49 -0
- package/dist/inventory-offload.js +209 -0
- package/dist/inventory-worker.d.ts +2 -0
- package/dist/inventory-worker.js +27 -0
- package/dist/jsonc-doc.d.ts +106 -0
- package/dist/jsonc-doc.js +354 -0
- package/dist/keep-awake.d.ts +32 -0
- package/dist/keep-awake.js +46 -0
- package/dist/key-changes.d.ts +48 -0
- package/dist/key-changes.js +140 -0
- package/dist/lan-auth.d.ts +140 -0
- package/dist/lan-auth.js +217 -0
- package/dist/lan-endpoint.d.ts +41 -0
- package/dist/lan-endpoint.js +115 -0
- package/dist/lan-receiver.d.ts +122 -0
- package/dist/lan-receiver.js +510 -0
- package/dist/lan-sender.d.ts +76 -0
- package/dist/lan-sender.js +184 -0
- package/dist/lan-transfer.d.ts +36 -0
- package/dist/lan-transfer.js +54 -0
- package/dist/listener-device-id.d.ts +20 -0
- package/dist/listener-device-id.js +133 -0
- package/dist/listener-log.d.ts +4 -0
- package/dist/listener-log.js +23 -0
- package/dist/listener-process.d.ts +200 -0
- package/dist/listener-process.js +470 -0
- package/dist/listener-service.d.ts +225 -0
- package/dist/listener-service.js +682 -0
- package/dist/listener-worktree-cleanup.d.ts +27 -0
- package/dist/listener-worktree-cleanup.js +62 -0
- package/dist/listener.d.ts +1325 -0
- package/dist/listener.js +5681 -0
- package/dist/login-address.d.ts +25 -0
- package/dist/login-address.js +97 -0
- package/dist/login-page.d.ts +29 -0
- package/dist/login-page.js +93 -0
- package/dist/login.d.ts +98 -0
- package/dist/login.js +388 -0
- package/dist/mcp-command.d.ts +45 -0
- package/dist/mcp-command.js +50 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +29 -0
- package/dist/node-support.d.ts +19 -0
- package/dist/node-support.js +38 -0
- package/dist/notify-failure.d.ts +57 -0
- package/dist/notify-failure.js +150 -0
- package/dist/opencode-transcript.d.ts +139 -0
- package/dist/opencode-transcript.js +390 -0
- package/dist/optional-features.d.ts +6 -0
- package/dist/optional-features.js +19 -0
- package/dist/pane-fit.d.ts +40 -0
- package/dist/pane-fit.js +119 -0
- package/dist/peer-key.d.ts +33 -0
- package/dist/peer-key.js +34 -0
- package/dist/pi-transcript.d.ts +26 -0
- package/dist/pi-transcript.js +255 -0
- package/dist/presence.d.ts +14 -0
- package/dist/presence.js +124 -0
- package/dist/private-files.d.ts +51 -0
- package/dist/private-files.js +218 -0
- package/dist/project-switch.d.ts +19 -0
- package/dist/project-switch.js +60 -0
- package/dist/redact.d.ts +10 -0
- package/dist/redact.js +21 -0
- package/dist/remote-agents.d.ts +410 -0
- package/dist/remote-agents.js +1218 -0
- package/dist/remote-hook.d.ts +12 -0
- package/dist/remote-hook.js +196 -0
- package/dist/replace-file.d.ts +50 -0
- package/dist/replace-file.js +328 -0
- package/dist/schedule-command.d.ts +35 -0
- package/dist/schedule-command.js +260 -0
- package/dist/schedule-launch.d.ts +15 -0
- package/dist/schedule-launch.js +24 -0
- package/dist/schedule-ready.d.ts +44 -0
- package/dist/schedule-ready.js +71 -0
- package/dist/schedule-remote.d.ts +77 -0
- package/dist/schedule-remote.js +167 -0
- package/dist/schedule-runner.d.ts +89 -0
- package/dist/schedule-runner.js +452 -0
- package/dist/schedule.d.ts +149 -0
- package/dist/schedule.js +428 -0
- package/dist/secret-mask.d.ts +29 -0
- package/dist/secret-mask.js +245 -0
- package/dist/self-register.d.ts +45 -0
- package/dist/self-register.js +67 -0
- package/dist/send.d.ts +56 -0
- package/dist/send.js +104 -0
- package/dist/session-diff.d.ts +161 -0
- package/dist/session-diff.js +445 -0
- package/dist/session-registry.d.ts +143 -0
- package/dist/session-registry.js +216 -0
- package/dist/sessions-report.d.ts +59 -0
- package/dist/sessions-report.js +99 -0
- package/dist/shell-audit.d.ts +34 -0
- package/dist/shell-audit.js +47 -0
- package/dist/subagent-transcripts.d.ts +167 -0
- package/dist/subagent-transcripts.js +437 -0
- package/dist/subscriber-trust.d.ts +128 -0
- package/dist/subscriber-trust.js +397 -0
- package/dist/templates.d.ts +105 -0
- package/dist/templates.js +871 -0
- package/dist/tmux-install.d.ts +44 -0
- package/dist/tmux-install.js +65 -0
- package/dist/tmux-liveness.d.ts +45 -0
- package/dist/tmux-liveness.js +71 -0
- package/dist/tmux-pane.d.ts +55 -0
- package/dist/tmux-pane.js +126 -0
- package/dist/tmux-socket.d.ts +20 -0
- package/dist/tmux-socket.js +292 -0
- package/dist/todo-fold.d.ts +31 -0
- package/dist/todo-fold.js +102 -0
- package/dist/transcript-tail.d.ts +312 -0
- package/dist/transcript-tail.js +718 -0
- package/dist/trust-command.d.ts +49 -0
- package/dist/trust-command.js +200 -0
- package/dist/trust-lock.d.ts +26 -0
- package/dist/trust-lock.js +94 -0
- package/dist/trust-pins.d.ts +37 -0
- package/dist/trust-pins.js +82 -0
- package/dist/turn-recorder-wire.d.ts +70 -0
- package/dist/turn-recorder-wire.js +165 -0
- package/dist/turn-recorder.d.ts +190 -0
- package/dist/turn-recorder.js +695 -0
- package/dist/turn-ring.d.ts +93 -0
- package/dist/turn-ring.js +418 -0
- package/dist/turn-watch.d.ts +177 -0
- package/dist/turn-watch.js +630 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.js +2 -0
- package/dist/uninstall.d.ts +55 -0
- package/dist/uninstall.js +507 -0
- package/dist/usage.d.ts +17 -0
- package/dist/usage.js +421 -0
- package/dist/verify.d.ts +47 -0
- package/dist/verify.js +396 -0
- package/dist/worktree.d.ts +144 -0
- package/dist/worktree.js +310 -0
- package/dist/wrapper.d.ts +254 -0
- package/dist/wrapper.js +704 -0
- package/dist/ws-url.d.ts +109 -0
- package/dist/ws-url.js +202 -0
- package/package.json +78 -10
- package/bin/hwip.js +0 -3
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tryLanDelivery = exports.lanUploadTimeoutMs = exports.LAN_PING_TIMEOUT_MS = exports.pickLanTarget = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const lan_auth_js_1 = require("./lan-auth.js");
|
|
6
|
+
/**
|
|
7
|
+
* The device a push can go to directly, or null for the relay: a push with
|
|
8
|
+
* no single target (a broadcast) never qualifies, nor does a target without
|
|
9
|
+
* a key, an endpoint, or a live connection.
|
|
10
|
+
*/
|
|
11
|
+
const pickLanTarget = (devices, targetDeviceId) => {
|
|
12
|
+
if (!targetDeviceId)
|
|
13
|
+
return null;
|
|
14
|
+
const device = devices.find((d) => d.deviceId === targetDeviceId);
|
|
15
|
+
if (!device?.publicKey || device.isOnline !== true || !device.lan)
|
|
16
|
+
return null;
|
|
17
|
+
const { host, port } = device.lan;
|
|
18
|
+
if (typeof host !== 'string' || host.length === 0 || typeof port !== 'number' || !Number.isInteger(port))
|
|
19
|
+
return null;
|
|
20
|
+
return { deviceId: device.deviceId, publicKey: device.publicKey, host, port };
|
|
21
|
+
};
|
|
22
|
+
exports.pickLanTarget = pickLanTarget;
|
|
23
|
+
/** A receiver that is there answers a ping on the same network in milliseconds. */
|
|
24
|
+
exports.LAN_PING_TIMEOUT_MS = 1_500;
|
|
25
|
+
/** 30 s, plus one second per MiB — slower than any Wi-Fi a LAN transfer is worth trying on. */
|
|
26
|
+
const lanUploadTimeoutMs = (bytes) => 30_000 + Math.ceil(bytes / 1_048_576) * 1_000;
|
|
27
|
+
exports.lanUploadTimeoutMs = lanUploadTimeoutMs;
|
|
28
|
+
const newTransferId = () => `lt_${(0, node_crypto_1.randomBytes)(12).toString('hex')}`;
|
|
29
|
+
/** A ping answer is `{"deviceId": "..."}`; anything longer is not a receiver. */
|
|
30
|
+
const PING_BODY_MAX_BYTES = 1024;
|
|
31
|
+
const UPLOAD_CHUNK_BYTES = 1_048_576;
|
|
32
|
+
/** Fires on the timeout or the caller's signal, whichever comes first
|
|
33
|
+
* (`AbortSignal.any` is Node 20+; this package supports 20.17+). `release`
|
|
34
|
+
* detaches from the caller's signal, which may outlive this request. */
|
|
35
|
+
const deadline = (timeoutMs, external) => {
|
|
36
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
37
|
+
if (!external)
|
|
38
|
+
return { signal: timeout, release: () => undefined };
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
const onExternal = () => { controller.abort(external.reason); };
|
|
41
|
+
const onTimeout = () => { controller.abort(timeout.reason); };
|
|
42
|
+
external.addEventListener('abort', onExternal, { once: true });
|
|
43
|
+
timeout.addEventListener('abort', onTimeout, { once: true });
|
|
44
|
+
if (external.aborted)
|
|
45
|
+
onExternal();
|
|
46
|
+
return {
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
release: () => {
|
|
49
|
+
external.removeEventListener('abort', onExternal);
|
|
50
|
+
timeout.removeEventListener('abort', onTimeout);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* The ciphertext as a stream of views into it. Node's fetch copies a
|
|
56
|
+
* `Uint8Array` body whole — measured at +2× the file for a buffered one — and
|
|
57
|
+
* the file may be a gigabyte already held twice (plaintext and ciphertext).
|
|
58
|
+
* Pulled one chunk at a time, only a chunk is ever in flight.
|
|
59
|
+
*/
|
|
60
|
+
const bodyStream = (buf) => {
|
|
61
|
+
let offset = 0;
|
|
62
|
+
return new ReadableStream({
|
|
63
|
+
pull(controller) {
|
|
64
|
+
if (offset >= buf.byteLength) {
|
|
65
|
+
controller.close();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const end = Math.min(offset + UPLOAD_CHUNK_BYTES, buf.byteLength);
|
|
69
|
+
controller.enqueue(new Uint8Array(buf.buffer, buf.byteOffset + offset, end - offset));
|
|
70
|
+
offset = end;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
/** Drop a body we will not read; a peer that never ends it must not hold us. */
|
|
75
|
+
const discard = (res) => (res.body ? res.body.cancel().catch(() => undefined) : Promise.resolve());
|
|
76
|
+
/** The body as text, or null past `max` bytes — read no further than that. */
|
|
77
|
+
const readCapped = async (res, max) => {
|
|
78
|
+
if (!res.body)
|
|
79
|
+
return '';
|
|
80
|
+
const reader = res.body.getReader();
|
|
81
|
+
const chunks = [];
|
|
82
|
+
let size = 0;
|
|
83
|
+
for (;;) {
|
|
84
|
+
const { done, value } = await reader.read();
|
|
85
|
+
if (done)
|
|
86
|
+
break;
|
|
87
|
+
size += value.byteLength;
|
|
88
|
+
if (size > max) {
|
|
89
|
+
await reader.cancel().catch(() => undefined);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
chunks.push(value);
|
|
93
|
+
}
|
|
94
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
95
|
+
};
|
|
96
|
+
const reasonOf = (err) => {
|
|
97
|
+
if (err instanceof Error && (err.name === 'TimeoutError' || err.name === 'AbortError'))
|
|
98
|
+
return 'timed out';
|
|
99
|
+
return err instanceof Error ? err.message : String(err);
|
|
100
|
+
};
|
|
101
|
+
const tryLanDelivery = async (input) => {
|
|
102
|
+
const { target, file } = input;
|
|
103
|
+
const fetchFn = input.fetchFn ?? fetch;
|
|
104
|
+
const base = `http://${target.host}:${target.port}`;
|
|
105
|
+
let keys;
|
|
106
|
+
try {
|
|
107
|
+
keys = (0, lan_auth_js_1.deriveLanKeys)(await input.deriveSharedSecret(target.publicKey));
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
return { delivered: false, reason: `key derivation failed: ${reasonOf(err)}` };
|
|
111
|
+
}
|
|
112
|
+
// Ping first: an address that has gone stale, or now belongs to another
|
|
113
|
+
// machine, costs the ping timeout — not an upload of the whole file.
|
|
114
|
+
const pingDeadline = deadline(input.pingTimeoutMs ?? exports.LAN_PING_TIMEOUT_MS, input.signal);
|
|
115
|
+
try {
|
|
116
|
+
const fields = { senderDeviceId: input.senderDeviceId, transferId: lan_auth_js_1.LAN_PING_TRANSFER_ID, timestamp: Date.now(), nonce: (0, lan_auth_js_1.newNonce)() };
|
|
117
|
+
const mac = (0, lan_auth_js_1.signLanRequest)(keys.macKey, { ...fields, method: 'GET', path: lan_auth_js_1.LAN_PATHS.ping, bodySha256: lan_auth_js_1.EMPTY_BODY_SHA256 });
|
|
118
|
+
const res = await fetchFn(`${base}${lan_auth_js_1.LAN_PATHS.ping}`, {
|
|
119
|
+
headers: (0, lan_auth_js_1.toLanAuthHeaders)(fields, mac),
|
|
120
|
+
signal: pingDeadline.signal,
|
|
121
|
+
});
|
|
122
|
+
// Status and receipt are headers: decide on them before reading a
|
|
123
|
+
// body whose size and end the peer controls.
|
|
124
|
+
if (res.status !== 200) {
|
|
125
|
+
await discard(res);
|
|
126
|
+
return { delivered: false, reason: `ping answered ${res.status}` };
|
|
127
|
+
}
|
|
128
|
+
if (!(0, lan_auth_js_1.verifyLanReceipt)(keys.macKey, mac, res.headers.get(lan_auth_js_1.LAN_HEADERS.receipt))) {
|
|
129
|
+
await discard(res);
|
|
130
|
+
return { delivered: false, reason: 'ping answered without a valid receipt' };
|
|
131
|
+
}
|
|
132
|
+
const body = await readCapped(res, PING_BODY_MAX_BYTES);
|
|
133
|
+
let answeredBy;
|
|
134
|
+
try {
|
|
135
|
+
answeredBy = JSON.parse(body ?? '').deviceId;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
answeredBy = undefined;
|
|
139
|
+
}
|
|
140
|
+
if (answeredBy !== target.deviceId)
|
|
141
|
+
return { delivered: false, reason: 'ping answered by another device' };
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
return { delivered: false, reason: input.signal?.aborted ? 'cancelled' : `ping failed: ${reasonOf(err)}` };
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
pingDeadline.release();
|
|
148
|
+
}
|
|
149
|
+
const transferId = newTransferId();
|
|
150
|
+
const uploadDeadline = deadline(input.uploadTimeoutMs ?? (0, exports.lanUploadTimeoutMs)(file.ciphertext.byteLength), input.signal);
|
|
151
|
+
try {
|
|
152
|
+
const fields = { senderDeviceId: input.senderDeviceId, transferId, timestamp: Date.now(), nonce: (0, lan_auth_js_1.newNonce)() };
|
|
153
|
+
const mac = (0, lan_auth_js_1.signLanRequest)(keys.macKey, { ...fields, method: 'POST', path: lan_auth_js_1.LAN_PATHS.upload, bodySha256: (0, lan_auth_js_1.sha256Hex)(file.ciphertext) });
|
|
154
|
+
const meta = { fileName: file.fileName, fileType: file.fileType, fileSize: file.fileSize, iv: file.iv, deviceKeyMap: file.deviceKeyMap, transferId };
|
|
155
|
+
const init = {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: {
|
|
158
|
+
...(0, lan_auth_js_1.toLanAuthHeaders)(fields, mac),
|
|
159
|
+
[lan_auth_js_1.LAN_HEADERS.meta]: (0, lan_auth_js_1.sealMeta)(keys.metaKey, meta),
|
|
160
|
+
'content-type': 'application/octet-stream',
|
|
161
|
+
// Explicit, so a streamed body still goes with a length: the
|
|
162
|
+
// receiver refuses chunked uploads (411).
|
|
163
|
+
'content-length': String(file.ciphertext.byteLength),
|
|
164
|
+
},
|
|
165
|
+
body: bodyStream(file.ciphertext),
|
|
166
|
+
duplex: 'half',
|
|
167
|
+
signal: uploadDeadline.signal,
|
|
168
|
+
};
|
|
169
|
+
const res = await fetchFn(`${base}${lan_auth_js_1.LAN_PATHS.upload}`, init);
|
|
170
|
+
await discard(res);
|
|
171
|
+
if (res.status !== 201)
|
|
172
|
+
return { delivered: false, reason: `upload answered ${res.status}` };
|
|
173
|
+
if (!(0, lan_auth_js_1.verifyLanReceipt)(keys.macKey, mac, res.headers.get(lan_auth_js_1.LAN_HEADERS.receipt)))
|
|
174
|
+
return { delivered: false, reason: 'upload answered without a valid receipt' };
|
|
175
|
+
return { delivered: true, transferId };
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
return { delivered: false, reason: input.signal?.aborted ? 'cancelled' : `upload failed: ${reasonOf(err)}` };
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
uploadDeadline.release();
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
exports.tryLanDelivery = tryLanDelivery;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { LanPublisher } from './lan-endpoint.js';
|
|
2
|
+
import type { LanReceiver } from './lan-receiver.js';
|
|
3
|
+
/**
|
|
4
|
+
* Local transfer (ADR-0013) — the listener's lifecycle for it, in one place.
|
|
5
|
+
*
|
|
6
|
+
* Order of events the daemon cannot control: the WebSocket may open before or
|
|
7
|
+
* after the receiver is bound, and SIGTERM may land while the keys are still
|
|
8
|
+
* loading. Every branch here ends the same way — the device record never
|
|
9
|
+
* points at a port nobody is listening on:
|
|
10
|
+
*
|
|
11
|
+
* keys → bind → (socket already open? publish now) → watch the address
|
|
12
|
+
* socket open → force-publish (the record exists from $connect on)
|
|
13
|
+
* stop → stop the watch, `lan: null`, close the socket — or, if
|
|
14
|
+
* the bind has not finished yet, close it the moment it does
|
|
15
|
+
*/
|
|
16
|
+
export interface LanTransferDeps {
|
|
17
|
+
log: (msg: string) => void;
|
|
18
|
+
initCrypto: () => Promise<void>;
|
|
19
|
+
/** Bind the receiver; `onError` fires if the bound socket later dies. */
|
|
20
|
+
startReceiver: (hooks: {
|
|
21
|
+
onError: (err: Error) => void;
|
|
22
|
+
}) => Promise<LanReceiver>;
|
|
23
|
+
createPublisher: (port: number) => LanPublisher;
|
|
24
|
+
/** Whether the WebSocket has reached `open` right now. */
|
|
25
|
+
isOpen: () => boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface LanTransfer {
|
|
28
|
+
/** Call on every WebSocket `open`. No-op until the receiver is bound. */
|
|
29
|
+
onOpen: () => void;
|
|
30
|
+
/** Retract the endpoint and release the port. Idempotent. Resolves once
|
|
31
|
+
* the receiver's socket is closed; the retract itself is one frame on a
|
|
32
|
+
* socket that is already closing, so it is sent and not waited on. */
|
|
33
|
+
stop: () => Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
export declare const createLanTransfer: (deps: LanTransferDeps) => LanTransfer;
|
|
36
|
+
//# sourceMappingURL=lan-transfer.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createLanTransfer = void 0;
|
|
4
|
+
const createLanTransfer = (deps) => {
|
|
5
|
+
let live = null;
|
|
6
|
+
let stopping = false;
|
|
7
|
+
// A bound socket that dies later must not keep being re-affirmed by the
|
|
8
|
+
// watch: retract, release, and turn the feature off with one log line.
|
|
9
|
+
const onReceiverError = (err) => {
|
|
10
|
+
deps.log(`local transfer: off — receiver failed after bind (${err.message})`);
|
|
11
|
+
void teardown();
|
|
12
|
+
};
|
|
13
|
+
const ready = deps.initCrypto()
|
|
14
|
+
.then(() => deps.startReceiver({ onError: onReceiverError }))
|
|
15
|
+
.then((receiver) => {
|
|
16
|
+
if (stopping) {
|
|
17
|
+
// Bound after stop() was already called: nothing was published,
|
|
18
|
+
// so there is nothing to retract — just let the port go.
|
|
19
|
+
void receiver.stop();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const publisher = deps.createPublisher(receiver.port);
|
|
23
|
+
live = { publisher, receiver };
|
|
24
|
+
publisher.startWatch(receiver.port);
|
|
25
|
+
if (deps.isOpen())
|
|
26
|
+
publisher.publish(receiver.port, { force: true });
|
|
27
|
+
})
|
|
28
|
+
.catch((err) => {
|
|
29
|
+
deps.log(`local transfer: off — ${err instanceof Error ? err.message : String(err)}`);
|
|
30
|
+
});
|
|
31
|
+
const onOpen = () => {
|
|
32
|
+
if (live)
|
|
33
|
+
live.publisher.publish(live.receiver.port, { force: true });
|
|
34
|
+
};
|
|
35
|
+
const teardown = async () => {
|
|
36
|
+
if (!live)
|
|
37
|
+
return;
|
|
38
|
+
const current = live;
|
|
39
|
+
live = null;
|
|
40
|
+
current.publisher.stopWatch();
|
|
41
|
+
current.publisher.clear();
|
|
42
|
+
await current.receiver.stop();
|
|
43
|
+
};
|
|
44
|
+
let stopped = null;
|
|
45
|
+
const stop = () => {
|
|
46
|
+
if (stopped)
|
|
47
|
+
return stopped;
|
|
48
|
+
stopping = true;
|
|
49
|
+
stopped = ready.then(() => teardown());
|
|
50
|
+
return stopped;
|
|
51
|
+
};
|
|
52
|
+
return { onOpen, stop };
|
|
53
|
+
};
|
|
54
|
+
exports.createLanTransfer = createLanTransfer;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
export declare const hashListenerId: (seed: string) => string;
|
|
3
|
+
/** The id the sticky file holds, or null when it is missing or not an id. */
|
|
4
|
+
export declare const readStickyListenerId: () => string | null;
|
|
5
|
+
/**
|
|
6
|
+
* Leave `id` in the sticky file (0600, in the 0700 `~/.hwip`) so a later process that cannot
|
|
7
|
+
* read the machine id — `ioreg` stalled past its timeout — still gets this one. The writer is
|
|
8
|
+
* `computeListenerDeviceId` (the listener, `hwip rename`). A file that already holds `id` is left
|
|
9
|
+
* alone; one that holds another is replaced (an id a stalled start wrote down earlier). The new
|
|
10
|
+
* content goes in by rename, so a hook reading in the middle sees the old id or the new one, never
|
|
11
|
+
* an empty file. Best-effort: the id in memory stands.
|
|
12
|
+
*/
|
|
13
|
+
export declare const rememberListenerId: (id: string) => void;
|
|
14
|
+
/** Platform machine id (macOS IOPlatformUUID / Linux machine-id), or null. */
|
|
15
|
+
export declare const readMachineId: (deps?: {
|
|
16
|
+
platform?: NodeJS.Platform;
|
|
17
|
+
exec?: typeof execFileSync;
|
|
18
|
+
}) => string | null;
|
|
19
|
+
export declare const listenerDeviceId: () => string;
|
|
20
|
+
//# sourceMappingURL=listener-device-id.d.ts.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listenerDeviceId = exports.readMachineId = exports.rememberListenerId = exports.readStickyListenerId = exports.hashListenerId = 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 node_crypto_1 = require("node:crypto");
|
|
9
|
+
const private_files_js_1 = require("./private-files.js");
|
|
10
|
+
// The listener daemon writes this file (see listener.ts computeListenerDeviceId; `hwip rename`
|
|
11
|
+
// reaches the same function, and writes the same value); the other processes in the CLI
|
|
12
|
+
// (hwip-hook's agent-session context) only READ it, so both resolve the same id.
|
|
13
|
+
const LISTENER_ID_FILE = (0, node_path_1.join)((0, node_os_1.homedir)(), '.hwip', 'listener-device-id');
|
|
14
|
+
const hashListenerId = (seed) => `dev_listener_${(0, node_crypto_1.createHash)('sha256').update(seed).digest('hex').slice(0, 8)}`;
|
|
15
|
+
exports.hashListenerId = hashListenerId;
|
|
16
|
+
/** The id the sticky file holds, or null when it is missing or not an id. */
|
|
17
|
+
const readStickyListenerId = () => {
|
|
18
|
+
try {
|
|
19
|
+
const saved = (0, node_fs_1.readFileSync)(LISTENER_ID_FILE, 'utf-8').trim();
|
|
20
|
+
return /^dev_listener_[0-9a-f]{8}$/.test(saved) ? saved : null;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
exports.readStickyListenerId = readStickyListenerId;
|
|
27
|
+
/**
|
|
28
|
+
* Leave `id` in the sticky file (0600, in the 0700 `~/.hwip`) so a later process that cannot
|
|
29
|
+
* read the machine id — `ioreg` stalled past its timeout — still gets this one. The writer is
|
|
30
|
+
* `computeListenerDeviceId` (the listener, `hwip rename`). A file that already holds `id` is left
|
|
31
|
+
* alone; one that holds another is replaced (an id a stalled start wrote down earlier). The new
|
|
32
|
+
* content goes in by rename, so a hook reading in the middle sees the old id or the new one, never
|
|
33
|
+
* an empty file. Best-effort: the id in memory stands.
|
|
34
|
+
*/
|
|
35
|
+
const rememberListenerId = (id) => {
|
|
36
|
+
if ((0, exports.readStickyListenerId)() === id)
|
|
37
|
+
return;
|
|
38
|
+
// Random name, exclusive create: a predictable name is one another process could pre-create as a symlink.
|
|
39
|
+
const tmp = `${LISTENER_ID_FILE}.${(0, node_crypto_1.randomBytes)(6).toString('hex')}.tmp`;
|
|
40
|
+
try {
|
|
41
|
+
(0, private_files_js_1.ensurePrivateDir)((0, node_path_1.dirname)(LISTENER_ID_FILE));
|
|
42
|
+
(0, node_fs_1.writeFileSync)(tmp, `${id}\n`, { mode: 0o600, flag: 'wx' });
|
|
43
|
+
// After the write: the create mode is cut by the umask, and an older file's mode is not this one's.
|
|
44
|
+
(0, node_fs_1.chmodSync)(tmp, 0o600);
|
|
45
|
+
(0, private_files_js_1.handBackToSudoUser)(tmp);
|
|
46
|
+
(0, node_fs_1.renameSync)(tmp, LISTENER_ID_FILE);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
try {
|
|
50
|
+
(0, node_fs_1.unlinkSync)(tmp);
|
|
51
|
+
}
|
|
52
|
+
catch { /* never created, or already renamed */ }
|
|
53
|
+
/* not writable: nothing is lost that was not already */
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
exports.rememberListenerId = rememberListenerId;
|
|
57
|
+
/**
|
|
58
|
+
* ioreg by its absolute path: it lives in /usr/sbin, which a PATH from an IDE or a
|
|
59
|
+
* hook environment may lack, and a failed lookup falls back to a hostname hash —
|
|
60
|
+
* the same Mac registered as another device. launchd's own PATH has it; nothing
|
|
61
|
+
* else is promised. Shared by the listener, which registers the id, and the
|
|
62
|
+
* read-only reader here, so both read the same machine id.
|
|
63
|
+
*/
|
|
64
|
+
const IOREG = '/usr/sbin/ioreg';
|
|
65
|
+
/**
|
|
66
|
+
* A stalled ioreg (IOKit wedged) used to hold every hook, `hwip rename` and the
|
|
67
|
+
* listener's start for as long as it hung. A timed-out run throws, which the
|
|
68
|
+
* caller reads as "no machine id" and falls back to the sticky file, then the
|
|
69
|
+
* hostname hash. mcp-server (config.ts IOREG_TIMEOUT_MS) gives up at the same
|
|
70
|
+
* 5 s and falls back the same way. The two run ioreg at different moments, so
|
|
71
|
+
* agreeing on the id still rests on ioreg being fast; this only keeps a wedged
|
|
72
|
+
* one from deciding who waits longest. A listener whose FIRST start (no sticky
|
|
73
|
+
* file yet) stalls keeps the hostname id until it restarts; on macOS that id is
|
|
74
|
+
* not written down, so the restart recovers (C109).
|
|
75
|
+
*/
|
|
76
|
+
const IOREG_TIMEOUT_MS = 5_000;
|
|
77
|
+
/** Platform machine id (macOS IOPlatformUUID / Linux machine-id), or null. */
|
|
78
|
+
const readMachineId = (deps = {}) => {
|
|
79
|
+
const platform = deps.platform ?? process.platform;
|
|
80
|
+
const exec = deps.exec ?? node_child_process_1.execFileSync;
|
|
81
|
+
try {
|
|
82
|
+
if (platform === 'darwin') {
|
|
83
|
+
// stdio: swallow ioreg's stderr so a one-off `hwip rename` / hook
|
|
84
|
+
// never leaks ioreg warnings to the terminal.
|
|
85
|
+
const out = exec(IOREG, ['-rd1', '-c', 'IOPlatformExpertDevice'], {
|
|
86
|
+
encoding: 'utf-8',
|
|
87
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
88
|
+
timeout: IOREG_TIMEOUT_MS,
|
|
89
|
+
});
|
|
90
|
+
const m = out.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
|
|
91
|
+
if (m)
|
|
92
|
+
return m[1];
|
|
93
|
+
}
|
|
94
|
+
if (platform === 'linux') {
|
|
95
|
+
for (const p of ['/etc/machine-id', '/var/lib/dbus/machine-id']) {
|
|
96
|
+
try {
|
|
97
|
+
const v = (0, node_fs_1.readFileSync)(p, 'utf-8').trim();
|
|
98
|
+
if (v)
|
|
99
|
+
return v;
|
|
100
|
+
}
|
|
101
|
+
catch { /* try next path */ }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch { /* no machine id readable — fall through */ }
|
|
106
|
+
return null;
|
|
107
|
+
};
|
|
108
|
+
exports.readMachineId = readMachineId;
|
|
109
|
+
/**
|
|
110
|
+
* Listener device id, resolved READ-ONLY — MUST equal what `listener.ts`
|
|
111
|
+
* `computeListenerDeviceId()` registers, so pushes tagged with this id land
|
|
112
|
+
* under the listener's device (otherwise a hook/notify reaches the feed but
|
|
113
|
+
* never threads into the agent chat). Hash the platform machine id first, then
|
|
114
|
+
* the sticky file the listener persisted, then a hostname hash — the same order
|
|
115
|
+
* as the writer, but never writing the file here.
|
|
116
|
+
*/
|
|
117
|
+
let cached;
|
|
118
|
+
const listenerDeviceId = () => {
|
|
119
|
+
// The machine id does not change under a running process, and on macOS
|
|
120
|
+
// reading it is an `ioreg` spawn — once per process is enough.
|
|
121
|
+
cached ??= resolveListenerDeviceId();
|
|
122
|
+
return cached;
|
|
123
|
+
};
|
|
124
|
+
exports.listenerDeviceId = listenerDeviceId;
|
|
125
|
+
const resolveListenerDeviceId = () => {
|
|
126
|
+
const machineId = (0, exports.readMachineId)();
|
|
127
|
+
if (machineId)
|
|
128
|
+
return (0, exports.hashListenerId)(machineId);
|
|
129
|
+
const saved = (0, exports.readStickyListenerId)();
|
|
130
|
+
if (saved)
|
|
131
|
+
return saved;
|
|
132
|
+
return (0, exports.hashListenerId)((0, node_os_1.hostname)());
|
|
133
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** The listener's timestamped log line, and the switch that silences it for a CLI command. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.log = exports.silenceListenerLog = void 0;
|
|
5
|
+
const stamp = () => new Date().toISOString().slice(11, 19);
|
|
6
|
+
/**
|
|
7
|
+
* The daemon's log is stdout, which its supervisor redirects to a file. A CLI
|
|
8
|
+
* command that borrows these helpers has no such redirect — `hwip forget` calls
|
|
9
|
+
* `sessionExists`, and socket discovery logs from there — so the chatter would
|
|
10
|
+
* land in that command's own output, and inside a `--json` line it would break
|
|
11
|
+
* the parse.
|
|
12
|
+
*/
|
|
13
|
+
let logSilenced = false;
|
|
14
|
+
const silenceListenerLog = () => {
|
|
15
|
+
logSilenced = true;
|
|
16
|
+
};
|
|
17
|
+
exports.silenceListenerLog = silenceListenerLog;
|
|
18
|
+
const log = (msg) => {
|
|
19
|
+
if (logSilenced)
|
|
20
|
+
return;
|
|
21
|
+
console.log(`[${stamp()}] ${msg}`);
|
|
22
|
+
};
|
|
23
|
+
exports.log = log;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { type ChildProcess } from 'node:child_process';
|
|
2
|
+
export declare const LISTENER_PID_FILE: string;
|
|
3
|
+
/** CLI version the running daemon booted from. Absent ⇒ pre-stamp build. */
|
|
4
|
+
export declare const LISTENER_VERSION_FILE: string;
|
|
5
|
+
/**
|
|
6
|
+
* The socket the running daemon actually connected to. A stamp rather than a
|
|
7
|
+
* line in listener.log, because the log rotates at 5MB and a long-lived daemon
|
|
8
|
+
* — exactly the one whose config edit has been outlived — loses its own startup
|
|
9
|
+
* line to that rotation, which would silently switch the staleness check off.
|
|
10
|
+
*/
|
|
11
|
+
export declare const LISTENER_WS_URL_FILE: string;
|
|
12
|
+
export declare const LISTENER_LOG_FILE: string;
|
|
13
|
+
export declare const sleep: (ms: number) => Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* PID of the listener on record, or null when the file is missing or stale.
|
|
16
|
+
* Stale PID files (process gone) read as "no listener" so a crashed daemon
|
|
17
|
+
* never blocks the next autostart.
|
|
18
|
+
*/
|
|
19
|
+
export declare const runningListenerPid: () => number | null;
|
|
20
|
+
/**
|
|
21
|
+
* CLI version the running daemon started from, or null when unknown.
|
|
22
|
+
* null is meaningful: every build before the stamp existed predates the
|
|
23
|
+
* live-terminal protocol, so callers treat "unknown" as "stale".
|
|
24
|
+
*/
|
|
25
|
+
export declare const runningListenerVersion: () => string | null;
|
|
26
|
+
/** Claim the singleton slot for THIS process and stamp the version with it. */
|
|
27
|
+
export declare const writeListenerRuntime: (version: string, wsUrl?: string) => void;
|
|
28
|
+
/** The socket the running daemon connected to, or null when it is unknown. */
|
|
29
|
+
export declare const runningListenerWsUrl: () => string | null;
|
|
30
|
+
/**
|
|
31
|
+
* Drop the PID/version stamps, but only when they still name `pid` — a
|
|
32
|
+
* successor that already claimed the slot must not be trampled by a
|
|
33
|
+
* predecessor's exit handler.
|
|
34
|
+
*/
|
|
35
|
+
export declare const clearListenerRuntime: (pid: number) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Drop stamps left behind by a daemon that died without running its exit
|
|
38
|
+
* handler (crash, SIGKILL, reboot-era leftovers). No-op while one is alive.
|
|
39
|
+
*/
|
|
40
|
+
export declare const clearStaleListenerRuntime: () => void;
|
|
41
|
+
/** What `ps` says about a pid. */
|
|
42
|
+
export type PidCommand = {
|
|
43
|
+
readonly kind: 'running';
|
|
44
|
+
readonly command: string;
|
|
45
|
+
} | {
|
|
46
|
+
readonly kind: 'gone';
|
|
47
|
+
} | {
|
|
48
|
+
readonly kind: 'unchecked';
|
|
49
|
+
readonly reason: string;
|
|
50
|
+
};
|
|
51
|
+
export declare const commandOfPid: (pid: number) => PidCommand;
|
|
52
|
+
/**
|
|
53
|
+
* True when a command line is hwip's listener daemon: the word before
|
|
54
|
+
* `listener` is hwip's own script — the bin shim (`…/hwip`), the published
|
|
55
|
+
* package's entry (`…/@hwip/cli/dist/cli.js`: global install, npx), or this
|
|
56
|
+
* CLI's own file (a dev checkout). Any other `…/cli.js listener` (Zeph's runs
|
|
57
|
+
* on the same machines), or a file that only sits under a `hwip*` directory,
|
|
58
|
+
* does not pass. Exported for tests, which pass `ownCliPath`.
|
|
59
|
+
*/
|
|
60
|
+
export declare const isHwipListenerCommand: (command: string, ownCliPath?: string | null) => boolean;
|
|
61
|
+
/** Whether the pid in the pid file is still hwip's listener. */
|
|
62
|
+
export type ListenerPidCheck = {
|
|
63
|
+
readonly kind: 'ours';
|
|
64
|
+
} | {
|
|
65
|
+
readonly kind: 'gone';
|
|
66
|
+
}
|
|
67
|
+
/** Another process holds the pid — only its program name, since its
|
|
68
|
+
* arguments can hold secrets. */
|
|
69
|
+
| {
|
|
70
|
+
readonly kind: 'foreign';
|
|
71
|
+
readonly program: string;
|
|
72
|
+
}
|
|
73
|
+
/** hwip's listener from another checkout or worktree
|
|
74
|
+
* (`…/cli-wt-x/dist/cli.js listener`), which `isHwipListenerCommand`
|
|
75
|
+
* cannot vouch for. Left running with its stamps — clearing them would
|
|
76
|
+
* let a second daemon start and answer every push twice. */
|
|
77
|
+
| {
|
|
78
|
+
readonly kind: 'other-listener';
|
|
79
|
+
readonly script: string;
|
|
80
|
+
} | {
|
|
81
|
+
readonly kind: 'unchecked';
|
|
82
|
+
readonly reason: string;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* The pid file can outlive a crashed daemon and its pid be reused by any
|
|
86
|
+
* process, so a pid is only ours while its command line says so.
|
|
87
|
+
*/
|
|
88
|
+
export declare const checkListenerPid: (pid: number, commandOf?: (pid: number) => PidCommand, isHwipScript?: (script: string) => boolean) => ListenerPidCheck;
|
|
89
|
+
/** What every caller says about an `other-listener`. */
|
|
90
|
+
export declare const otherListenerNote: (pid: number, script: string) => string;
|
|
91
|
+
/**
|
|
92
|
+
* Whether the pid on record should be treated as a live listener: hwip's own
|
|
93
|
+
* or listener-shaped command line, or one `ps` could not check — two daemons
|
|
94
|
+
* answering the same pushes is the worse failure. `hwip verify` and the
|
|
95
|
+
* startup guard ask the same question.
|
|
96
|
+
*/
|
|
97
|
+
export declare const isListenerPid: (pid: number, commandOf?: (pid: number) => PidCommand) => boolean;
|
|
98
|
+
/**
|
|
99
|
+
* The pid of another hwip listener holding the singleton slot, or null. A pid
|
|
100
|
+
* the OS handed to an unrelated process is not a listener: deferring to it
|
|
101
|
+
* would end launchd's or `hwip cc`'s daemon with exit 0 and leave no listener
|
|
102
|
+
* at all, so its stamps are cleared and the slot is free.
|
|
103
|
+
*/
|
|
104
|
+
export declare const otherListenerPid: (selfPid?: number, commandOf?: (pid: number) => PidCommand) => number | null;
|
|
105
|
+
export type ListenerStop = {
|
|
106
|
+
readonly kind: 'stopped';
|
|
107
|
+
}
|
|
108
|
+
/** It outlived the grace window and took SIGKILL. */
|
|
109
|
+
| {
|
|
110
|
+
readonly kind: 'killed';
|
|
111
|
+
} | Exclude<ListenerPidCheck, {
|
|
112
|
+
kind: 'ours';
|
|
113
|
+
}>;
|
|
114
|
+
/**
|
|
115
|
+
* Stop the daemon and wait for the process to actually go away — but only a
|
|
116
|
+
* process that is still hwip's listener (`checkListenerPid`); anything else is
|
|
117
|
+
* left alone and reported, and a pid file pointing at a stranger is cleared.
|
|
118
|
+
* SIGTERM first so its exit handler clears the stamps, SIGKILL once the grace
|
|
119
|
+
* window expires.
|
|
120
|
+
*
|
|
121
|
+
* Waiting matters: the caller's next move is spawning a replacement, and two
|
|
122
|
+
* live daemons on one machine both answer the same pushes.
|
|
123
|
+
*/
|
|
124
|
+
export declare const stopListener: (pid: number, commandOf?: (pid: number) => PidCommand, timeoutMs?: number) => Promise<ListenerStop>;
|
|
125
|
+
/**
|
|
126
|
+
* Set by the listener on the `hwip <agent> --detach` it runs for a schedule. That child is the listener's own: when its
|
|
127
|
+
* build is newer than the running listener's (after an upgrade) `ensureListenerRunning` would restart the listener, and
|
|
128
|
+
* the restart would end the launch's own parent. With this set it leaves the listener alone, and removes the variable from
|
|
129
|
+
* its environment so that nothing the launch starts after it (the tmux server, the agent) inherits it.
|
|
130
|
+
*/
|
|
131
|
+
export declare const LISTENER_LAUNCH_ENV = "HWIP_LISTENER_LAUNCH";
|
|
132
|
+
/**
|
|
133
|
+
* Path to the running cli.js entry. wrapper.js/listener.js sit next to cli.js
|
|
134
|
+
* in dist/, so __dirname resolves it directly — independent of how the user
|
|
135
|
+
* invoked us.
|
|
136
|
+
*
|
|
137
|
+
* `process.argv[1]` is unreliable here: when `hwip` runs via the npm-installed
|
|
138
|
+
* bin shim (`/usr/local/bin/hwip` → cli.js via a wrapper script), argv[1] is
|
|
139
|
+
* the shim path (`.../bin/hwip`), NOT `cli.js`. That made the original
|
|
140
|
+
* `/cli\.(js|ts|mjs|cjs)$/` check silently reject the entry and the autospawn
|
|
141
|
+
* never fired — the bug only surfaced once the user switched to the global
|
|
142
|
+
* npm install.
|
|
143
|
+
*
|
|
144
|
+
* Fall back to argv[1] only when the __dirname-relative file doesn't exist
|
|
145
|
+
* (some packaging where dist layout differs).
|
|
146
|
+
*/
|
|
147
|
+
export declare const resolveCliPath: () => string | null;
|
|
148
|
+
export declare const rotateListenerLogIfLarge: () => void;
|
|
149
|
+
/**
|
|
150
|
+
* How long a daemon `hwip cc` just spawned gets to fail. The fast deaths — no
|
|
151
|
+
* key, no tmux, no socket URL — come before the daemon writes its pid file;
|
|
152
|
+
* a rejected key closes the first socket within about a second.
|
|
153
|
+
*/
|
|
154
|
+
export declare const LISTENER_START_GRACE_MS = 1500;
|
|
155
|
+
export type ListenerStart = {
|
|
156
|
+
readonly kind: 'running';
|
|
157
|
+
}
|
|
158
|
+
/** It exited 0: another daemon won the singleton slot, so one is running. */
|
|
159
|
+
| {
|
|
160
|
+
readonly kind: 'deferred';
|
|
161
|
+
}
|
|
162
|
+
/** It died inside the grace window; `line` is the last line it logged. */
|
|
163
|
+
| {
|
|
164
|
+
readonly kind: 'died';
|
|
165
|
+
readonly code: number | null;
|
|
166
|
+
readonly line: string | null;
|
|
167
|
+
}
|
|
168
|
+
/** launchd started it, and after the grace window it is not running. */
|
|
169
|
+
| {
|
|
170
|
+
readonly kind: 'not-up';
|
|
171
|
+
readonly line: string | null;
|
|
172
|
+
} | {
|
|
173
|
+
readonly kind: 'not-spawned';
|
|
174
|
+
};
|
|
175
|
+
/** Why a start failed, in one line for the user; null when a listener is up. */
|
|
176
|
+
export declare const startFailure: (started: ListenerStart) => string | null;
|
|
177
|
+
/** Where the log ends now: what a start writes after this is that start's. */
|
|
178
|
+
export declare const listenerLogSize: () => number;
|
|
179
|
+
/**
|
|
180
|
+
* Spawn the daemon and watch it through the grace window. A daemon that dies
|
|
181
|
+
* on a missing key, a missing tmux or a rejected key only says so in the log,
|
|
182
|
+
* so `hwip cc` used to print "listener autostarted" over a dead one and the
|
|
183
|
+
* phone silently lost control.
|
|
184
|
+
*/
|
|
185
|
+
export declare const startListenerDetached: (spawnChild?: () => Pick<ChildProcess, "once" | "exitCode"> | null, graceMs?: number) => Promise<ListenerStart>;
|
|
186
|
+
/** How long launchd gets to bring a kickstarted daemon as far as its pid file. */
|
|
187
|
+
export declare const LISTENER_SERVICE_START_MS = 5000;
|
|
188
|
+
/**
|
|
189
|
+
* After `launchctl kickstart`: launchd starts the daemon, so there is no child
|
|
190
|
+
* to watch — only the pid file it writes once its key, tmux and socket URL
|
|
191
|
+
* check out, and the log it says why in. A daemon that dies there is restarted
|
|
192
|
+
* by launchd on a loop, and `hwip cc` used to report it "started". Waits for a
|
|
193
|
+
* pid other than `previousPid` (the daemon `kickstart -k` replaced may still
|
|
194
|
+
* be shutting down), then for the grace window a rejected key needs.
|
|
195
|
+
*/
|
|
196
|
+
export declare const awaitServiceListener: (logOffset: number, previousPid: number | null, deps?: {
|
|
197
|
+
sleep: (ms: number) => Promise<void>;
|
|
198
|
+
runningPid: () => number | null;
|
|
199
|
+
}, graceMs?: number, startMs?: number) => Promise<ListenerStart>;
|
|
200
|
+
//# sourceMappingURL=listener-process.d.ts.map
|