@opencode/client 2.0.8 → 2.0.9

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 (56) hide show
  1. package/dist/chunks/pty-handoff-1tbj6z39.js +81 -0
  2. package/dist/chunks/pty-handoff-4tveyqeh.js +34 -0
  3. package/dist/chunks/pty-handoff-65kht65h.js +62 -0
  4. package/dist/chunks/pty-handoff-6mptkzbc.js +0 -0
  5. package/dist/chunks/pty-handoff-9rqn6x4v.js +16 -0
  6. package/dist/chunks/pty-handoff-atpwm825.js +1638 -0
  7. package/dist/chunks/pty-handoff-bssvn03b.js +594 -0
  8. package/dist/chunks/pty-handoff-frbwqjmf.js +145 -0
  9. package/dist/chunks/pty-handoff-m2eywc8w.js +0 -0
  10. package/dist/chunks/pty-handoff-n3c99vt5.js +11 -0
  11. package/dist/chunks/pty-handoff-nqd0dnpk.js +112 -0
  12. package/dist/chunks/pty-handoff-qav6mfdz.js +0 -0
  13. package/dist/chunks/pty-handoff-radw39tv.js +1402 -0
  14. package/dist/chunks/pty-handoff-sxt88q69.js +12 -0
  15. package/dist/chunks/pty-handoff-t1xa31vm.js +39 -0
  16. package/dist/chunks/pty-handoff-txckps9j.js +57 -0
  17. package/dist/chunks/pty-handoff-vgxtr9hm.js +30 -0
  18. package/dist/chunks/pty-handoff-vmytr4q8.js +4 -0
  19. package/dist/chunks/pty-handoff-vyas3vgt.js +0 -0
  20. package/dist/chunks/pty-handoff-xgnj2w4v.js +20 -0
  21. package/dist/chunks/pty-handoff-y2etg5bs.js +9 -0
  22. package/dist/chunks/pty-handoff-ydhz1zrh.js +238 -0
  23. package/dist/chunks/pty-handoff-yepdgb80.js +44 -0
  24. package/dist/chunks/pty-handoff-zadn5mwr.js +52 -0
  25. package/dist/contract.js +12 -1
  26. package/dist/effect/client.d.ts +4 -4
  27. package/dist/effect/client.js +15 -29
  28. package/dist/effect/generated/client-error.js +6 -5
  29. package/dist/effect/generated/client.d.ts +4 -4
  30. package/dist/effect/generated/client.js +8 -582
  31. package/dist/effect/generated/index.js +12 -2
  32. package/dist/effect/index.js +75 -30
  33. package/dist/effect/rpc.js +11 -49
  34. package/dist/effect/service.js +238 -245
  35. package/dist/promise/client.js +14 -13
  36. package/dist/promise/generated/client-error.js +6 -8
  37. package/dist/promise/generated/client.js +7 -1396
  38. package/dist/promise/generated/index.js +75 -3
  39. package/dist/promise/generated/types.d.ts +90 -88
  40. package/dist/promise/generated/types.js +66 -34
  41. package/dist/promise/index.js +79 -2
  42. package/dist/promise/rpc.js +7 -86
  43. package/dist/promise/service.js +230 -227
  44. package/dist/pty-handoff.js +14 -121
  45. package/dist/rpc-runtime.js +14 -32
  46. package/dist/service-contender.js +10 -56
  47. package/dist/service-timing.d.ts +0 -1
  48. package/dist/service-timing.js +9 -18
  49. package/dist/service-version.js +6 -9
  50. package/dist/service.js +1 -0
  51. package/dist/shared-events.js +8 -137
  52. package/dist/solid/connection.js +10 -246
  53. package/dist/solid/data.js +18 -1774
  54. package/dist/solid/index.js +32 -3
  55. package/dist/solid/pty.js +8 -41
  56. package/package.json +4 -4
@@ -1,263 +1,266 @@
1
+ import {
2
+ spawnServiceContender2,
3
+ contenderFailure2,
4
+ contenderFinished2
5
+ } from "../chunks/pty-handoff-txckps9j.js";
6
+ import {
7
+ defaultEnsureTiming2,
8
+ ensureTiming2
9
+ } from "../chunks/pty-handoff-xgnj2w4v.js";
10
+ import {
11
+ matchesVersion2
12
+ } from "../chunks/pty-handoff-sxt88q69.js";
13
+ import {
14
+ prepare2,
15
+ environment2,
16
+ complete2,
17
+ clear2
18
+ } from "../chunks/pty-handoff-nqd0dnpk.js";
19
+ import"../chunks/pty-handoff-qav6mfdz.js";
20
+
21
+ // src/promise/service.ts
1
22
  import { readFile, rm } from "node:fs/promises";
2
23
  import { homedir } from "node:os";
3
24
  import { join } from "node:path";
4
- import { contenderFailure, contenderFinished, spawnServiceContender, } from "../service-contender.js";
5
- import { defaultEnsureTiming, ensureTiming } from "../service-timing.js";
6
- import { matchesVersion } from "../service-version.js";
7
- import { PtyHandoff } from "../pty-handoff.js";
8
- export * from "../service.js";
9
- // Find, start, and stop the local opencode background service.
10
- //
11
- // The registration file is the complete discovery contract. This module is
12
- // intentionally implemented with Node APIs so Promise clients do not need
13
- // Effect or @effect/platform-node at runtime.
14
- /** Discover a healthy, compatible local service without starting one. */
15
- export async function discover(options = {}) {
16
- const found = (await registered(options.file)).service;
17
- if (found?.state !== "ready")
18
- return undefined;
19
- if (!found.compatible)
20
- return undefined;
21
- if (!matchesVersion(found.version, options))
22
- return undefined;
23
- return found.endpoint;
25
+ async function discover(options = {}) {
26
+ const found = (await registered(options.file)).service;
27
+ if (found?.state !== "ready")
28
+ return;
29
+ if (!found.compatible)
30
+ return;
31
+ if (!matchesVersion2(found.version, options))
32
+ return;
33
+ return found.endpoint;
24
34
  }
25
- /** Ensure a healthy, compatible local service is running. */
26
- export async function ensure(options = {}) {
27
- const timing = ensureTiming(options);
28
- const deadline = Date.now() + timing.promiseTimeout;
29
- const contenders = new Set();
30
- let timeouts;
31
- let announced = false;
32
- let lastSpawn = 0;
33
- let spawnDelay = timing.spawnDelay;
34
- const announce = (reason, previousVersion) => {
35
- if (announced)
35
+ async function ensure(options = {}) {
36
+ const timing = ensureTiming2(options);
37
+ const deadline = Date.now() + timing.promiseTimeout;
38
+ const contenders = new Set;
39
+ let timeouts;
40
+ let announced = false;
41
+ let lastSpawn = 0;
42
+ let spawnDelay = timing.spawnDelay;
43
+ const announce = (reason, previousVersion) => {
44
+ if (announced)
45
+ return;
46
+ announced = true;
47
+ options.onStart?.(reason, previousVersion);
48
+ };
49
+ const spawnContender = async () => {
50
+ const [command, ...args] = options.command ?? ["opencode", "serve", "--service"];
51
+ if (command === undefined)
52
+ throw new Error("Missing service command");
53
+ try {
54
+ return spawnServiceContender2(command, args, await environment2(options.file ?? fallback(), options.env));
55
+ } catch (cause) {
56
+ throw new Error("Failed to start server", { cause });
57
+ }
58
+ };
59
+ try {
60
+ while (true) {
61
+ if (Date.now() >= deadline)
62
+ throw new Error("Timed out waiting for the background service to start");
63
+ const registration = await registered(options.file, timing.requestTimeout);
64
+ if (registration.timedOut && registration.info !== undefined) {
65
+ timeouts = {
66
+ info: registration.info,
67
+ count: timeouts !== undefined && same(timeouts.info, registration.info) ? timeouts.count + 1 : 1
68
+ };
69
+ if (timeouts.count >= 3) {
70
+ announce("missing");
71
+ console.warn("Background service is unresponsive; recovery cannot preserve persistent terminals");
72
+ await clear2(options.file ?? fallback());
73
+ await terminate(registration.info, options, timing);
74
+ timeouts = undefined;
75
+ lastSpawn = Date.now() - spawnDelay;
76
+ }
77
+ } else
78
+ timeouts = undefined;
79
+ if (registration.service !== undefined) {
80
+ spawnDelay = timing.spawnDelay;
81
+ const service2 = registration.service;
82
+ const compatible = service2.compatible && matchesVersion2(service2.version, options);
83
+ if (compatible && service2.state === "ready") {
84
+ await complete2(options.file ?? fallback(), service2.info);
85
+ return service2.endpoint;
86
+ }
87
+ if (compatible && service2.state === "failed")
88
+ throw new Error("Background service failed to start");
89
+ if (!compatible) {
90
+ announce("version-mismatch", service2.version);
91
+ if (service2.state !== "ready")
92
+ console.warn("Background service is not ready; replacement cannot preserve persistent terminals");
93
+ await stop({
94
+ file: options.file,
95
+ pty: service2.state === "ready" ? "handoff" : "clear"
96
+ }).catch(() => {
36
97
  return;
37
- announced = true;
38
- options.onStart?.(reason, previousVersion);
39
- };
40
- const spawnContender = async () => {
41
- const [command, ...args] = options.command ?? ["opencode", "serve", "--service"];
42
- if (command === undefined)
43
- throw new Error("Missing service command");
44
- try {
45
- return spawnServiceContender(command, args, await PtyHandoff.environment(options.file ?? fallback(), options.env));
98
+ });
99
+ lastSpawn = 0;
46
100
  }
47
- catch (cause) {
48
- throw new Error("Failed to start server", { cause });
101
+ } else {
102
+ if (lastSpawn === 0 && registration.info !== undefined)
103
+ lastSpawn = Date.now();
104
+ const finished = [...contenders].filter(contenderFinished2);
105
+ const failure = finished.map(contenderFailure2).find((error) => error !== undefined);
106
+ if (finished.some((item) => item.child.exitCode === 0)) {
107
+ spawnDelay = Math.min(spawnDelay * 2, timing.maxSpawnDelay);
49
108
  }
50
- };
51
- try {
52
- while (true) {
53
- if (Date.now() >= deadline)
54
- throw new Error("Timed out waiting for the background service to start");
55
- const registration = await registered(options.file, timing.requestTimeout);
56
- if (registration.timedOut && registration.info !== undefined) {
57
- timeouts = {
58
- info: registration.info,
59
- count: timeouts !== undefined && same(timeouts.info, registration.info) ? timeouts.count + 1 : 1,
60
- };
61
- if (timeouts.count >= 3) {
62
- announce("missing");
63
- console.warn("Background service is unresponsive; recovery cannot preserve persistent terminals");
64
- await PtyHandoff.clear(options.file ?? fallback());
65
- await terminate(registration.info, options, timing);
66
- timeouts = undefined;
67
- lastSpawn = Date.now() - spawnDelay;
68
- }
69
- }
70
- else
71
- timeouts = undefined;
72
- if (registration.service !== undefined) {
73
- spawnDelay = timing.spawnDelay;
74
- const service = registration.service;
75
- const compatible = service.compatible && matchesVersion(service.version, options);
76
- if (compatible && service.state === "ready") {
77
- await PtyHandoff.complete(options.file ?? fallback(), service.info);
78
- return service.endpoint;
79
- }
80
- if (compatible && service.state === "failed")
81
- throw new Error("Background service failed to start");
82
- if (!compatible) {
83
- announce("version-mismatch", service.version);
84
- if (service.state !== "ready")
85
- console.warn("Background service is not ready; replacement cannot preserve persistent terminals");
86
- await stop({
87
- file: options.file,
88
- pty: service.state === "ready" ? "handoff" : "clear",
89
- }).catch(() => undefined);
90
- lastSpawn = 0;
91
- }
92
- }
93
- else {
94
- if (lastSpawn === 0 && registration.info !== undefined)
95
- lastSpawn = Date.now();
96
- const finished = [...contenders].filter(contenderFinished);
97
- const failure = finished.map(contenderFailure).find((error) => error !== undefined);
98
- if (finished.some((item) => item.child.exitCode === 0)) {
99
- spawnDelay = Math.min(spawnDelay * 2, timing.maxSpawnDelay);
100
- }
101
- finished.forEach((item) => contenders.delete(item));
102
- if (failure !== undefined && contenders.size === 0)
103
- throw failure;
104
- // Keep one candidate plus one lock probe so a pre-lock stall cannot block recovery.
105
- if (contenders.size < 2 && Date.now() - lastSpawn >= spawnDelay) {
106
- announce("missing");
107
- contenders.add(await spawnContender());
108
- lastSpawn = Date.now();
109
- }
110
- }
111
- await delay(timing.pollInterval);
109
+ finished.forEach((item) => contenders.delete(item));
110
+ if (failure !== undefined && contenders.size === 0)
111
+ throw failure;
112
+ if (contenders.size < 2 && Date.now() - lastSpawn >= spawnDelay) {
113
+ announce("missing");
114
+ contenders.add(await spawnContender());
115
+ lastSpawn = Date.now();
112
116
  }
117
+ }
118
+ await delay(timing.pollInterval);
113
119
  }
114
- finally {
115
- contenders.forEach((contender) => contender.release());
116
- }
120
+ } finally {
121
+ contenders.forEach((contender) => contender.release());
122
+ }
117
123
  }
118
- /** Stop the registered local service. */
119
- export async function stop(options = {}) {
120
- const info = await read(options.file);
121
- if (options.pty === "handoff" && info !== undefined)
122
- await PtyHandoff.prepare(options.file ?? fallback(), info, defaultEnsureTiming.requestTimeout);
123
- else
124
- await PtyHandoff.clear(options.file ?? fallback());
125
- if (info !== undefined)
126
- await terminate(info, options, defaultEnsureTiming);
124
+ async function stop(options = {}) {
125
+ const info = await read(options.file);
126
+ if (options.pty === "handoff" && info !== undefined)
127
+ await prepare2(options.file ?? fallback(), info, defaultEnsureTiming2.requestTimeout);
128
+ else
129
+ await clear2(options.file ?? fallback());
130
+ if (info !== undefined)
131
+ await terminate(info, options, defaultEnsureTiming2);
127
132
  }
128
133
  function fallback() {
129
- return join(process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state"), "opencode", "service.json");
134
+ return join(process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state"), "opencode", "service.json");
130
135
  }
131
- /** Create HTTP authentication headers for a service endpoint. */
132
- export function headers(endpoint) {
133
- if (endpoint.auth === undefined)
134
- return undefined;
135
- return {
136
- authorization: "Basic " + Buffer.from(endpoint.auth.username + ":" + endpoint.auth.password).toString("base64"),
137
- };
136
+ function headers(endpoint) {
137
+ if (endpoint.auth === undefined)
138
+ return;
139
+ return {
140
+ authorization: "Basic " + Buffer.from(endpoint.auth.username + ":" + endpoint.auth.password).toString("base64")
141
+ };
138
142
  }
139
143
  async function read(file) {
140
- const text = await readFile(file ?? fallback(), "utf8").catch(() => undefined);
141
- if (text === undefined)
142
- return undefined;
143
- try {
144
- return JSON.parse(text);
145
- }
146
- catch {
147
- return undefined;
148
- }
144
+ const text = await readFile(file ?? fallback(), "utf8").catch(() => {
145
+ return;
146
+ });
147
+ if (text === undefined)
148
+ return;
149
+ try {
150
+ return JSON.parse(text);
151
+ } catch {
152
+ return;
153
+ }
149
154
  }
150
- async function probeResult(info, timeout = defaultEnsureTiming.requestTimeout) {
151
- const endpoint = {
152
- url: info.url,
153
- auth: info.password === undefined
154
- ? undefined
155
- : { type: "basic", username: "opencode", password: info.password },
155
+ async function probeResult(info, timeout = defaultEnsureTiming2.requestTimeout) {
156
+ const endpoint = {
157
+ url: info.url,
158
+ auth: info.password === undefined ? undefined : { type: "basic", username: "opencode", password: info.password }
159
+ };
160
+ const signal = AbortSignal.timeout(timeout);
161
+ const result = await fetch(new URL("/api/info", info.url), { headers: headers(endpoint), signal }).then(async (response) => ({
162
+ response,
163
+ body: response.status === 404 ? undefined : await response.json()
164
+ })).then((value) => ({ value }), (cause) => ({ cause }));
165
+ if ("cause" in result)
166
+ return { service: undefined, timedOut: signal.aborted };
167
+ const response = result.value.response;
168
+ if (response.status === 404)
169
+ return {
170
+ service: {
171
+ info,
172
+ endpoint,
173
+ version: info.version,
174
+ state: "ready",
175
+ compatible: false
176
+ },
177
+ timedOut: false
156
178
  };
157
- const signal = AbortSignal.timeout(timeout);
158
- const result = await fetch(new URL("/api/info", info.url), { headers: headers(endpoint), signal })
159
- .then(async (response) => ({
160
- response,
161
- body: response.status === 404 ? undefined : (await response.json()),
162
- }))
163
- .then((value) => ({ value }), (cause) => ({ cause }));
164
- if ("cause" in result)
165
- return { service: undefined, timedOut: signal.aborted };
166
- const response = result.value.response;
167
- // The previous V2 service exposes /api/status instead. Its authenticated 404 is enough
168
- // to recognize the registered daemon as incompatible and route it through replacement.
169
- if (response.status === 404)
170
- return {
171
- service: {
172
- info,
173
- endpoint,
174
- version: info.version,
175
- state: "ready",
176
- compatible: false,
177
- },
178
- timedOut: false,
179
- };
180
- const serverInfo = decodeInfo(result.value.body);
181
- if (serverInfo !== undefined) {
182
- if (serverInfo.pid !== info.pid)
183
- return { service: undefined, timedOut: false };
184
- if (info.version !== undefined && serverInfo.version !== info.version)
185
- return { service: undefined, timedOut: false };
186
- return {
187
- service: {
188
- info,
189
- endpoint,
190
- version: serverInfo.version,
191
- state: response.ok ? "ready" : response.status === 500 ? "failed" : "waiting",
192
- compatible: true,
193
- },
194
- timedOut: false,
195
- };
196
- }
197
- return { service: undefined, timedOut: false };
179
+ const serverInfo = decodeInfo(result.value.body);
180
+ if (serverInfo !== undefined) {
181
+ if (serverInfo.pid !== info.pid)
182
+ return { service: undefined, timedOut: false };
183
+ if (info.version !== undefined && serverInfo.version !== info.version)
184
+ return { service: undefined, timedOut: false };
185
+ return {
186
+ service: {
187
+ info,
188
+ endpoint,
189
+ version: serverInfo.version,
190
+ state: response.ok ? "ready" : response.status === 500 ? "failed" : "waiting",
191
+ compatible: true
192
+ },
193
+ timedOut: false
194
+ };
195
+ }
196
+ return { service: undefined, timedOut: false };
198
197
  }
199
198
  function decodeInfo(input) {
200
- if (typeof input !== "object" || input === null)
201
- return;
202
- if (!("version" in input) || typeof input.version !== "string")
203
- return;
204
- if (!("pid" in input) || typeof input.pid !== "number" || !Number.isInteger(input.pid) || input.pid < 0)
205
- return;
206
- return { version: input.version, pid: input.pid };
199
+ if (typeof input !== "object" || input === null)
200
+ return;
201
+ if (!("version" in input) || typeof input.version !== "string")
202
+ return;
203
+ if (!("pid" in input) || typeof input.pid !== "number" || !Number.isInteger(input.pid) || input.pid < 0)
204
+ return;
205
+ return { version: input.version, pid: input.pid };
207
206
  }
208
207
  async function registered(file, timeout) {
209
- const info = await read(file);
210
- if (info === undefined)
211
- return { info: undefined, service: undefined, timedOut: false };
212
- return { info, ...(await probeResult(info, timeout)) };
208
+ const info = await read(file);
209
+ if (info === undefined)
210
+ return { info: undefined, service: undefined, timedOut: false };
211
+ return { info, ...await probeResult(info, timeout) };
213
212
  }
214
213
  function signal(pid, name) {
215
- try {
216
- process.kill(pid, name);
217
- }
218
- catch { }
214
+ try {
215
+ process.kill(pid, name);
216
+ } catch {}
219
217
  }
220
218
  function stopped(pid) {
221
- try {
222
- process.kill(pid, 0);
223
- return false;
224
- }
225
- catch {
226
- return true;
227
- }
219
+ try {
220
+ process.kill(pid, 0);
221
+ return false;
222
+ } catch {
223
+ return true;
224
+ }
228
225
  }
229
226
  async function waitUntilStopped(pid, timing) {
230
- for (let attempt = 0; attempt <= timing.stopPollAttempts; attempt++) {
231
- if (stopped(pid))
232
- return true;
233
- if (attempt < timing.stopPollAttempts)
234
- await delay(timing.stopPollInterval);
235
- }
236
- return false;
227
+ for (let attempt = 0;attempt <= timing.stopPollAttempts; attempt++) {
228
+ if (stopped(pid))
229
+ return true;
230
+ if (attempt < timing.stopPollAttempts)
231
+ await delay(timing.stopPollInterval);
232
+ }
233
+ return false;
237
234
  }
238
235
  function same(left, right) {
239
- return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid;
236
+ return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid;
240
237
  }
241
238
  async function terminate(info, options, timing) {
242
- const current = await read(options.file);
243
- if (current === undefined || !same(current, info))
244
- return;
245
- signal(info.pid, "SIGTERM");
246
- if (!(await waitUntilStopped(info.pid, timing))) {
247
- const latest = await read(options.file);
248
- if (latest === undefined || !same(latest, info))
249
- return;
250
- signal(info.pid, "SIGKILL");
251
- if (!(await waitUntilStopped(info.pid, timing)))
252
- throw new Error(`Server process ${info.pid} is still running`);
253
- }
239
+ const current = await read(options.file);
240
+ if (current === undefined || !same(current, info))
241
+ return;
242
+ signal(info.pid, "SIGTERM");
243
+ if (!await waitUntilStopped(info.pid, timing)) {
254
244
  const latest = await read(options.file);
255
245
  if (latest === undefined || !same(latest, info))
256
- return;
257
- await rm(options.file ?? fallback(), { force: true });
246
+ return;
247
+ signal(info.pid, "SIGKILL");
248
+ if (!await waitUntilStopped(info.pid, timing))
249
+ throw new Error(`Server process ${info.pid} is still running`);
250
+ }
251
+ const latest = await read(options.file);
252
+ if (latest === undefined || !same(latest, info))
253
+ return;
254
+ await rm(options.file ?? fallback(), { force: true });
258
255
  }
259
256
  function delay(milliseconds) {
260
- return new Promise((resolve) => setTimeout(resolve, milliseconds));
257
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
261
258
  }
262
- /** Promise-based local service lifecycle operations. */
263
- export const Service = { discover, ensure, stop, headers };
259
+ var Service = { discover, ensure, stop, headers };
260
+ export {
261
+ Service,
262
+ discover,
263
+ ensure,
264
+ headers,
265
+ stop
266
+ };
@@ -1,121 +1,14 @@
1
- export * as PtyHandoff from "./pty-handoff.js";
2
- import { readFile, rename, rm, writeFile } from "node:fs/promises";
3
- /** Publish the ticket before stopping its owner so every replacement contender can adopt it. */
4
- export async function prepare(file, info, timeout) {
5
- const existing = await read(file);
6
- if (existing !== undefined && existing.expiresAt > Date.now() && same(existing.source, info))
7
- return;
8
- const { ClientError, OpenCode } = await import("./promise/index.js");
9
- const client = OpenCode.make({
10
- baseUrl: info.url,
11
- headers: info.password === undefined
12
- ? undefined
13
- : { authorization: "Basic " + Buffer.from(`opencode:${info.password}`).toString("base64") },
14
- });
15
- const missing = (error) => error instanceof ClientError &&
16
- error.reason === "UnexpectedStatus" &&
17
- typeof error.cause === "object" &&
18
- error.cause !== null &&
19
- "status" in error.cause &&
20
- error.cause.status === 404;
21
- const result = await client.experimental.persistentPty.handoff({ signal: AbortSignal.timeout(timeout) }).then((value) => ({ value }), (cause) => ({ cause }));
22
- if ("cause" in result) {
23
- // Another caller may already have prepared and stopped this server.
24
- const concurrent = await read(file);
25
- if (concurrent !== undefined && concurrent.expiresAt > Date.now() && same(concurrent.source, info))
26
- return;
27
- if (!missing(result.cause))
28
- throw new Error("Failed to prepare persistent terminals for service replacement", { cause: result.cause });
29
- console.warn("Background service cannot hand off persistent terminals; shutting them down before replacement");
30
- await client.experimental.persistentPty
31
- .shutdown({ signal: AbortSignal.timeout(timeout) })
32
- .catch((cause) => {
33
- if (missing(cause))
34
- return;
35
- throw new Error("Failed to shut down persistent terminals before service replacement", { cause });
36
- });
37
- await publish(file, info, null);
38
- return;
39
- }
40
- const body = result.value;
41
- if (typeof body !== "object" || body === null || !("handoff" in body))
42
- throw new Error("Invalid persistent terminal handoff response");
43
- if (body.handoff === null) {
44
- await publish(file, info, null);
45
- return;
46
- }
47
- if (!isHandoff(body.handoff) || body.handoff.expiresAt <= Date.now())
48
- throw new Error("Invalid or expired persistent terminal handoff");
49
- await publish(file, info, body.handoff);
50
- }
51
- async function publish(file, info, handoff) {
52
- const temporary = `${file}.pty-handoff.${crypto.randomUUID()}.tmp`;
53
- await writeFile(temporary, JSON.stringify({
54
- source: { id: info.id, pid: info.pid, url: info.url },
55
- handoff,
56
- expiresAt: handoff?.expiresAt ?? Date.now() + 30_000,
57
- }), { mode: 0o600, flag: "wx" });
58
- await rename(temporary, file + ".pty-handoff").finally(() => rm(temporary, { force: true }));
59
- }
60
- export async function environment(file, env) {
61
- const record = await read(file);
62
- const current = await readFile(file, "utf8")
63
- .then((text) => JSON.parse(text))
64
- .catch(() => undefined);
65
- const handoff = record !== undefined && record.expiresAt > Date.now() && (current === undefined || same(record.source, current))
66
- ? record.handoff
67
- : undefined;
68
- return { ...env, OPENCODE_PTY_HANDOFF: handoff == null ? undefined : JSON.stringify(handoff) };
69
- }
70
- export async function complete(file, info) {
71
- const record = await read(file);
72
- if (record !== undefined && !same(record.source, info))
73
- await clear(file);
74
- }
75
- export async function clear(file) {
76
- await rm(file + ".pty-handoff", { force: true });
77
- }
78
- async function read(file) {
79
- const value = await readFile(file + ".pty-handoff", "utf8")
80
- .then((text) => JSON.parse(text))
81
- .catch(() => undefined);
82
- if (typeof value !== "object" || value === null || !("source" in value) || !("handoff" in value))
83
- return;
84
- if (typeof value.source !== "object" || value.source === null)
85
- return;
86
- if (!("pid" in value.source) || typeof value.source.pid !== "number")
87
- return;
88
- if (!("url" in value.source) || typeof value.source.url !== "string")
89
- return;
90
- if ("id" in value.source && typeof value.source.id !== "string")
91
- return;
92
- if (value.handoff !== null && !isHandoff(value.handoff))
93
- return;
94
- if (!("expiresAt" in value) || typeof value.expiresAt !== "number" || !Number.isFinite(value.expiresAt))
95
- return;
96
- return {
97
- source: {
98
- id: "id" in value.source && typeof value.source.id === "string" ? value.source.id : undefined,
99
- pid: value.source.pid,
100
- url: value.source.url,
101
- },
102
- handoff: value.handoff,
103
- expiresAt: value.expiresAt,
104
- };
105
- }
106
- function same(left, right) {
107
- return left.id === right.id && left.pid === right.pid && left.url === right.url;
108
- }
109
- function isHandoff(value) {
110
- return (typeof value === "object" &&
111
- value !== null &&
112
- "directory" in value &&
113
- typeof value.directory === "string" &&
114
- "instanceID" in value &&
115
- typeof value.instanceID === "string" &&
116
- "ticket" in value &&
117
- typeof value.ticket === "string" &&
118
- "expiresAt" in value &&
119
- typeof value.expiresAt === "number" &&
120
- Number.isFinite(value.expiresAt));
121
- }
1
+ import {
2
+ prepare2,
3
+ environment2,
4
+ complete2,
5
+ clear2,
6
+ exports_pty_handoff
7
+ } from "./chunks/pty-handoff-nqd0dnpk.js";
8
+ export {
9
+ exports_pty_handoff as PtyHandoff,
10
+ clear2 as clear,
11
+ complete2 as complete,
12
+ environment2 as environment,
13
+ prepare2 as prepare
14
+ };