dsh-zen-remote 2.3.11 → 2.4.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 (249) hide show
  1. package/CHANGELOG.md +45 -78
  2. package/README.md +94 -84
  3. package/README.zh-CN.md +90 -81
  4. package/bin/zen-remote.mjs +5 -0
  5. package/docs/interface.md +6 -6
  6. package/docs/remote-access.en.md +6 -4
  7. package/docs/remote-access.md +6 -4
  8. package/dsh-push.mjs +96 -10
  9. package/lib/adapters/dsh-better-sidebar/index.js +7 -3
  10. package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
  11. package/lib/admin-routes.js +10 -2
  12. package/lib/admin-routes.js.map +1 -1
  13. package/lib/cli/args.js +251 -0
  14. package/lib/cli/args.js.map +1 -0
  15. package/lib/cli/bootstrap.js +449 -0
  16. package/lib/cli/bootstrap.js.map +1 -0
  17. package/lib/cli/command-setup.js +484 -0
  18. package/lib/cli/command-setup.js.map +1 -0
  19. package/lib/cli/commands-client.js +359 -0
  20. package/lib/cli/commands-client.js.map +1 -0
  21. package/lib/cli/commands-host.js +1108 -0
  22. package/lib/cli/commands-host.js.map +1 -0
  23. package/lib/cli/commands-status.js +279 -0
  24. package/lib/cli/commands-status.js.map +1 -0
  25. package/lib/cli/config-writer.js +334 -0
  26. package/lib/cli/config-writer.js.map +1 -0
  27. package/lib/cli/context.js +10 -0
  28. package/lib/cli/context.js.map +1 -0
  29. package/lib/cli/i18n.js +620 -0
  30. package/lib/cli/i18n.js.map +1 -0
  31. package/lib/cli/ipc-client.js +160 -0
  32. package/lib/cli/ipc-client.js.map +1 -0
  33. package/lib/cli/ipc-server.js +464 -0
  34. package/lib/cli/ipc-server.js.map +1 -0
  35. package/lib/cli/main.js +245 -0
  36. package/lib/cli/main.js.map +1 -0
  37. package/lib/cli/qr-terminal.js +48 -0
  38. package/lib/cli/qr-terminal.js.map +1 -0
  39. package/lib/client-data/qr.js +286 -0
  40. package/lib/client-data/qr.js.map +1 -0
  41. package/lib/client-routes.js +308 -25
  42. package/lib/client-routes.js.map +1 -1
  43. package/lib/client.js +1013 -229
  44. package/lib/config.js +50 -12
  45. package/lib/config.js.map +1 -1
  46. package/lib/debug-report.js +28 -1
  47. package/lib/debug-report.js.map +1 -1
  48. package/lib/fetch-route-intercept.js +334 -26
  49. package/lib/fetch-route-intercept.js.map +1 -1
  50. package/lib/index.js +608 -375
  51. package/lib/index.js.map +1 -1
  52. package/lib/intercept.js +1833 -280
  53. package/lib/intercept.js.map +1 -1
  54. package/lib/lan-gate-server.cjs +36 -10
  55. package/lib/merge-streams.js +103 -11
  56. package/lib/merge-streams.js.map +1 -1
  57. package/lib/plugin-route-intercept.js +93 -63
  58. package/lib/plugin-route-intercept.js.map +1 -1
  59. package/lib/relay-access.js +8 -1
  60. package/lib/relay-access.js.map +1 -1
  61. package/lib/relay-client.js +133 -4
  62. package/lib/relay-client.js.map +1 -1
  63. package/lib/relay-pool.js +275 -0
  64. package/lib/relay-pool.js.map +1 -0
  65. package/lib/relay-server.js +269 -17
  66. package/lib/relay-server.js.map +1 -1
  67. package/lib/restart-fields.js +6 -4
  68. package/lib/restart-fields.js.map +1 -1
  69. package/lib/share-ops.js +68 -0
  70. package/lib/share-ops.js.map +1 -1
  71. package/lib/share-store.js +65 -27
  72. package/lib/share-store.js.map +1 -1
  73. package/lib/state-store.js +400 -0
  74. package/lib/state-store.js.map +1 -0
  75. package/lib/types/adapters/dsh-better-sidebar/index.d.ts +3 -2
  76. package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
  77. package/lib/types/admin-routes.d.ts.map +1 -1
  78. package/lib/types/cli/args.d.ts +78 -0
  79. package/lib/types/cli/args.d.ts.map +1 -0
  80. package/lib/types/cli/bootstrap.d.ts +170 -0
  81. package/lib/types/cli/bootstrap.d.ts.map +1 -0
  82. package/lib/types/cli/command-setup.d.ts +72 -0
  83. package/lib/types/cli/command-setup.d.ts.map +1 -0
  84. package/lib/types/cli/commands-client.d.ts +36 -0
  85. package/lib/types/cli/commands-client.d.ts.map +1 -0
  86. package/lib/types/cli/commands-host.d.ts +82 -0
  87. package/lib/types/cli/commands-host.d.ts.map +1 -0
  88. package/lib/types/cli/commands-status.d.ts +46 -0
  89. package/lib/types/cli/commands-status.d.ts.map +1 -0
  90. package/lib/types/cli/config-writer.d.ts +92 -0
  91. package/lib/types/cli/config-writer.d.ts.map +1 -0
  92. package/lib/types/cli/context.d.ts +39 -0
  93. package/lib/types/cli/context.d.ts.map +1 -0
  94. package/lib/types/cli/i18n.d.ts +32 -0
  95. package/lib/types/cli/i18n.d.ts.map +1 -0
  96. package/lib/types/cli/ipc-client.d.ts +83 -0
  97. package/lib/types/cli/ipc-client.d.ts.map +1 -0
  98. package/lib/types/cli/ipc-server.d.ts +113 -0
  99. package/lib/types/cli/ipc-server.d.ts.map +1 -0
  100. package/lib/types/cli/main.d.ts +57 -0
  101. package/lib/types/cli/main.d.ts.map +1 -0
  102. package/lib/types/cli/qr-terminal.d.ts +17 -0
  103. package/lib/types/cli/qr-terminal.d.ts.map +1 -0
  104. package/lib/types/client/RemoteStatusIcon.d.ts.map +1 -1
  105. package/lib/types/client/current-session-report.d.ts +4 -0
  106. package/lib/types/client/current-session-report.d.ts.map +1 -1
  107. package/lib/types/client/effects/model-group-side.d.ts.map +1 -1
  108. package/lib/types/client/effects/remote-session.d.ts.map +1 -1
  109. package/lib/types/client/locales.d.ts +47 -3
  110. package/lib/types/client/locales.d.ts.map +1 -1
  111. package/lib/types/client/settings/SettingsSection.d.ts.map +1 -1
  112. package/lib/types/client/settings/settings-css.d.ts +1 -1
  113. package/lib/types/client/settings/settings-css.d.ts.map +1 -1
  114. package/lib/types/client/styles/remote-session.css.d.ts +1 -1
  115. package/lib/types/client/styles/remote-session.css.d.ts.map +1 -1
  116. package/lib/types/client/wizard/GatewayWizard.d.ts.map +1 -1
  117. package/lib/types/client/wizard/cards/ProxyCard.d.ts.map +1 -1
  118. package/lib/types/client/wizard/wizard-css.d.ts +1 -1
  119. package/lib/types/client/wizard/wizard-css.d.ts.map +1 -1
  120. package/lib/types/client-data/model-group-side.d.ts +23 -2
  121. package/lib/types/client-data/model-group-side.d.ts.map +1 -1
  122. package/lib/types/client-data/remote-status.d.ts +47 -1
  123. package/lib/types/client-data/remote-status.d.ts.map +1 -1
  124. package/lib/types/client-data/settings-form.d.ts +85 -1
  125. package/lib/types/client-data/settings-form.d.ts.map +1 -1
  126. package/lib/types/client-data/wizard.d.ts +56 -1
  127. package/lib/types/client-data/wizard.d.ts.map +1 -1
  128. package/lib/types/client-routes.d.ts +25 -1
  129. package/lib/types/client-routes.d.ts.map +1 -1
  130. package/lib/types/config.d.ts +40 -14
  131. package/lib/types/config.d.ts.map +1 -1
  132. package/lib/types/debug-report.d.ts.map +1 -1
  133. package/lib/types/fetch-route-intercept.d.ts +66 -8
  134. package/lib/types/fetch-route-intercept.d.ts.map +1 -1
  135. package/lib/types/index.d.ts +9 -2
  136. package/lib/types/index.d.ts.map +1 -1
  137. package/lib/types/intercept.d.ts +72 -10
  138. package/lib/types/intercept.d.ts.map +1 -1
  139. package/lib/types/merge-streams.d.ts +79 -1
  140. package/lib/types/merge-streams.d.ts.map +1 -1
  141. package/lib/types/plugin-route-intercept.d.ts +25 -9
  142. package/lib/types/plugin-route-intercept.d.ts.map +1 -1
  143. package/lib/types/relay-access.d.ts +3 -1
  144. package/lib/types/relay-access.d.ts.map +1 -1
  145. package/lib/types/relay-client.d.ts +23 -2
  146. package/lib/types/relay-client.d.ts.map +1 -1
  147. package/lib/types/relay-pool.d.ts +148 -0
  148. package/lib/types/relay-pool.d.ts.map +1 -0
  149. package/lib/types/relay-server.d.ts +30 -3
  150. package/lib/types/relay-server.d.ts.map +1 -1
  151. package/lib/types/restart-fields.d.ts +5 -5
  152. package/lib/types/restart-fields.d.ts.map +1 -1
  153. package/lib/types/share-ops.d.ts +47 -1
  154. package/lib/types/share-ops.d.ts.map +1 -1
  155. package/lib/types/share-store.d.ts.map +1 -1
  156. package/lib/types/state-store.d.ts +179 -0
  157. package/lib/types/state-store.d.ts.map +1 -0
  158. package/lib/types/upload-limits.d.ts +15 -0
  159. package/lib/types/upload-limits.d.ts.map +1 -1
  160. package/lib/types/virtual-id.d.ts +21 -0
  161. package/lib/types/virtual-id.d.ts.map +1 -1
  162. package/lib/types/wizard/probe.d.ts.map +1 -1
  163. package/lib/types/wizard/proxy-snippets.d.ts +3 -0
  164. package/lib/types/wizard/proxy-snippets.d.ts.map +1 -1
  165. package/lib/types/wizard/routes.d.ts +1 -0
  166. package/lib/types/wizard/routes.d.ts.map +1 -1
  167. package/lib/types/wizard/status.d.ts +9 -0
  168. package/lib/types/wizard/status.d.ts.map +1 -1
  169. package/lib/types/wizard/verify.d.ts +3 -0
  170. package/lib/types/wizard/verify.d.ts.map +1 -1
  171. package/lib/upload-limits.js +15 -0
  172. package/lib/upload-limits.js.map +1 -1
  173. package/lib/virtual-id.js +60 -0
  174. package/lib/virtual-id.js.map +1 -1
  175. package/lib/wizard/probe.js +46 -1
  176. package/lib/wizard/probe.js.map +1 -1
  177. package/lib/wizard/proxy-snippets.js +14 -1
  178. package/lib/wizard/proxy-snippets.js.map +1 -1
  179. package/lib/wizard/routes.js +23 -4
  180. package/lib/wizard/routes.js.map +1 -1
  181. package/lib/wizard/status.js +21 -0
  182. package/lib/wizard/status.js.map +1 -1
  183. package/lib/wizard/verify.js +31 -8
  184. package/lib/wizard/verify.js.map +1 -1
  185. package/package.json +8 -4
  186. package/pwa/icons/badge-96.png +0 -0
  187. package/pwa/icons/badge.svg +1 -0
  188. package/pwa/manifest.json +2 -1
  189. package/pwa/sw.js +3 -1
  190. package/scripts/check-debug-report.mjs +41 -1
  191. package/scripts/check-settings-form.mjs +14 -3
  192. package/scripts/check-settings-view.mjs +247 -0
  193. package/scripts/check-wizard-ui.mjs +37 -0
  194. package/src/adapters/dsh-better-sidebar/index.ts +7 -3
  195. package/src/admin-routes.ts +10 -2
  196. package/src/cli/args.ts +289 -0
  197. package/src/cli/bootstrap.ts +569 -0
  198. package/src/cli/command-setup.ts +540 -0
  199. package/src/cli/commands-client.ts +369 -0
  200. package/src/cli/commands-host.ts +1127 -0
  201. package/src/cli/commands-status.ts +296 -0
  202. package/src/cli/config-writer.ts +354 -0
  203. package/src/cli/context.ts +38 -0
  204. package/src/cli/i18n.ts +645 -0
  205. package/src/cli/ipc-client.ts +216 -0
  206. package/src/cli/ipc-server.ts +531 -0
  207. package/src/cli/main.ts +285 -0
  208. package/src/cli/qr-terminal.ts +49 -0
  209. package/src/client/RemoteStatusIcon.tsx +4 -1
  210. package/src/client/current-session-report.ts +28 -4
  211. package/src/client/effects/model-group-side.ts +19 -5
  212. package/src/client/effects/remote-session.ts +26 -6
  213. package/src/client/locales.ts +96 -6
  214. package/src/client/settings/SettingsSection.tsx +287 -92
  215. package/src/client/settings/settings-css.ts +18 -0
  216. package/src/client/styles/remote-session.css.ts +15 -1
  217. package/src/client/wizard/GatewayWizard.tsx +470 -109
  218. package/src/client/wizard/cards/ProxyCard.tsx +2 -1
  219. package/src/client/wizard/wizard-css.ts +23 -0
  220. package/src/client-data/model-group-side.ts +37 -10
  221. package/src/client-data/remote-status.ts +112 -17
  222. package/src/client-data/settings-form.ts +164 -8
  223. package/src/client-data/wizard.ts +112 -2
  224. package/src/client-routes.ts +334 -26
  225. package/src/config.ts +62 -16
  226. package/src/debug-report.ts +27 -1
  227. package/src/fetch-route-intercept.ts +381 -32
  228. package/src/index.ts +608 -380
  229. package/src/intercept.ts +1866 -286
  230. package/src/merge-streams.ts +155 -10
  231. package/src/plugin-route-intercept.ts +120 -71
  232. package/src/relay-access.ts +14 -3
  233. package/src/relay-client.ts +163 -5
  234. package/src/relay-pool.ts +387 -0
  235. package/src/relay-server.ts +312 -16
  236. package/src/restart-fields.ts +6 -4
  237. package/src/share-ops.ts +82 -1
  238. package/src/share-store.ts +59 -26
  239. package/src/state-store.ts +428 -0
  240. package/src/upload-limits.ts +16 -0
  241. package/src/virtual-id.ts +57 -0
  242. package/src/wizard/probe.ts +54 -2
  243. package/src/wizard/proxy-snippets.ts +17 -2
  244. package/src/wizard/routes.ts +24 -5
  245. package/src/wizard/status.ts +28 -0
  246. package/src/wizard/verify.ts +33 -11
  247. package/AGENTS.md +0 -185
  248. package/docs/adr/0001-plugin-bridge-forwards-plugin-routes.md +0 -15
  249. package/docs/adr/0002-relay-emulates-websocket.md +0 -8
@@ -0,0 +1,216 @@
1
+ /**
2
+ * CLI-2 IPC client: the CLI half of the local control channel that
3
+ * `src/cli/ipc-server.ts` serves inside the DSH plugin process. Discovery
4
+ * walks the same candidate socket paths the server binds (or a single
5
+ * explicit `--socket` path), pings each existing one with a short 200ms
6
+ * budget, and sorts the answers into three buckets the command layer can
7
+ * speak about: LIVE instances whose `cliProtocolVersion` matches, sockets
8
+ * speaking a DIFFERENT protocol version (the upgrade-prompt case), and paths
9
+ * that refused us with EACCES / EPERM (the "DSH runs as another user, try
10
+ * `sudo -E`" case).
11
+ *
12
+ * The request helper is a thin http-over-Unix-socket (Windows: named pipe)
13
+ * round trip. Transport errors REJECT — a dead socket is a real failure the
14
+ * caller must see — while HTTP error statuses resolve normally: the IPC
15
+ * protocol answers every failure as JSON (`{ ok: false, error: … }`), so a
16
+ * 409 wrong-role or 409 env-shadowed is data, not an exception.
17
+ */
18
+
19
+ import { request as httpRequest } from 'node:http'
20
+ import type { IncomingHttpHeaders } from 'node:http'
21
+ import { statSync } from 'node:fs'
22
+ import { CLI_CONFIG_ROUTE, CLI_PING_ROUTE, CLI_PROTOCOL_VERSION, candidateSocketPaths } from './ipc-server.js'
23
+ import { CLIENT_DEBUG_REPORT_ROUTE, CLIENT_STATUS_ROUTE } from '../client-routes.js'
24
+
25
+ /** What a live plugin instance answers `GET /ping` with. Mirrors the body
26
+ * `src/cli/ipc-server.ts` builds (minus `ok`). */
27
+ export interface CliPingResponse {
28
+ ok: true
29
+ cliProtocolVersion: number
30
+ pluginVersion: string
31
+ pid: number
32
+ role: 'host' | 'client' | 'peer'
33
+ serves: boolean
34
+ connects: boolean
35
+ serverName: string
36
+ profileName?: string
37
+ }
38
+
39
+ export type DiscoveredLive = CliPingResponse & { socketPath: string }
40
+
41
+ export interface DiscoveredSockets {
42
+ /** Version-matching live instances, one entry per answering socket. */
43
+ live: DiscoveredLive[]
44
+ /** Sockets that answered but speak another `cliProtocolVersion`. */
45
+ versionMismatch: { socketPath: string; cliProtocolVersion: number | undefined }[]
46
+ /** Paths that refused the probe (or even the stat) with EACCES / EPERM —
47
+ * the cross-user DSH case. */
48
+ permissionDenied: string[]
49
+ }
50
+
51
+ export interface DiscoverCliSocketsOptions {
52
+ dshHome: string
53
+ /** Probe ONLY this path instead of the candidate walk. */
54
+ explicitSocket?: string
55
+ /** Bind-shape override (tests pin both platforms). */
56
+ platform?: NodeJS.Platform
57
+ }
58
+
59
+ export interface CliIpcRequestResult {
60
+ status: number
61
+ body: unknown
62
+ headers: IncomingHttpHeaders
63
+ }
64
+
65
+ export interface CliIpcClient {
66
+ readonly socketPath: string
67
+ ping(): Promise<CliPingResponse>
68
+ getStatus(): Promise<unknown>
69
+ getDebugReport(): Promise<unknown>
70
+ writeConfig(input: { set?: Record<string, unknown>; unset?: readonly string[] }): Promise<CliIpcRequestResult>
71
+ request(method: string, path: string, body?: unknown, timeoutMs?: number): Promise<CliIpcRequestResult>
72
+ }
73
+
74
+ /** Discovery ping budget: finding out whether anything answers must stay
75
+ * fast — up to 8 candidates × 200ms is the worst case, and every absent
76
+ * path skips the probe entirely. */
77
+ const PING_TIMEOUT_MS = 200
78
+ const DEFAULT_REQUEST_TIMEOUT_MS = 15_000
79
+ const DEBUG_REPORT_TIMEOUT_MS = 30_000
80
+
81
+ function errnoCode(error: unknown): string | undefined {
82
+ if (!(error instanceof Error)) return undefined
83
+ const code = (error as { code?: unknown }).code
84
+ return typeof code === 'string' ? code : undefined
85
+ }
86
+
87
+ function isDenied(error: unknown): boolean {
88
+ const code = errnoCode(error)
89
+ return code === 'EACCES' || code === 'EPERM'
90
+ }
91
+
92
+ interface IpcAnswer {
93
+ status: number
94
+ headers: IncomingHttpHeaders
95
+ text: string
96
+ }
97
+
98
+ /** One http round trip over the socket. Rejects on transport errors; HTTP
99
+ * error statuses are answers, not failures. */
100
+ function ipcRequest(socketPath: string, method: string, route: string, body?: unknown, timeoutMs?: number): Promise<IpcAnswer> {
101
+ return new Promise((resolve, reject) => {
102
+ const payload = body === undefined ? undefined : JSON.stringify(body)
103
+ const headers: Record<string, string> = {}
104
+ if (payload !== undefined) {
105
+ headers['content-type'] = 'application/json'
106
+ headers['content-length'] = String(Buffer.byteLength(payload))
107
+ }
108
+ const req = httpRequest({ socketPath, method, path: route, headers, agent: false, timeout: timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS }, (res) => {
109
+ const chunks: Buffer[] = []
110
+ res.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array)))
111
+ res.on('end', () => resolve({ status: res.statusCode ?? 0, headers: res.headers, text: Buffer.concat(chunks).toString('utf8') }))
112
+ res.on('error', reject)
113
+ })
114
+ req.on('timeout', () => {
115
+ req.destroy()
116
+ reject(new Error(`IPC request timed out after ${timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS}ms: ${method} ${route}`))
117
+ })
118
+ req.on('error', reject)
119
+ if (payload !== undefined) req.write(payload)
120
+ req.end()
121
+ })
122
+ }
123
+
124
+ type ProbeVerdict =
125
+ | { kind: 'live'; ping: CliPingResponse }
126
+ | { kind: 'mismatch'; reportedVersion: number | undefined }
127
+ | { kind: 'denied' }
128
+ | { kind: 'absent' }
129
+ | { kind: 'dead' }
130
+
131
+ async function probeCandidate(socketPath: string, probeEvenIfAbsent: boolean): Promise<ProbeVerdict> {
132
+ let statDenied = false
133
+ try {
134
+ statSync(socketPath)
135
+ } catch (error) {
136
+ if (isDenied(error)) statDenied = true
137
+ else if (!probeEvenIfAbsent) return { kind: 'absent' }
138
+ // A missing explicit path is still probed below — the connect error tells
139
+ // the same story, and ENOENT is not a permission problem.
140
+ }
141
+ if (statDenied) return { kind: 'denied' }
142
+
143
+ let answer: IpcAnswer
144
+ try {
145
+ answer = await ipcRequest(socketPath, 'GET', CLI_PING_ROUTE, undefined, PING_TIMEOUT_MS)
146
+ } catch (error) {
147
+ return isDenied(error) ? { kind: 'denied' } : { kind: 'dead' }
148
+ }
149
+ if (answer.status !== 200) return { kind: 'dead' }
150
+ let parsed: unknown
151
+ try {
152
+ parsed = JSON.parse(answer.text)
153
+ } catch {
154
+ return { kind: 'dead' }
155
+ }
156
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) return { kind: 'dead' }
157
+ const body = parsed as { ok?: unknown; cliProtocolVersion?: unknown }
158
+ if (body.ok !== true) return { kind: 'dead' }
159
+ const reported = typeof body.cliProtocolVersion === 'number' ? body.cliProtocolVersion : undefined
160
+ if (reported !== CLI_PROTOCOL_VERSION) return { kind: 'mismatch', reportedVersion: reported }
161
+ return { kind: 'live', ping: parsed as CliPingResponse }
162
+ }
163
+
164
+ /**
165
+ * Walk the CLI socket candidates under `dshHome` (or only `explicitSocket`)
166
+ * and classify every answering socket. Absent candidates are skipped without
167
+ * a probe; a dead file that merely squats a candidate name simply answers
168
+ * nothing and lands nowhere.
169
+ */
170
+ export async function discoverCliSockets(options: DiscoverCliSocketsOptions): Promise<DiscoveredSockets> {
171
+ const paths = options.explicitSocket !== undefined
172
+ ? [options.explicitSocket]
173
+ : candidateSocketPaths(options.dshHome, options.platform ?? process.platform)
174
+ const found: DiscoveredSockets = { live: [], versionMismatch: [], permissionDenied: [] }
175
+ for (const socketPath of paths) {
176
+ const verdict = await probeCandidate(socketPath, options.explicitSocket !== undefined)
177
+ if (verdict.kind === 'live') found.live.push({ ...verdict.ping, socketPath })
178
+ else if (verdict.kind === 'mismatch') found.versionMismatch.push({ socketPath, cliProtocolVersion: verdict.reportedVersion })
179
+ else if (verdict.kind === 'denied') found.permissionDenied.push(socketPath)
180
+ }
181
+ return found
182
+ }
183
+
184
+ function parseBody(text: string): unknown {
185
+ try {
186
+ return JSON.parse(text)
187
+ } catch {
188
+ return text
189
+ }
190
+ }
191
+
192
+ /**
193
+ * A client bound to one socket path. `request` is the generic escape hatch
194
+ * for routes added in later CLI phases; the named methods cover the routes
195
+ * this phase already drives.
196
+ */
197
+ export function createCliIpcClient(socketPath: string): CliIpcClient {
198
+ const request = async (method: string, path: string, body?: unknown, timeoutMs?: number): Promise<CliIpcRequestResult> => {
199
+ const answer = await ipcRequest(socketPath, method, path, body, timeoutMs)
200
+ return { status: answer.status, body: parseBody(answer.text), headers: answer.headers }
201
+ }
202
+ return {
203
+ socketPath,
204
+ ping: async () => {
205
+ const result = await request('GET', CLI_PING_ROUTE, undefined, PING_TIMEOUT_MS)
206
+ return result.body as CliPingResponse
207
+ },
208
+ getStatus: async () => (await request('GET', CLIENT_STATUS_ROUTE)).body,
209
+ getDebugReport: async () => (await request('GET', CLIENT_DEBUG_REPORT_ROUTE, undefined, DEBUG_REPORT_TIMEOUT_MS)).body,
210
+ writeConfig: async (input) => await request('POST', CLI_CONFIG_ROUTE, {
211
+ ...(input.set === undefined ? {} : { set: input.set }),
212
+ ...(input.unset === undefined ? {} : { unset: input.unset }),
213
+ }),
214
+ request,
215
+ }
216
+ }