@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.
Files changed (245) hide show
  1. package/LICENSE +190 -0
  2. package/NOTICE +11 -0
  3. package/README.md +1983 -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 +305 -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 +39 -0
  39. package/dist/blocked-prompt-seal.js +92 -0
  40. package/dist/blocked-prompt.d.ts +62 -0
  41. package/dist/blocked-prompt.js +518 -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 +278 -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 +5681 -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
package/dist/verify.js ADDED
@@ -0,0 +1,396 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleVerify = exports.classifyMcpRegistration = exports.binaryResolves = exports.registeredMcpArgv = exports.UNREADABLE = exports.readCodexMcpArgv = exports.claudeHasHwip = void 0;
4
+ const fs_1 = require("fs");
5
+ const os_1 = require("os");
6
+ const path_1 = require("path");
7
+ const agents_js_1 = require("./agents.js");
8
+ const config_js_1 = require("./config.js");
9
+ const listener_service_js_1 = require("./listener-service.js");
10
+ const hwip_hook_js_1 = require("./hwip-hook.js");
11
+ const jsonc_doc_js_1 = require("./jsonc-doc.js");
12
+ const notify_failure_js_1 = require("./notify-failure.js");
13
+ const codex_config_js_1 = require("./codex-config.js");
14
+ const mcp_command_js_1 = require("./mcp-command.js");
15
+ const listener_process_js_1 = require("./listener-process.js");
16
+ const insecure_url_js_1 = require("./insecure-url.js");
17
+ const redact_js_1 = require("./redact.js");
18
+ const ws_url_js_1 = require("./ws-url.js");
19
+ const args_js_1 = require("./args.js");
20
+ const HOME = (0, os_1.homedir)();
21
+ /** A settings file's value — `{}` for an empty file and without an editor's BOM,
22
+ * comments and trailing commas allowed, as the installer reads it; undefined
23
+ * when it is missing or does not parse. */
24
+ const readJson = (filePath) => {
25
+ try {
26
+ return (0, jsonc_doc_js_1.parseJsonc)((0, fs_1.readFileSync)(filePath, 'utf-8'));
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ };
32
+ const isPlainObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
33
+ /** Is the hwip plugin installed in Claude Code, or a `hwip` MCP server registered in ~/.claude.json? Exported for tests. */
34
+ const claudeHasHwip = (home) => {
35
+ const installed = readJson((0, path_1.join)(home, '.claude', 'plugins', 'installed_plugins.json'));
36
+ const plugins = isPlainObject(installed) ? installed.plugins : undefined;
37
+ const installs = isPlainObject(plugins) ? plugins['hwip@hwip'] : undefined;
38
+ // An array of installs (v2), or one install object (v1).
39
+ if ((Array.isArray(installs) && installs.length > 0) || isPlainObject(installs))
40
+ return true;
41
+ const claude = readJson((0, path_1.join)(home, '.claude.json'));
42
+ const servers = isPlainObject(claude) ? claude.mcpServers : undefined;
43
+ return isPlainObject(servers) && 'hwip' in servers;
44
+ };
45
+ exports.claudeHasHwip = claudeHasHwip;
46
+ /** Does a shared rule file contain the Hwip managed block? */
47
+ const hasManagedBlock = (filePath) => {
48
+ try {
49
+ return (0, fs_1.readFileSync)(filePath, 'utf-8').includes('HWIP:START');
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ };
55
+ // Per-agent: report whether the rule artifact Hwip installs is present.
56
+ const AGENT_RULE_PRESENT = {
57
+ // The plugin `hwip install` adds, or an MCP server registered by hand —
58
+ // not any mention of "hwip" anywhere in ~/.claude.json (a project path).
59
+ claude: () => (0, exports.claudeHasHwip)(HOME),
60
+ cursor: () => (0, fs_1.existsSync)((0, path_1.join)(HOME, '.cursor', 'rules', 'hwip.mdc')),
61
+ windsurf: () => hasManagedBlock((0, path_1.join)(HOME, '.codeium', 'windsurf', 'memories', 'global_rules.md')),
62
+ gemini: () => hasManagedBlock((0, path_1.join)(HOME, '.gemini', 'GEMINI.md')),
63
+ codex: () => hasManagedBlock((0, path_1.join)(HOME, '.codex', 'AGENTS.md')),
64
+ copilot: () => (0, fs_1.existsSync)((0, path_1.join)(HOME, '.copilot', 'instructions', 'hwip.instructions.md')),
65
+ cline: () => (0, fs_1.existsSync)((0, path_1.join)(HOME, '.cline', 'rules', 'hwip.md')),
66
+ aider: () => (0, fs_1.existsSync)((0, path_1.join)(HOME, '.hwip', 'aider-conventions.md')),
67
+ pi: () => hasManagedBlock((0, path_1.join)(HOME, '.pi', 'agent', 'AGENTS.md')),
68
+ opencode: () => hasManagedBlock((0, path_1.join)(HOME, '.config', 'opencode', 'AGENTS.md')),
69
+ };
70
+ /** Codex keeps its registry in TOML, so its reader is text-level — the table
71
+ * shape lives in codex-config.ts, which install and uninstall share. */
72
+ const readCodexMcpArgv = (filePath) => {
73
+ try {
74
+ return (0, codex_config_js_1.readHwipMcpArgv)((0, fs_1.readFileSync)(filePath, 'utf-8'));
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ };
80
+ exports.readCodexMcpArgv = readCodexMcpArgv;
81
+ /**
82
+ * Where each agent records the MCP launch, and how to read it back — the row
83
+ * carries its own reader because codex keeps its registry in TOML while every
84
+ * other agent keeps it in JSON.
85
+ * `agent` matches the label `detectAgents` uses so one report never names the
86
+ * same agent two ways. Cursor/Windsurf/OpenCode are files the installer writes
87
+ * itself; the Gemini row is READ-ONLY — `hwip install` shells out to
88
+ * `gemini mcp add`, so this path is the gemini CLI's own storage layout,
89
+ * confirmed by reading the file it produced, not something Hwip chose.
90
+ */
91
+ const MCP_REGISTRIES = [
92
+ { agent: 'Cursor', path: (0, path_1.join)(HOME, '.cursor', 'mcp.json'), argv: (p) => (0, exports.registeredMcpArgv)(p, 'mcpServers') },
93
+ { agent: 'Windsurf', path: (0, path_1.join)(HOME, '.codeium', 'windsurf', 'mcp_config.json'), argv: (p) => (0, exports.registeredMcpArgv)(p, 'mcpServers') },
94
+ { agent: 'Gemini CLI', path: (0, path_1.join)(HOME, '.gemini', 'settings.json'), argv: (p) => (0, exports.registeredMcpArgv)(p, 'mcpServers') },
95
+ { agent: 'OpenCode', path: (0, path_1.join)(HOME, '.config', 'opencode', 'opencode.json'), argv: (p) => (0, exports.registeredMcpArgv)(p, 'mcp') },
96
+ { agent: 'Codex CLI', path: (0, path_1.join)(HOME, '.codex', 'config.toml'), argv: exports.readCodexMcpArgv },
97
+ ];
98
+ /** A registry file that exists but is not a JSON object — its hwip entry, if any, is unknowable. */
99
+ exports.UNREADABLE = 'unreadable';
100
+ /**
101
+ * The launch argv a registry file records for hwip, verbatim — null when the
102
+ * file is absent or has no hwip entry, `UNREADABLE` when it exists but is not
103
+ * a JSON object. Two schemas in the wild:
104
+ * `command` + `args` (Cursor/Windsurf/Gemini) and `command` as a whole array
105
+ * (opencode). Returned as-is on purpose: a stale entry is only detectable if
106
+ * the reader does not normalize it into the shape it expects.
107
+ */
108
+ const registeredMcpArgv = (filePath, key) => {
109
+ if (!(0, fs_1.existsSync)(filePath))
110
+ return null;
111
+ const data = readJson(filePath);
112
+ if (!isPlainObject(data))
113
+ return exports.UNREADABLE;
114
+ const container = data[key];
115
+ const entry = container?.hwip;
116
+ if (!entry)
117
+ return null;
118
+ if (Array.isArray(entry.command))
119
+ return entry.command.map(String);
120
+ if (typeof entry.command !== 'string')
121
+ return null;
122
+ return [entry.command, ...(Array.isArray(entry.args) ? entry.args.map(String) : [])];
123
+ };
124
+ exports.registeredMcpArgv = registeredMcpArgv;
125
+ /**
126
+ * Can this launch binary actually be run? Registered commands come out of a
127
+ * config file, so unlike everywhere else in the CLI they can be an absolute
128
+ * path — `which` is the wrong question for one of those, hence the split.
129
+ * The bare-name half is `resolveCommand`; neither half touches a shell, so a
130
+ * path with a space in it stays one word and a `;` in the file stays inert.
131
+ */
132
+ const binaryResolves = (bin) => {
133
+ if (!bin.includes('/'))
134
+ return (0, agents_js_1.resolveCommand)(bin) !== null;
135
+ try {
136
+ (0, fs_1.accessSync)(bin, fs_1.constants.X_OK);
137
+ return true;
138
+ }
139
+ catch {
140
+ return false;
141
+ }
142
+ };
143
+ exports.binaryResolves = binaryResolves;
144
+ /** null = this agent has no hwip registration at all, so there is nothing to report. */
145
+ const classifyMcpRegistration = (argv, isOnPath) => {
146
+ if (!argv || argv.length === 0)
147
+ return null;
148
+ if (!isOnPath(argv[0]))
149
+ return { state: 'unresolvable', argv };
150
+ const isCanonical = argv.length === mcp_command_js_1.MCP_LAUNCH_ARGV.length
151
+ && argv.every((token, i) => token === mcp_command_js_1.MCP_LAUNCH_ARGV[i]);
152
+ return isCanonical ? { state: 'current' } : { state: 'stale', argv };
153
+ };
154
+ exports.classifyMcpRegistration = classifyMcpRegistration;
155
+ /** The registries that actually hold a hwip entry, classified. Agents that were
156
+ * never installed drop out here so the report has no row for them. */
157
+ const activeMcpRegistrations = (isOnPath) => MCP_REGISTRIES.flatMap((registry) => {
158
+ const argv = registry.argv(registry.path);
159
+ if (argv === exports.UNREADABLE)
160
+ return [{ agent: registry.agent, result: { state: 'unreadable', path: registry.path } }];
161
+ const result = (0, exports.classifyMcpRegistration)(argv, isOnPath);
162
+ return result ? [{ agent: registry.agent, result }] : [];
163
+ });
164
+ /** Where a resolved WebSocket URL came from, in the user's terms. */
165
+ const WS_SOURCE_LABEL = {
166
+ flag: '--ws-url',
167
+ config: '~/.hwip/config.json',
168
+ 'env-deprecated': 'HWIP_WS_URL (deprecated)',
169
+ default: 'built-in default',
170
+ };
171
+ /**
172
+ * The URL the live daemon connected to, or null when none is running.
173
+ *
174
+ * The stamp first; the log only as a fallback, for a daemon started by a build
175
+ * that predates the stamp. Reading the log alone was not enough — it rotates at
176
+ * 5MB, and the daemon whose startup line has been rotated away is precisely the
177
+ * long-lived one whose config edit this check exists to catch.
178
+ */
179
+ const liveListenerWsUrl = () => {
180
+ if ((0, listener_process_js_1.runningListenerPid)() === null)
181
+ return null;
182
+ const stamped = (0, listener_process_js_1.runningListenerWsUrl)();
183
+ if (stamped)
184
+ return stamped;
185
+ try {
186
+ return (0, ws_url_js_1.parseListenerStartUrl)((0, fs_1.readFileSync)(listener_process_js_1.LISTENER_LOG_FILE, 'utf-8'));
187
+ }
188
+ catch {
189
+ return null;
190
+ }
191
+ };
192
+ const handleVerify = async (args) => {
193
+ const doPing = (0, args_js_1.flagOn)(args, 'ping');
194
+ // `--json`: the same checks as one JSON object on stdout, nothing else.
195
+ const json = (0, args_js_1.flagOn)(args, 'json');
196
+ const say = (line) => { if (!json)
197
+ console.log(line); };
198
+ const checks = [];
199
+ let current = '';
200
+ const section = (name) => {
201
+ current = name;
202
+ say(`\n ${name}:`);
203
+ };
204
+ const record = (label, state) => {
205
+ checks.push({ section: current, label, state });
206
+ say(` ${state === 'pass' ? '✓' : state === 'warn' ? '!' : '✗'} ${label}`);
207
+ };
208
+ // A bare flag parses as `true`, which is no key and no URL.
209
+ const flag = (name) => (typeof args[name] === 'string' ? args[name] : undefined);
210
+ say(`\n Hwip verify — v${config_js_1.VERSION}`);
211
+ // ── Credentials ──────────────────────────────────────────────
212
+ section('Credentials');
213
+ const config = (0, config_js_1.loadConfig)();
214
+ // Resolved as every other command resolves them (cli.ts createHook), so
215
+ // `--ping` tests the key and the API the user's commands actually use.
216
+ const keySource = flag('key') ? '--key' : (0, config_js_1.resolvedEnv)('HWIP_API_KEY') ? 'HWIP_API_KEY' : config.apiKey ? '~/.hwip/config.json' : null;
217
+ const apiKey = flag('key') || (0, config_js_1.resolvedEnv)('HWIP_API_KEY') || config.apiKey;
218
+ const fileKey = (0, config_js_1.keyIsFromFile)(flag('key'), config);
219
+ // A config.json that cannot be used is a failure of its own, whatever else resolves: the listener runs with its options off.
220
+ const ignored = (0, config_js_1.configIgnoredBecause)();
221
+ if (ignored) {
222
+ record(`${ignored} — the listener runs with its options off (turn history, keep-awake) and nothing writes over it: fix it, or move it aside`, 'fail');
223
+ }
224
+ let baseUrl = ws_url_js_1.DEFAULT_API_BASE;
225
+ let baseKnown = true;
226
+ try {
227
+ baseUrl = (0, config_js_1.resolveBaseUrl)(flag('base-url'), config, fileKey);
228
+ }
229
+ catch (err) {
230
+ baseKnown = false;
231
+ record(err instanceof Error ? err.message : String(err), 'fail');
232
+ }
233
+ const hookId = (0, config_js_1.resolveHookId)();
234
+ const baseInsecure = (0, insecure_url_js_1.insecureUrlWarning)(baseUrl);
235
+ record(keySource ? `API key set (from ${keySource})` : 'API key not set (--key, HWIP_API_KEY or ~/.hwip/config.json)', apiKey ? 'pass' : 'fail');
236
+ record(hookId
237
+ ? 'HWIP_HOOK_ID is set (two-way hwip_ask enabled)'
238
+ : 'HWIP_HOOK_ID not set (notify-only — set it for remote control)', hookId ? 'pass' : 'warn');
239
+ if (baseInsecure)
240
+ record(baseInsecure.replace(/^hwip: /, ''), 'warn');
241
+ // ── Runtime ──────────────────────────────────────────────────
242
+ // One resolution per binary: `hwip` alone was being looked up five times
243
+ // (twice here, once per registry below), and each lookup is a spawn.
244
+ const resolved = new Map();
245
+ const resolves = (bin) => {
246
+ const cached = resolved.get(bin);
247
+ if (cached !== undefined)
248
+ return cached;
249
+ const found = (0, exports.binaryResolves)(bin);
250
+ resolved.set(bin, found);
251
+ return found;
252
+ };
253
+ section('Runtime');
254
+ const hasNode = resolves('node');
255
+ const hasNpx = resolves('npx');
256
+ const hasHwip = resolves('hwip');
257
+ record(hasNode ? 'node available' : 'node not found', hasNode ? 'pass' : 'fail');
258
+ record(hasNpx ? 'npx available (hook fallback when hwip is off PATH)' : 'npx not found', hasNpx ? 'pass' : 'warn');
259
+ // Not a warning any more: `hwip mcp` IS the MCP server, so a hwip that
260
+ // isn't on PATH means no MCP tools at all, not just a slower first call.
261
+ record(hasHwip
262
+ ? 'hwip CLI on PATH (runs the MCP server and the hooks)'
263
+ : 'hwip CLI not on PATH — MCP tools will not start. Reinstall: npm i -g @hwip/cli', hasHwip ? 'pass' : 'fail');
264
+ // Notify-only setups need neither, so both are warnings. The listener
265
+ // dies on a missing key or tmux and says so only in its log.
266
+ const hasTmux = resolves('tmux');
267
+ record(hasTmux
268
+ ? 'tmux available (hwip cc and phone control run in it)'
269
+ : 'tmux not found — hwip cc and phone control need it', hasTmux ? 'pass' : 'warn');
270
+ // A reused pid is alive too: only a listener's command line counts, as in
271
+ // the daemon's own startup guard.
272
+ const pidOnRecord = (0, listener_process_js_1.runningListenerPid)();
273
+ const listenerPid = pidOnRecord !== null && (0, listener_process_js_1.isListenerPid)(pidOnRecord) ? pidOnRecord : null;
274
+ record(listenerPid !== null
275
+ ? `listener running (pid ${listenerPid})`
276
+ : `listener not running — the phone cannot reach agent sessions; \`hwip cc\` starts it (log: ${listener_process_js_1.LISTENER_LOG_FILE})`, listenerPid !== null ? 'pass' : 'warn');
277
+ // ── Login-time service ───────────────────────────────────────
278
+ // Optional, so a missing one is a warning. A broken one is not: every way
279
+ // it breaks still looks installed from the outside.
280
+ const serviceRows = (0, listener_service_js_1.serviceHealthChecks)((0, listener_service_js_1.serviceStatus)());
281
+ if (serviceRows.length > 0) {
282
+ section('Login-time service');
283
+ for (const row of serviceRows)
284
+ record(row.label, row.state);
285
+ }
286
+ // ── WebSocket ────────────────────────────────────────────────
287
+ // The phone's Agents list is fed over this socket, so a machine on the
288
+ // wrong stage is simply absent from the app with nothing to explain it.
289
+ section('WebSocket');
290
+ const wsBaseUrl = baseUrl;
291
+ const staleWsEnv = (0, ws_url_js_1.legacyWsEnvNotice)(config, process.env, fileKey ? { baseUrl: wsBaseUrl } : undefined);
292
+ if (staleWsEnv)
293
+ record(staleWsEnv.replace(/^hwip: /, ''), 'warn');
294
+ const ws = (0, ws_url_js_1.resolveWsUrlDetailed)(args, config, wsBaseUrl, fileKey);
295
+ if (!baseKnown) {
296
+ record('WebSocket URL not checked — it and the API base may be set in the config.json that cannot be read', 'warn');
297
+ }
298
+ else if (!ws) {
299
+ record('no WebSocket URL, and none can be guessed for a custom API base — set "wsUrl" in ~/.hwip/config.json', 'fail');
300
+ }
301
+ else {
302
+ const wsProblem = (0, ws_url_js_1.wsUrlProblem)(ws.url);
303
+ const wsInsecure = wsProblem ? null : (0, insecure_url_js_1.insecureUrlWarning)(ws.url);
304
+ record(wsProblem ?? `${(0, redact_js_1.redactSecrets)(ws.url)} (from ${WS_SOURCE_LABEL[ws.source]})`, wsProblem ? 'fail' : 'pass');
305
+ if (wsInsecure)
306
+ record(wsInsecure.replace(/^hwip: /, ''), 'warn');
307
+ // Socket on one stage and REST on another splits the device: pushes go
308
+ // one way, the agent list the other, and neither half looks broken.
309
+ const wsStage = (0, ws_url_js_1.endpointStage)(ws.url);
310
+ const apiStage = (0, ws_url_js_1.endpointStage)(wsBaseUrl);
311
+ record(wsStage === apiStage
312
+ ? `API base is the same stage (${apiStage})`
313
+ : `stage mismatch — socket is ${wsStage}, API base is ${apiStage} (${wsBaseUrl}). Pushes and the agent list would land in different environments`, wsStage === apiStage ? 'pass' : 'warn');
314
+ // The daemon reads this once, at startup. Editing the config does
315
+ // nothing to a listener that has been up since before the edit — which
316
+ // looks exactly like the config not being read at all.
317
+ const live = liveListenerWsUrl();
318
+ if (live && live !== ws.url) {
319
+ record(`the running listener is on ${live}, not the resolved URL — restart it: hwip listener --restart`, 'warn');
320
+ }
321
+ }
322
+ // ── MCP registrations ────────────────────────────────────────
323
+ const registrations = activeMcpRegistrations(resolves);
324
+ if (registrations.length > 0) {
325
+ section('MCP registrations');
326
+ for (const { agent, result } of registrations) {
327
+ if (result.state === 'current')
328
+ record(`${agent}: launches \`${mcp_command_js_1.MCP_LAUNCH_ARGV.join(' ')}\``, 'pass');
329
+ else if (result.state === 'stale') {
330
+ record(`${agent}: still launches \`${result.argv.join(' ')}\` — one extra resident process per session. Re-run: hwip install`, 'warn');
331
+ }
332
+ else if (result.state === 'unresolvable') {
333
+ record(`${agent}: launches \`${result.argv[0]}\`, which is not on PATH — hwip tools will not load. Re-run: hwip install`, 'fail');
334
+ }
335
+ else {
336
+ // Not "unregistered": an entry in a file nothing can parse is unknown, not absent.
337
+ record(`${agent}: could not read ${result.path} as a JSON object — check its hwip entry by hand`, 'warn');
338
+ }
339
+ }
340
+ }
341
+ // ── Last push ────────────────────────────────────────────────
342
+ // Hooks run `hwip notify` with stderr discarded, so this record is the only
343
+ // trace a failed push leaves (notify-failure.ts). A good push deletes it.
344
+ section('Push delivery');
345
+ const lastFailure = (0, notify_failure_js_1.readNotifyFailure)();
346
+ if (lastFailure) {
347
+ record((0, notify_failure_js_1.describeNotifyFailure)(lastFailure), (0, notify_failure_js_1.isTransientNotifyFailure)(lastFailure) ? 'warn' : 'fail');
348
+ }
349
+ else {
350
+ record('no failed push since the last successful one', 'pass');
351
+ }
352
+ // ── Per-agent config ─────────────────────────────────────────
353
+ section('Agents');
354
+ const detected = (0, agents_js_1.detectAgents)().filter((a) => a.detected);
355
+ if (detected.length === 0) {
356
+ record('no supported agents detected', 'warn');
357
+ }
358
+ for (const agent of detected) {
359
+ const present = AGENT_RULE_PRESENT[agent.id]?.() ?? false;
360
+ record(`${agent.name}: ${present ? 'Hwip rules installed' : 'Hwip rules NOT installed — run: hwip install'}`, present ? 'pass' : 'warn');
361
+ }
362
+ // ── Optional live API ping ───────────────────────────────────
363
+ if (doPing) {
364
+ section('API ping');
365
+ if (!apiKey) {
366
+ record('skipped — no API key', 'warn');
367
+ }
368
+ else if (!baseKnown) {
369
+ record('skipped — the key would go to the default API, not necessarily the one it belongs to', 'warn');
370
+ }
371
+ else {
372
+ try {
373
+ const hook = new hwip_hook_js_1.HwipHook({ apiKey, baseUrl });
374
+ await hook.list({ limit: 1 });
375
+ record('API reachable, key accepted', 'pass');
376
+ }
377
+ catch (err) {
378
+ record(`API call failed: ${err instanceof Error ? err.message : 'unknown'}`, 'fail');
379
+ }
380
+ }
381
+ }
382
+ // ── Summary ──────────────────────────────────────────────────
383
+ const fails = checks.filter((c) => c.state === 'fail').length;
384
+ const warns = checks.filter((c) => c.state === 'warn').length;
385
+ if (json) {
386
+ console.log(JSON.stringify({ version: config_js_1.VERSION, failed: fails, warnings: warns, checks }, null, 2));
387
+ }
388
+ else if (fails === 0 && warns === 0) {
389
+ console.log('\n ✓ All checks passed.\n');
390
+ }
391
+ else {
392
+ console.log(`\n ${fails} failed, ${warns} warnings.${doPing ? '' : ' (run with --ping to test the API)'}\n`);
393
+ }
394
+ return fails === 0 ? 0 : 1;
395
+ };
396
+ exports.handleVerify = handleVerify;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * `hwip <agent> --wt <name>`: a git worktree made for a session, and taken back
3
+ * when the session is over and nothing in it would be lost.
4
+ *
5
+ * hwip only ever removes a worktree it made itself — the rows in
6
+ * `<stateDir>/worktrees.json` are the whole list. A worktree the user made by
7
+ * hand, or one `--wt` merely found at the path, has no row and no code path
8
+ * here that removes it. Every doubt about a worktree (a git call that fails or
9
+ * times out, a count that does not parse) reads as "keep it": the cost of a
10
+ * leftover directory is a line in `hwip worktree`, the cost of a wrong removal
11
+ * is somebody's work. `--force`, `rm -rf` and `branch -D` are not used anywhere.
12
+ */
13
+ /** A git call's outcome: its stdout, or the first line of what it said when it failed. */
14
+ export type GitResult = {
15
+ ok: true;
16
+ stdout: string;
17
+ } | {
18
+ ok: false;
19
+ error: string;
20
+ };
21
+ /** Runs `git <args>` (or tmux's); `timeoutMs` 0 means no timeout. Swappable in tests. */
22
+ export type GitRun = (args: string[], timeoutMs?: number) => Promise<GitResult>;
23
+ /** `execFile` of one command with argv (no shell: branch names and paths are user input), as a `GitRun`. */
24
+ export declare const commandRunner: (cmd: string) => GitRun;
25
+ export declare const gitRun: GitRun;
26
+ export declare const tmuxRun: GitRun;
27
+ /** One worktree hwip made. The lifecycle fields are written by `reconcileWorktrees`. */
28
+ export interface WorktreeRow {
29
+ /** realpath of the worktree directory. */
30
+ path: string;
31
+ /** The main checkout it hangs off — where `git worktree remove` runs from. */
32
+ root: string;
33
+ branch: string;
34
+ /** HEAD when the worktree was made: commits after it are this worktree's own work. */
35
+ baseSha: string;
36
+ /** hwip made the branch too, so hwip may delete it (`branch -d`) with the worktree. */
37
+ createdBranch: boolean;
38
+ createdAt: string;
39
+ /** Last time a pane was seen inside it. A worktree no session has used yet is not "over". */
40
+ sessionSeenAt?: string;
41
+ /** When it was judged not removable, and why — the automatic pass says so once. */
42
+ keptAt?: string;
43
+ reasons?: string[];
44
+ }
45
+ export declare const readWorktreeRows: () => WorktreeRow[];
46
+ /**
47
+ * Read, change, write. A row that cannot be written is a worktree that is not
48
+ * cleaned up automatically — the launch goes on. `expected` turns a write by
49
+ * the other process (wrapper vs listener) since the read into an error, which
50
+ * lands here too: a lost row only ever means "kept".
51
+ */
52
+ export declare const updateWorktreeRows: (change: (rows: WorktreeRow[]) => WorktreeRow[]) => void;
53
+ export type EnsureResult = {
54
+ ok: true;
55
+ path: string;
56
+ branch: string;
57
+ created: boolean;
58
+ } | {
59
+ ok: false;
60
+ error: string;
61
+ usage: boolean;
62
+ };
63
+ /**
64
+ * The directory name of the worktree for branch `name` of the repo at `root`: `<repo>-wt-<name with separators as dashes>`.
65
+ * Null when nothing but separators is left. `hwip schedule add` names its session from this, so the two cannot drift.
66
+ */
67
+ export declare const worktreeDirName: (root: string, name: string) => string | null;
68
+ /** Whether `name` is a branch name git accepts as it stands (`--branch` also expands `@{-1}`-style names; one that changes is not what was typed). */
69
+ export declare const isValidBranchName: (root: string, name: string, run?: GitRun) => Promise<boolean>;
70
+ /**
71
+ * The worktree for branch `name` next to the main checkout of the repo `dir` is
72
+ * in — made, or found already made by this same command. Anything else at the
73
+ * path is refused and left alone.
74
+ */
75
+ export declare const ensureWorktree: ({ dir, name }: {
76
+ dir: string;
77
+ name: string;
78
+ }, run?: GitRun) => Promise<EnsureResult>;
79
+ export type Inspection = {
80
+ removable: true;
81
+ } | {
82
+ removable: false;
83
+ reasons: string[];
84
+ };
85
+ /**
86
+ * Whether this worktree can go without losing anything. Asked in git's own
87
+ * terms rather than left to `git worktree remove`, which deletes a worktree
88
+ * holding only ignored files (a hand-written `.env`) and one whose commits
89
+ * were never pushed (measured, git 2.47.0). Both checks always run, so the
90
+ * reasons are the whole list; a check that cannot be answered is a reason too.
91
+ */
92
+ export declare const inspectWorktree: (row: WorktreeRow, run?: GitRun) => Promise<Inspection>;
93
+ export interface ReconcileInput {
94
+ /** `pane_current_path` of every live tmux pane. The caller must have read them all: an empty list means "none". */
95
+ paneCwds: string[];
96
+ /** `hwip worktree clean`: judge every worktree not in use, not only the ones a session has just left. */
97
+ manual?: boolean;
98
+ /** Judge and report, change nothing (disk and registry alike). */
99
+ dryRun?: boolean;
100
+ now?: Date;
101
+ }
102
+ export interface ReconcileReport {
103
+ /** Gone (or, under `dryRun`, would be). */
104
+ removed: Array<{
105
+ path: string;
106
+ branch: string;
107
+ branchDeleted: boolean;
108
+ }>;
109
+ kept: Array<{
110
+ path: string;
111
+ branch: string;
112
+ reasons: string[];
113
+ removeFailed: boolean;
114
+ }>;
115
+ inUse: string[];
116
+ /** Rows whose directory is no longer there — the user removed it. */
117
+ gone: string[];
118
+ }
119
+ /**
120
+ * Which of the worktrees hwip made to take back, and which to keep and say why.
121
+ *
122
+ * Automatic (`manual` off): a worktree is judged once, after a pane was last
123
+ * seen inside it and none is now. Judging again later would delete a directory
124
+ * somebody went back into and pushed from, so a worktree that was kept stays
125
+ * kept until `hwip worktree clean`, or until a session uses it again.
126
+ *
127
+ * Rows are re-read when written, so a row the wrapper added while git was
128
+ * being asked about is not lost.
129
+ */
130
+ export declare const reconcileWorktrees: (input: ReconcileInput, run?: GitRun) => Promise<ReconcileReport>;
131
+ export interface WorktreeStatus {
132
+ row: WorktreeRow;
133
+ /** `in use`, `never used` (clean, and no session ever started in it), `clean`, or what keeps it. */
134
+ state: string;
135
+ }
136
+ /** What `hwip worktree` shows: every row with its state now. Read-only. */
137
+ export declare const worktreeStatuses: (paneCwds: string[], run?: GitRun) => Promise<WorktreeStatus[]>;
138
+ /**
139
+ * The cwd of every live tmux pane, `[]` when there is no tmux server, null when
140
+ * tmux could not be asked. The difference matters: an unreadable list read as
141
+ * empty would make every worktree in use look abandoned.
142
+ */
143
+ export declare const readPaneCwds: (tmux?: GitRun) => Promise<string[] | null>;
144
+ //# sourceMappingURL=worktree.d.ts.map