@chatcode/cco-market 1.45.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 (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/README.zh.md +143 -0
  4. package/UPDATE-API-V1.md +132 -0
  5. package/client/client.js +11534 -0
  6. package/cordis.patch.yml +5 -0
  7. package/lib/accelerate.js +184 -0
  8. package/lib/agents.js +36 -0
  9. package/lib/backup.js +572 -0
  10. package/lib/catalog-local-match.js +126 -0
  11. package/lib/catalog-npm.js +111 -0
  12. package/lib/changelog.js +242 -0
  13. package/lib/channels.js +64 -0
  14. package/lib/check.js +1067 -0
  15. package/lib/compatibility.js +198 -0
  16. package/lib/diagnostics.js +49 -0
  17. package/lib/discovery-compatibility.js +260 -0
  18. package/lib/dsh-cli.js +949 -0
  19. package/lib/dsh-install.js +108 -0
  20. package/lib/gist.js +352 -0
  21. package/lib/groups.js +97 -0
  22. package/lib/home-paths.js +42 -0
  23. package/lib/hot.js +519 -0
  24. package/lib/http.js +39 -0
  25. package/lib/index.js +97 -0
  26. package/lib/install.js +387 -0
  27. package/lib/log.js +197 -0
  28. package/lib/ndjson.js +154 -0
  29. package/lib/net.js +98 -0
  30. package/lib/order.js +284 -0
  31. package/lib/patch.js +521 -0
  32. package/lib/pnpm-compat.js +490 -0
  33. package/lib/presets.js +278 -0
  34. package/lib/profile.js +911 -0
  35. package/lib/region-probe.js +93 -0
  36. package/lib/regions.js +253 -0
  37. package/lib/registry.js +204 -0
  38. package/lib/restart.js +373 -0
  39. package/lib/routes.js +4372 -0
  40. package/lib/settings.js +109 -0
  41. package/lib/skill-market/api.js +119 -0
  42. package/lib/skill-market/config.js +29 -0
  43. package/lib/skill-market/installer.js +352 -0
  44. package/lib/skill-market/runtime.js +13 -0
  45. package/lib/skill-market/tui.js +119 -0
  46. package/lib/skill-market/types.js +1 -0
  47. package/lib/skill-market/web-routes.js +123 -0
  48. package/lib/snapshot.js +500 -0
  49. package/lib/source-migration.js +54 -0
  50. package/lib/sources.js +596 -0
  51. package/lib/store.js +91 -0
  52. package/lib/themes.js +102 -0
  53. package/lib/trial.js +116 -0
  54. package/lib/types/accelerate.d.ts +85 -0
  55. package/lib/types/agents.d.ts +22 -0
  56. package/lib/types/backup.d.ts +131 -0
  57. package/lib/types/catalog-local-match.d.ts +50 -0
  58. package/lib/types/catalog-npm.d.ts +47 -0
  59. package/lib/types/changelog.d.ts +102 -0
  60. package/lib/types/channels.d.ts +56 -0
  61. package/lib/types/check.d.ts +266 -0
  62. package/lib/types/compatibility.d.ts +64 -0
  63. package/lib/types/diagnostics.d.ts +29 -0
  64. package/lib/types/discovery-compatibility.d.ts +80 -0
  65. package/lib/types/dsh-cli.d.ts +324 -0
  66. package/lib/types/dsh-install.d.ts +46 -0
  67. package/lib/types/gist.d.ts +53 -0
  68. package/lib/types/groups.d.ts +34 -0
  69. package/lib/types/home-paths.d.ts +16 -0
  70. package/lib/types/hot.d.ts +221 -0
  71. package/lib/types/http.d.ts +12 -0
  72. package/lib/types/index.d.ts +14 -0
  73. package/lib/types/install.d.ts +172 -0
  74. package/lib/types/log.d.ts +43 -0
  75. package/lib/types/ndjson.d.ts +52 -0
  76. package/lib/types/net.d.ts +58 -0
  77. package/lib/types/order.d.ts +100 -0
  78. package/lib/types/patch.d.ts +129 -0
  79. package/lib/types/pnpm-compat.d.ts +93 -0
  80. package/lib/types/presets.d.ts +90 -0
  81. package/lib/types/profile.d.ts +253 -0
  82. package/lib/types/region-probe.d.ts +50 -0
  83. package/lib/types/regions.d.ts +122 -0
  84. package/lib/types/registry.d.ts +86 -0
  85. package/lib/types/restart.d.ts +194 -0
  86. package/lib/types/routes.d.ts +67 -0
  87. package/lib/types/settings.d.ts +78 -0
  88. package/lib/types/skill-market/api.d.ts +10 -0
  89. package/lib/types/skill-market/config.d.ts +3 -0
  90. package/lib/types/skill-market/installer.d.ts +23 -0
  91. package/lib/types/skill-market/runtime.d.ts +10 -0
  92. package/lib/types/skill-market/tui.d.ts +4 -0
  93. package/lib/types/skill-market/types.d.ts +71 -0
  94. package/lib/types/skill-market/web-routes.d.ts +19 -0
  95. package/lib/types/snapshot.d.ts +93 -0
  96. package/lib/types/source-migration.d.ts +11 -0
  97. package/lib/types/sources.d.ts +216 -0
  98. package/lib/types/store.d.ts +30 -0
  99. package/lib/types/themes.d.ts +40 -0
  100. package/lib/types/trial.d.ts +61 -0
  101. package/lib/types/update-api-v1.d.ts +66 -0
  102. package/lib/types/updates.d.ts +123 -0
  103. package/lib/types/verify.d.ts +139 -0
  104. package/lib/update-api-v1.js +215 -0
  105. package/lib/updates.js +361 -0
  106. package/lib/verify.js +453 -0
  107. package/package.json +121 -0
  108. package/src/accelerate.ts +213 -0
  109. package/src/agents.ts +43 -0
  110. package/src/backup.ts +583 -0
  111. package/src/catalog-local-match.ts +144 -0
  112. package/src/catalog-npm.ts +120 -0
  113. package/src/changelog.ts +282 -0
  114. package/src/channels.ts +70 -0
  115. package/src/check.ts +1239 -0
  116. package/src/client/CommentsModal.tsx +119 -0
  117. package/src/client/Diagnostics.tsx +907 -0
  118. package/src/client/ErrorBoundary.tsx +111 -0
  119. package/src/client/InstallToast.tsx +31 -0
  120. package/src/client/Market.module.css +830 -0
  121. package/src/client/MarketSection.tsx +5301 -0
  122. package/src/client/OperationsPanel.tsx +365 -0
  123. package/src/client/SettingsCard.tsx +621 -0
  124. package/src/client/SkillMarket.module.css +21 -0
  125. package/src/client/SkillMarketSection.tsx +162 -0
  126. package/src/client/comments.ts +54 -0
  127. package/src/client/globals.d.ts +13 -0
  128. package/src/client/index.ts +180 -0
  129. package/src/client/locales.ts +1112 -0
  130. package/src/client/market-data.ts +1321 -0
  131. package/src/client/operations.ts +201 -0
  132. package/src/client/preset-panel.tsx +263 -0
  133. package/src/client/primitives.d.ts +140 -0
  134. package/src/client/self-check.ts +147 -0
  135. package/src/client/snapshot-panel.tsx +244 -0
  136. package/src/compatibility.ts +237 -0
  137. package/src/diagnostics.ts +84 -0
  138. package/src/discovery-compatibility.ts +315 -0
  139. package/src/dsh-cli.ts +1126 -0
  140. package/src/dsh-install.ts +118 -0
  141. package/src/gist.ts +362 -0
  142. package/src/groups.ts +111 -0
  143. package/src/home-paths.ts +53 -0
  144. package/src/hot.ts +628 -0
  145. package/src/http.ts +41 -0
  146. package/src/index.ts +128 -0
  147. package/src/install.ts +420 -0
  148. package/src/log.ts +206 -0
  149. package/src/ndjson.ts +185 -0
  150. package/src/net.ts +106 -0
  151. package/src/order.ts +303 -0
  152. package/src/patch.ts +522 -0
  153. package/src/pnpm-compat.ts +527 -0
  154. package/src/presets.ts +344 -0
  155. package/src/profile.ts +940 -0
  156. package/src/region-probe.ts +97 -0
  157. package/src/regions.ts +310 -0
  158. package/src/registry.ts +250 -0
  159. package/src/restart.ts +396 -0
  160. package/src/routes.ts +4506 -0
  161. package/src/settings.ts +141 -0
  162. package/src/skill-market/api.ts +130 -0
  163. package/src/skill-market/config.ts +32 -0
  164. package/src/skill-market/installer.ts +337 -0
  165. package/src/skill-market/runtime.ts +14 -0
  166. package/src/skill-market/tui.ts +157 -0
  167. package/src/skill-market/types.ts +80 -0
  168. package/src/skill-market/web-routes.ts +132 -0
  169. package/src/snapshot.ts +532 -0
  170. package/src/source-migration.ts +61 -0
  171. package/src/sources.ts +565 -0
  172. package/src/store.ts +89 -0
  173. package/src/themes.ts +125 -0
  174. package/src/trial.ts +156 -0
  175. package/src/update-api-v1.ts +277 -0
  176. package/src/updates.ts +400 -0
  177. package/src/verify.ts +492 -0
package/lib/restart.js ADDED
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Self-restart: relaunch the exact ChatCode CLI invocation that booted this host so
3
+ * pending (non-hot) plugin changes take effect without the user leaving the
4
+ * UI. Contributed in #14 by @ysyyhhh; ported onto the layered architecture.
5
+ *
6
+ * Safety model: the endpoint accepts only direct same-origin loopback
7
+ * requests (no forwarding headers), refuses while a plugin operation runs,
8
+ * and deployments under a supervisor (systemd/launchd/pm2) can disable the
9
+ * whole feature with `allowRestart: false` — the supervisor owns restarts.
10
+ */
11
+ import { spawn } from 'node:child_process';
12
+ import { readFileSync } from 'node:fs';
13
+ import inspector from 'node:inspector';
14
+ import { tmpdir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ import { dshArgv, nodeExecutable } from './dsh-cli.js';
17
+ /** Vitest / flows can pin detection without opening a real inspector port. */
18
+ let debuggerOverride;
19
+ /** @internal test hook — production callers use detectedDebugger() only. */
20
+ export function setDetectedDebuggerOverride(value) {
21
+ debuggerOverride = value;
22
+ }
23
+ const INSPECT_ARG_PREFIXES = ['--inspect', '--inspect-brk', '--inspect-port', '--inspect-wait'];
24
+ function tokenHasInspectFlag(token) {
25
+ for (const prefix of INSPECT_ARG_PREFIXES) {
26
+ if (token === prefix || token.startsWith(`${prefix}=`))
27
+ return true;
28
+ }
29
+ if (token === '--debug-brk' || token.startsWith('--debug-brk='))
30
+ return true;
31
+ if (token === '--debug' || token.startsWith('--debug='))
32
+ return true;
33
+ return false;
34
+ }
35
+ function argvHasInspectFlag(tokens) {
36
+ for (const token of tokens) {
37
+ if (tokenHasInspectFlag(token))
38
+ return true;
39
+ }
40
+ return false;
41
+ }
42
+ /**
43
+ * The process supervisor running this host, when one can be identified —
44
+ * `null` when nothing says so.
45
+ *
46
+ * This exists because the failure it prevents is the worst one the market
47
+ * can cause. Under systemd's default `KillMode=control-group`, everything in
48
+ * the unit's cgroup dies with the main process — including the detached
49
+ * helper that was supposed to bring the replacement up. So "restart" killed
50
+ * a production service and nothing came back (#229 by @SkillBase-Al: "杀死了
51
+ * 服务但是无法重复启动服务"). `allowRestart: false` was always the documented
52
+ * answer, but it is opt-in, and nothing told the operator to opt in until
53
+ * after they had already lost the service.
54
+ *
55
+ * TWO signals are required, and the second is the whole reason this function
56
+ * is not a one-line env check. `INVOCATION_ID` is INHERITED: every
57
+ * descendant of a systemd unit carries it, which on Linux includes an
58
+ * ordinary desktop terminal (its shell descends from a user-session unit)
59
+ * and a CI runner (the agent is a unit — this repo's own smoke test caught
60
+ * that). Treating inheritance as ownership would disable the button for a
61
+ * large population of hosts where it works fine, which is a worse bug than
62
+ * the one being fixed.
63
+ *
64
+ * The parent test is what distinguishes being the unit's own main process
65
+ * from merely descending from one: a terminal's node has the shell as its
66
+ * parent and a runner's has the agent. It used to be `ppid === 1`, which is
67
+ * only true for SYSTEM units. A per-user unit's service is forked by that
68
+ * user's manager instance — `systemd --user`, an ordinary PID — so user-unit
69
+ * hosts read as unsupervised, and one-click restart killed them for good
70
+ * (#471 by @automagik-genie, with the journal to prove the whole chain:
71
+ * clean SIGTERM exit → `Restart=on-failure` does not fire → `KillMode=mixed`
72
+ * SIGKILLs the detached helper before it can spawn the replacement). So the
73
+ * parent counts as a manager when it is PID 1 or its comm is `systemd` —
74
+ * the one name both manager instances share and the false-positive parents
75
+ * (shells, CI agents) never carry. `/proc` is Linux-only, which is exactly
76
+ * as wide as systemd itself; anywhere it cannot be read the answer stays
77
+ * "not a manager".
78
+ *
79
+ * Scoped to systemd on purpose. pm2 sets `pm_id`, but it is inherited the
80
+ * same way and pm2's God daemon — not PID 1 — is the parent, so there is no
81
+ * equivalent second signal; a guess there would reintroduce exactly the
82
+ * false positive this pair exists to avoid. launchd has no marker at all.
83
+ * Both still need the explicit setting: detection is a safety net over the
84
+ * documented option, never a replacement for it.
85
+ */
86
+ export function detectedSupervisor(env = process.env, ppid = process.ppid, parentComm = readParentComm) {
87
+ const set = (name) => (env[name] ?? '') !== '';
88
+ if (!set('INVOCATION_ID') && !set('JOURNAL_STREAM'))
89
+ return null;
90
+ if (ppid === 1 || parentComm(ppid) === 'systemd')
91
+ return 'systemd';
92
+ return null;
93
+ }
94
+ /** The comm of `pid` from /proc, or null wherever that cannot be read. */
95
+ function readParentComm(pid) {
96
+ try {
97
+ return readFileSync(`/proc/${String(pid)}/comm`, 'utf8').trim();
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
103
+ /**
104
+ * Whether this host process is under a debugger, when one can be identified —
105
+ * `'inspector'` or `null`.
106
+ *
107
+ * Parallel to `detectedSupervisor()` (#229): a runtime latch for the restart
108
+ * route and status poll, NOT an entry in `restartAllowed()`. Folding it into
109
+ * `restartAllowed()` would write `allowRestart: false` through the settings
110
+ * page while a debug session is open, and the switch would stay off after
111
+ * the debugger detaches (#447).
112
+ *
113
+ * An explicit `allowRestart: true` does NOT override this latch either —
114
+ * unlike systemd, there is no documented deployment shape where killing a
115
+ * debug-attached host from the market UI is the right answer.
116
+ *
117
+ * Primary signal: `inspector.url()` is set (covers `--inspect` at boot,
118
+ * `inspector.open()`, and SIGUSR1 attach). Secondary: inspect-family flags
119
+ * in `execArgv` and `NODE_OPTIONS`, matched by token prefix — not a
120
+ * `/inspect/` substring, so script paths like `.../inspect-tool.js` do not
121
+ * false-positive. Includes `--inspect-wait` for Node versions that expose it
122
+ * as a distinct flag before `inspector.url()` is populated.
123
+ *
124
+ * Accepted false positive (same trade as #229 not guessing pm2): a host left
125
+ * listening on an inspector port — including `NODE_OPTIONS=--inspect` with
126
+ * nobody attached — is treated as debug-owned and gets no one-click restart.
127
+ */
128
+ export function detectedDebugger(inspectorUrl = inspector.url(), execArgv = process.execArgv, nodeOptions = process.env.NODE_OPTIONS ?? '') {
129
+ if (debuggerOverride !== undefined)
130
+ return debuggerOverride;
131
+ if (inspectorUrl !== undefined && inspectorUrl !== '')
132
+ return 'inspector';
133
+ if (argvHasInspectFlag(execArgv))
134
+ return 'inspector';
135
+ const options = nodeOptions.trim();
136
+ if (options !== '' && argvHasInspectFlag(options.split(/\s+/u)))
137
+ return 'inspector';
138
+ return null;
139
+ }
140
+ /**
141
+ * Self-restart is enabled by default, disabled by an explicit false — and
142
+ * disabled by DEFAULT under a detected supervisor, which owns restarts and
143
+ * whose process group would take the replacement helper down with it.
144
+ *
145
+ * An explicit `true` still wins: an operator who has configured their unit
146
+ * for it (`KillMode=process`, or a wrapper that survives) is making a
147
+ * statement about their own deployment, and this should not overrule it.
148
+ */
149
+ export function restartAllowed(config, env = process.env, ppid = process.ppid) {
150
+ if (config.allowRestart !== undefined)
151
+ return config.allowRestart;
152
+ return detectedSupervisor(env, ppid) === null;
153
+ }
154
+ /**
155
+ * The port this process is serving on, read off the request that asked for
156
+ * the restart.
157
+ *
158
+ * The alternative is to parse it out of the launch argv, which is wrong for
159
+ * every host that binds from config or an env var. The Host header is what
160
+ * the browser actually reached us on, so it is the port the replacement has
161
+ * to take over — and it is already validated against Origin by the guard
162
+ * below before any of this runs.
163
+ * @returns the port, or null when the header carries none (a default port).
164
+ */
165
+ export function servingPort(request) {
166
+ const host = request.headers.host;
167
+ if (host === undefined)
168
+ return null;
169
+ const match = /:(\d{1,5})$/u.exec(host);
170
+ if (match === null)
171
+ return null;
172
+ const port = Number(match[1]);
173
+ return Number.isInteger(port) && port > 0 && port < 65536 ? port : null;
174
+ }
175
+ /** Whether a process-control request came from this Web host on loopback. */
176
+ export function trustedRestartRequest(request) {
177
+ const address = request.socket.remoteAddress;
178
+ if (address !== '127.0.0.1' && address !== '::1' && address !== '::ffff:127.0.0.1')
179
+ return false;
180
+ // Any forwarding trace means the loopback peer is a proxy, not the user.
181
+ if (request.headers.forwarded !== undefined
182
+ || request.headers['x-forwarded-for'] !== undefined
183
+ || request.headers['x-real-ip'] !== undefined)
184
+ return false;
185
+ const origin = request.headers.origin;
186
+ const host = request.headers.host;
187
+ if (origin === undefined || host === undefined)
188
+ return false;
189
+ try {
190
+ const parsed = new URL(origin);
191
+ return (parsed.protocol === 'http:' || parsed.protocol === 'https:') && parsed.host === host;
192
+ }
193
+ catch {
194
+ return false;
195
+ }
196
+ }
197
+ /**
198
+ * Whether a download navigation may fetch a sensitive GET export.
199
+ * Browsers do NOT send an Origin header on same-origin GET navigations
200
+ * (`<a href="/..." download>`), so unlike process-control requests a missing
201
+ * Origin is the NORMAL shape of a user-initiated download and must pass.
202
+ * Keep the rest of the posture: loopback peer only, no proxy forwarding
203
+ * headers, and — when an Origin IS present (fetch/CORS attempts) — it must
204
+ * still match Host so a cross-origin page cannot read the export.
205
+ */
206
+ export function trustedDownloadRequest(request) {
207
+ const address = request.socket.remoteAddress;
208
+ if (address !== '127.0.0.1' && address !== '::1' && address !== '::ffff:127.0.0.1')
209
+ return false;
210
+ if (request.headers.forwarded !== undefined
211
+ || request.headers['x-forwarded-for'] !== undefined
212
+ || request.headers['x-real-ip'] !== undefined)
213
+ return false;
214
+ const origin = request.headers.origin;
215
+ const host = request.headers.host;
216
+ if (host === undefined)
217
+ return false;
218
+ if (origin === undefined)
219
+ return true; // plain browser download navigation
220
+ try {
221
+ const parsed = new URL(origin);
222
+ return (parsed.protocol === 'http:' || parsed.protocol === 'https:') && parsed.host === host;
223
+ }
224
+ catch {
225
+ return false;
226
+ }
227
+ }
228
+ /** The exact boot invocation the detached restart helper replays. */
229
+ export function restartLaunch() {
230
+ const launch = dshArgv();
231
+ return {
232
+ ...launch,
233
+ args: [...launch.args, ...process.argv.slice(2)],
234
+ cwd: launch.cwd ?? process.cwd(),
235
+ };
236
+ }
237
+ /**
238
+ * Platform-correct spawn invocation for the replacement host (#40 by
239
+ * @1123762794): on Windows a `detached` spawn maps to DETACHED_PROCESS — the
240
+ * new host gets NO console, and every console child it later spawns (e.g.
241
+ * ChatCode CLI sandbox tool runners) pops a visible node window. Wrapping the launch
242
+ * in `powershell -WindowStyle Hidden` gives the host a HIDDEN console that
243
+ * children inherit instead. POSIX keeps the plain detached spawn.
244
+ */
245
+ export function respawnInvocation(launch, platform = process.platform) {
246
+ if (platform !== 'win32') {
247
+ return { file: launch.file, args: launch.args, viaShell: launch.viaShell, detached: true };
248
+ }
249
+ // PowerShell single-quoting: only embedded single quotes need escaping
250
+ // (doubled). Name the cmd shim explicitly: invoking a bare npm bin lets
251
+ // PowerShell prefer its .ps1 shim, which a default Restricted execution policy
252
+ // refuses before the replacement can start (#397). The .cmd shim is produced
253
+ // by the same npm install and is not governed by PowerShell script policy.
254
+ const quote = (part) => `'${part.replace(/'/g, "''")}'`;
255
+ const file = launch.viaShell && !/\.(?:cmd|bat)$/iu.test(launch.file)
256
+ ? `${launch.file}.cmd`
257
+ : launch.file;
258
+ return {
259
+ file: 'powershell.exe',
260
+ args: ['-NoProfile', '-WindowStyle', 'Hidden', '-Command',
261
+ [`& ${quote(file)}`, ...launch.args.map(quote)].join(' ')],
262
+ viaShell: false,
263
+ detached: false,
264
+ };
265
+ }
266
+ /**
267
+ * Source for the detached helper that outlives this process and brings the
268
+ * replacement up.
269
+ *
270
+ * Extracted so the waiting can be tested by RUNNING it, which is the only
271
+ * way this class of bug shows itself: every part of the old helper looked
272
+ * right in isolation.
273
+ *
274
+ * What it fixes (#177, reported on Windows 11, reproducible every time): the
275
+ * helper slept a flat 1500ms and spawned. The old process had exited, but
276
+ * the listening socket had not been released yet, so the replacement died
277
+ * instantly with EADDRINUSE — and the spawn was wrapped in `catch {}`, so
278
+ * nothing was written anywhere. The user saw a restart button that did
279
+ * nothing. The docstring above it even claimed the helper "waits for our
280
+ * port to free up"; it never did.
281
+ *
282
+ * So: wait for the port to actually go quiet, then start, then CHECK that
283
+ * something came up, and write a diagnosis when it did not. A restart that
284
+ * fails must leave evidence — this one is invisible by construction, since
285
+ * the process that would have logged it is the one that just exited.
286
+ * @param port - the port the replacement must bind; when unknown, the helper
287
+ * falls back to the old fixed delay, which is better than nothing.
288
+ */
289
+ export function restartHelperSource(spawned, launch, logs, port) {
290
+ return [
291
+ "const { spawn } = require('node:child_process')",
292
+ "const fs = require('node:fs')",
293
+ "const net = require('node:net')",
294
+ `const file = ${JSON.stringify(spawned.file)}`,
295
+ `const args = ${JSON.stringify(spawned.args)}`,
296
+ `const cwd = ${JSON.stringify(launch.cwd)}`,
297
+ `const viaShell = ${JSON.stringify(spawned.viaShell)}`,
298
+ `const detached = ${JSON.stringify(spawned.detached)}`,
299
+ `const logOut = ${JSON.stringify(logs.out)}`,
300
+ `const logErr = ${JSON.stringify(logs.err)}`,
301
+ `const port = ${JSON.stringify(port)}`,
302
+ 'const sleep = (ms) => new Promise(r => setTimeout(r, ms))',
303
+ 'const note = (line) => { try { fs.appendFileSync(logErr, `[ChatCode CLI Market] ${line}\n`) } catch {} }',
304
+ // "Free" means nothing accepts a connection. Checked by connecting rather
305
+ // than by binding: binding to test would itself hold the port for the
306
+ // moment the replacement needs it.
307
+ 'const listening = () => new Promise((resolve) => {',
308
+ ' const probe = net.connect({ host: "127.0.0.1", port })',
309
+ ' const done = (value) => { probe.destroy(); resolve(value) }',
310
+ ' probe.on("connect", () => done(true))',
311
+ ' probe.on("error", () => done(false))',
312
+ ' setTimeout(() => done(false), 500)',
313
+ '})',
314
+ 'const main = async () => {',
315
+ ' if (port) {',
316
+ ' const until = Date.now() + 30000',
317
+ ' while (Date.now() < until && await listening()) await sleep(250)',
318
+ ' if (await listening()) note(`port ${port} was still in use after 30s; starting anyway`)',
319
+ // A released socket can still be in TIME_WAIT for a moment on Windows.
320
+ ' await sleep(300)',
321
+ ' } else {',
322
+ ' await sleep(1500)',
323
+ ' }',
324
+ ' let child',
325
+ ' try {',
326
+ ' const out = fs.openSync(logOut, "a")',
327
+ ' const err = fs.openSync(logErr, "a")',
328
+ ' child = spawn(file, args, { cwd, detached, stdio: ["ignore", out, err], env: process.env, shell: viaShell })',
329
+ // spawn reports a missing or unexecutable file ASYNCHRONOUSLY; the
330
+ // try/catch below only covers the synchronous throw, so without this
331
+ // listener that failure is exactly as silent as the bug being fixed.
332
+ ' child.on("error", (error) => note(`could not start the replacement: ${error && error.message ? error.message : error}`))',
333
+ ' child.unref()',
334
+ ' } catch (error) {',
335
+ ' note(`could not start the replacement: ${error && error.message ? error.message : error}`)',
336
+ ' return',
337
+ ' }',
338
+ // Outliving the spawn matters on Windows: a helper that exits the
339
+ // instant it has spawned can take the replacement with it, because the
340
+ // child is in its process group and has not detached yet. The port path
341
+ // below already lingers while it polls; this is the same guarantee for
342
+ // the path that has no port to poll. CI on windows-latest caught it —
343
+ // locally it passes either way.
344
+ " if (!port) { await sleep(3000); return }",
345
+ ' const upBy = Date.now() + 20000',
346
+ ' while (Date.now() < upBy && !(await listening())) await sleep(500)',
347
+ ' if (!(await listening())) note(`the replacement did not bind port ${port} within 20s — see the output log beside this one`)',
348
+ '}',
349
+ 'main()',
350
+ ].join('\n');
351
+ }
352
+ /**
353
+ * Relaunch this exact ChatCode CLI entry after a detached handoff, then stop this
354
+ * process. The helper outlives us (detached + unref), waits for our port to
355
+ * be released before starting the replacement, and logs under tmpdir.
356
+ * @param port - the port this process is serving on, so the helper can wait
357
+ * for it rather than guessing at a delay.
358
+ */
359
+ export function scheduleRestart(port = null) {
360
+ const launch = restartLaunch();
361
+ const spawned = respawnInvocation(launch);
362
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
363
+ const logOut = join(tmpdir(), `chatcode-cli-market-restart-${stamp}.out.log`);
364
+ const logErr = join(tmpdir(), `chatcode-cli-market-restart-${stamp}.err.log`);
365
+ const helper = spawn(nodeExecutable(), ['-e', restartHelperSource(spawned, launch, { out: logOut, err: logErr }, port)], {
366
+ detached: true,
367
+ stdio: 'ignore',
368
+ env: process.env,
369
+ });
370
+ helper.unref();
371
+ setTimeout(() => process.kill(process.pid, 'SIGTERM'), 500);
372
+ return { pid: process.pid, helperPid: helper.pid, logOut, logErr };
373
+ }