@opencode/client 2.0.7 → 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.
- package/dist/chunks/pty-handoff-1tbj6z39.js +81 -0
- package/dist/chunks/pty-handoff-4tveyqeh.js +34 -0
- package/dist/chunks/pty-handoff-65kht65h.js +62 -0
- package/dist/chunks/pty-handoff-6mptkzbc.js +0 -0
- package/dist/chunks/pty-handoff-9rqn6x4v.js +16 -0
- package/dist/chunks/pty-handoff-atpwm825.js +1638 -0
- package/dist/chunks/pty-handoff-bssvn03b.js +594 -0
- package/dist/chunks/pty-handoff-frbwqjmf.js +145 -0
- package/dist/chunks/pty-handoff-m2eywc8w.js +0 -0
- package/dist/chunks/pty-handoff-n3c99vt5.js +11 -0
- package/dist/chunks/pty-handoff-nqd0dnpk.js +112 -0
- package/dist/chunks/pty-handoff-qav6mfdz.js +0 -0
- package/dist/chunks/pty-handoff-radw39tv.js +1402 -0
- package/dist/chunks/pty-handoff-sxt88q69.js +12 -0
- package/dist/chunks/pty-handoff-t1xa31vm.js +39 -0
- package/dist/chunks/pty-handoff-txckps9j.js +57 -0
- package/dist/chunks/pty-handoff-vgxtr9hm.js +30 -0
- package/dist/chunks/pty-handoff-vmytr4q8.js +4 -0
- package/dist/chunks/pty-handoff-vyas3vgt.js +0 -0
- package/dist/chunks/pty-handoff-xgnj2w4v.js +20 -0
- package/dist/chunks/pty-handoff-y2etg5bs.js +9 -0
- package/dist/chunks/pty-handoff-ydhz1zrh.js +238 -0
- package/dist/chunks/pty-handoff-yepdgb80.js +44 -0
- package/dist/chunks/pty-handoff-zadn5mwr.js +52 -0
- package/dist/contract.js +12 -1
- package/dist/effect/client.d.ts +4 -4
- package/dist/effect/client.js +15 -29
- package/dist/effect/generated/client-error.js +6 -5
- package/dist/effect/generated/client.d.ts +4 -4
- package/dist/effect/generated/client.js +8 -582
- package/dist/effect/generated/index.js +12 -2
- package/dist/effect/index.js +75 -30
- package/dist/effect/rpc.js +11 -49
- package/dist/effect/service.js +238 -245
- package/dist/promise/client.js +14 -13
- package/dist/promise/generated/client-error.js +6 -8
- package/dist/promise/generated/client.js +7 -1396
- package/dist/promise/generated/index.js +75 -3
- package/dist/promise/generated/types.d.ts +91 -89
- package/dist/promise/generated/types.js +66 -34
- package/dist/promise/index.js +79 -2
- package/dist/promise/rpc.js +7 -86
- package/dist/promise/service.js +230 -227
- package/dist/pty-handoff.js +14 -121
- package/dist/rpc-runtime.js +14 -32
- package/dist/service-contender.js +10 -56
- package/dist/service-timing.d.ts +0 -1
- package/dist/service-timing.js +9 -18
- package/dist/service-version.js +6 -9
- package/dist/service.js +1 -0
- package/dist/shared-events.js +8 -137
- package/dist/solid/connection.js +10 -246
- package/dist/solid/data.js +18 -1769
- package/dist/solid/index.js +32 -3
- package/dist/solid/pty.js +8 -41
- package/package.json +4 -4
package/dist/promise/service.js
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
} finally {
|
|
121
|
+
contenders.forEach((contender) => contender.release());
|
|
122
|
+
}
|
|
117
123
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
134
|
+
return join(process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state"), "opencode", "service.json");
|
|
130
135
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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 =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
catch { }
|
|
214
|
+
try {
|
|
215
|
+
process.kill(pid, name);
|
|
216
|
+
} catch {}
|
|
219
217
|
}
|
|
220
218
|
function stopped(pid) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
257
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
261
258
|
}
|
|
262
|
-
|
|
263
|
-
export
|
|
259
|
+
var Service = { discover, ensure, stop, headers };
|
|
260
|
+
export {
|
|
261
|
+
Service,
|
|
262
|
+
discover,
|
|
263
|
+
ensure,
|
|
264
|
+
headers,
|
|
265
|
+
stop
|
|
266
|
+
};
|
package/dist/pty-handoff.js
CHANGED
|
@@ -1,121 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
+
};
|