@opencode-ai/client 0.0.0-next-15548 → 0.0.0-next-15555
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/effect/api/api.d.ts +8 -0
- package/dist/effect/generated/client.d.ts +21 -0
- package/dist/effect/generated/client.js +2 -1
- package/dist/effect/service.d.ts +26 -10
- package/dist/effect/service.js +182 -55
- package/dist/promise/generated/client.d.ts +3 -2
- package/dist/promise/generated/client.js +8 -0
- package/dist/promise/generated/types.d.ts +34 -5
- package/package.json +4 -4
package/dist/effect/api/api.d.ts
CHANGED
|
@@ -6,8 +6,16 @@ type EffectValue<A> = A extends Effect.Effect<infer Success, any, any> ? Success
|
|
|
6
6
|
type StreamValue<A> = A extends Stream.Stream<infer Success, any, any> ? Success : never;
|
|
7
7
|
export type Endpoint0_0Output = EffectValue<ReturnType<RawClient["server.health"]["health.get"]>>;
|
|
8
8
|
export type HealthGetOperation<E = never> = () => Effect.Effect<Endpoint0_0Output, E>;
|
|
9
|
+
type Endpoint0_1Request = Parameters<RawClient["server.health"]["health.stop"]>[0];
|
|
10
|
+
export type Endpoint0_1Input = {
|
|
11
|
+
readonly instanceID: Endpoint0_1Request["payload"]["instanceID"];
|
|
12
|
+
readonly targetVersion?: Endpoint0_1Request["payload"]["targetVersion"];
|
|
13
|
+
};
|
|
14
|
+
export type Endpoint0_1Output = EffectValue<ReturnType<RawClient["server.health"]["health.stop"]>>;
|
|
15
|
+
export type HealthStopOperation<E = never> = (input: Endpoint0_1Input) => Effect.Effect<Endpoint0_1Output, E>;
|
|
9
16
|
export interface HealthApi<E = never> {
|
|
10
17
|
readonly get: HealthGetOperation<E>;
|
|
18
|
+
readonly stop: HealthStopOperation<E>;
|
|
11
19
|
}
|
|
12
20
|
export type Endpoint1_0Output = EffectValue<ReturnType<RawClient["server.server"]["server.get"]>>;
|
|
13
21
|
export type ServerGetOperation<E = never> = () => Effect.Effect<Endpoint1_0Output, E>;
|
|
@@ -5,6 +5,11 @@ import { HttpApiClient } from "effect/unstable/httpapi";
|
|
|
5
5
|
import { ClientApi } from "../../contract";
|
|
6
6
|
import { ClientError } from "./client-error";
|
|
7
7
|
type RawClient = HttpApiClient.ForApi<typeof ClientApi>;
|
|
8
|
+
type Endpoint0_1Request = Parameters<RawClient["server.health"]["health.stop"]>[0];
|
|
9
|
+
type Endpoint0_1Input = {
|
|
10
|
+
readonly instanceID: Endpoint0_1Request["payload"]["instanceID"];
|
|
11
|
+
readonly targetVersion?: Endpoint0_1Request["payload"]["targetVersion"];
|
|
12
|
+
};
|
|
8
13
|
type Endpoint2_0Request = Parameters<RawClient["server.location"]["location.get"]>[0];
|
|
9
14
|
type Endpoint2_0Input = {
|
|
10
15
|
readonly location?: Endpoint2_0Request["query"]["location"];
|
|
@@ -506,6 +511,22 @@ export declare const make: (options?: {
|
|
|
506
511
|
readonly healthy: true;
|
|
507
512
|
readonly version: string;
|
|
508
513
|
readonly pid: number;
|
|
514
|
+
readonly status: {
|
|
515
|
+
readonly type: "starting";
|
|
516
|
+
} | {
|
|
517
|
+
readonly type: "ready";
|
|
518
|
+
} | {
|
|
519
|
+
readonly type: "stopping";
|
|
520
|
+
readonly targetVersion?: string | undefined;
|
|
521
|
+
} | {
|
|
522
|
+
readonly type: "failed";
|
|
523
|
+
readonly message: string;
|
|
524
|
+
readonly action: string;
|
|
525
|
+
};
|
|
526
|
+
readonly instanceID?: string | undefined;
|
|
527
|
+
}, Schema.SchemaError | import("@opencode-ai/protocol/errors").InvalidRequestError | import("@opencode-ai/protocol/errors").UnauthorizedError | HttpClientError.HttpClientError | ClientError, never>;
|
|
528
|
+
stop: (input: Endpoint0_1Input) => Effect.Effect<{
|
|
529
|
+
readonly accepted: boolean;
|
|
509
530
|
}, Schema.SchemaError | import("@opencode-ai/protocol/errors").InvalidRequestError | import("@opencode-ai/protocol/errors").UnauthorizedError | HttpClientError.HttpClientError | ClientError, never>;
|
|
510
531
|
};
|
|
511
532
|
server: {
|
|
@@ -9,7 +9,8 @@ const mapClientError = (error) => HttpClientError.isHttpClientError(error) || Sc
|
|
|
9
9
|
? new ClientError({ cause: error })
|
|
10
10
|
: error;
|
|
11
11
|
const Endpoint0_0 = (raw) => () => raw["health.get"]({}).pipe(Effect.mapError(mapClientError));
|
|
12
|
-
const
|
|
12
|
+
const Endpoint0_1 = (raw) => (input) => raw["health.stop"]({ payload: { instanceID: input["instanceID"], targetVersion: input["targetVersion"] } }).pipe(Effect.mapError(mapClientError));
|
|
13
|
+
const adaptGroup0 = (raw) => ({ get: Endpoint0_0(raw), stop: Endpoint0_1(raw) });
|
|
13
14
|
const Endpoint1_0 = (raw) => () => raw["server.get"]({}).pipe(Effect.mapError(mapClientError));
|
|
14
15
|
const adaptGroup1 = (raw) => ({ get: Endpoint1_0(raw) });
|
|
15
16
|
const Endpoint2_0 = (raw) => (input) => raw["location.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError));
|
package/dist/effect/service.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ServiceStatus } from "@opencode-ai/protocol/groups/health";
|
|
1
2
|
import { Effect, FileSystem, Schema } from "effect";
|
|
2
3
|
export type Endpoint = {
|
|
3
4
|
readonly url: string;
|
|
@@ -15,7 +16,23 @@ export type Options = {
|
|
|
15
16
|
export type StartReason = "missing" | "version-mismatch";
|
|
16
17
|
export type StartOptions = Options & {
|
|
17
18
|
readonly onStart?: (reason: StartReason, existing?: Info) => void;
|
|
19
|
+
readonly onStatus?: (status: Status) => void;
|
|
18
20
|
};
|
|
21
|
+
export type Status = {
|
|
22
|
+
readonly type: "missing";
|
|
23
|
+
} | {
|
|
24
|
+
readonly type: "unreachable";
|
|
25
|
+
} | {
|
|
26
|
+
readonly type: "unresponsive";
|
|
27
|
+
} | (ServiceStatus.State & {
|
|
28
|
+
readonly version?: string;
|
|
29
|
+
});
|
|
30
|
+
declare const FailedError_base: Schema.Class<FailedError, Schema.TaggedStruct<"ServiceFailedError", {
|
|
31
|
+
readonly message: Schema.String;
|
|
32
|
+
readonly action: Schema.String;
|
|
33
|
+
}>, import("effect/Cause").YieldableError>;
|
|
34
|
+
export declare class FailedError extends FailedError_base {
|
|
35
|
+
}
|
|
19
36
|
export declare const discover: (options?: Options | undefined) => Effect.Effect<{
|
|
20
37
|
url: string;
|
|
21
38
|
auth: {
|
|
@@ -24,16 +41,15 @@ export declare const discover: (options?: Options | undefined) => Effect.Effect<
|
|
|
24
41
|
password: string;
|
|
25
42
|
} | undefined;
|
|
26
43
|
} | undefined, never, FileSystem.FileSystem>;
|
|
27
|
-
export declare const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
export declare function headers(endpoint: Endpoint): RequestInit["headers"];
|
|
44
|
+
export declare const status: (options?: Options | undefined) => Effect.Effect<Status, never, FileSystem.FileSystem>;
|
|
45
|
+
export declare const start: (options?: StartOptions | undefined) => Effect.Effect<Endpoint, Error, FileSystem.FileSystem>;
|
|
46
|
+
export type StopMetadata = {
|
|
47
|
+
readonly targetVersion?: string;
|
|
48
|
+
};
|
|
49
|
+
export declare const stop: (options?: Options | undefined, metadata?: StopMetadata | undefined) => Effect.Effect<void, Error, FileSystem.FileSystem>;
|
|
50
|
+
export declare function headers(endpoint: Endpoint): {
|
|
51
|
+
authorization: string;
|
|
52
|
+
} | undefined;
|
|
37
53
|
export declare const Info: Schema.Struct<{
|
|
38
54
|
readonly id: Schema.optional<Schema.String>;
|
|
39
55
|
readonly version: Schema.optional<Schema.String>;
|
package/dist/effect/service.js
CHANGED
|
@@ -1,55 +1,149 @@
|
|
|
1
|
+
import { ServiceStatus } from "@opencode-ai/protocol/groups/health";
|
|
1
2
|
import { Effect, FileSystem, Option, Schedule, Schema } from "effect";
|
|
2
3
|
import { spawn } from "node:child_process";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
6
|
+
export class FailedError extends Schema.TaggedErrorClass()("ServiceFailedError", {
|
|
7
|
+
message: Schema.String,
|
|
8
|
+
action: Schema.String,
|
|
9
|
+
}) {
|
|
10
|
+
}
|
|
5
11
|
// Read-only lookup: registration file plus health check and version gate.
|
|
6
12
|
// Never spawns; escalation to start() is the caller's policy.
|
|
7
13
|
export const discover = Effect.fn("service.discover")(function* (options = {}) {
|
|
8
14
|
return (yield* discoverLocal(options))?.endpoint;
|
|
9
15
|
});
|
|
16
|
+
export const status = Effect.fn("service.status")(function* (options = {}) {
|
|
17
|
+
const result = yield* registered(options.file, true);
|
|
18
|
+
if (result.info === undefined)
|
|
19
|
+
return { type: "missing" };
|
|
20
|
+
if (result.service === undefined)
|
|
21
|
+
return { type: "unreachable" };
|
|
22
|
+
return publicStatus(result.service);
|
|
23
|
+
});
|
|
24
|
+
function publicStatus(service) {
|
|
25
|
+
return { ...service.status, version: service.version };
|
|
26
|
+
}
|
|
10
27
|
const discoverLocal = Effect.fnUntraced(function* (options) {
|
|
11
|
-
const
|
|
12
|
-
if (
|
|
28
|
+
const found = (yield* registered(options.file)).service;
|
|
29
|
+
if (found?.status.type !== "ready")
|
|
13
30
|
return undefined;
|
|
14
|
-
if (options.version !== undefined &&
|
|
31
|
+
if (options.version !== undefined && found.version !== options.version)
|
|
15
32
|
return undefined;
|
|
16
|
-
return
|
|
33
|
+
return found;
|
|
17
34
|
});
|
|
18
35
|
// Idempotent ensure-running: reuses a healthy compatible server, replaces a
|
|
19
|
-
// version-mismatched one, and otherwise spawns
|
|
36
|
+
// version-mismatched one, and otherwise spawns small contenders until a server
|
|
37
|
+
// becomes discoverable. A contender is never killed merely for slow startup.
|
|
20
38
|
export const start = Effect.fn("service.start")(function* (options = {}) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
const contenders = new Set();
|
|
40
|
+
let announced = false;
|
|
41
|
+
let reported;
|
|
42
|
+
let lastSpawn = 0;
|
|
43
|
+
let spawnDelay = 5_000;
|
|
44
|
+
let ownerHeld = false;
|
|
45
|
+
const announce = (reason, existing) => Effect.sync(() => {
|
|
46
|
+
if (announced)
|
|
47
|
+
return;
|
|
48
|
+
announced = true;
|
|
49
|
+
options.onStart?.(reason, existing);
|
|
50
|
+
});
|
|
51
|
+
const spawnContender = Effect.gen(function* () {
|
|
52
|
+
const [command, ...args] = options.command ?? ["opencode", "serve", "--service"];
|
|
53
|
+
if (command === undefined)
|
|
54
|
+
return yield* Effect.fail(new Error("Missing service command"));
|
|
55
|
+
return yield* Effect.try({
|
|
56
|
+
try: () => {
|
|
57
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
58
|
+
let error;
|
|
59
|
+
child.once("error", (cause) => {
|
|
60
|
+
error = new Error("Failed to start server", { cause });
|
|
61
|
+
});
|
|
62
|
+
child.unref();
|
|
63
|
+
return { child, error: () => error };
|
|
64
|
+
},
|
|
65
|
+
catch: (cause) => new Error("Failed to start server", { cause }),
|
|
66
|
+
});
|
|
40
67
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
const found = yield* Effect.gen(function* () {
|
|
69
|
+
const registration = yield* registered(options.file, true);
|
|
70
|
+
const info = registration.info;
|
|
71
|
+
const service = registration.service;
|
|
72
|
+
const current = service === undefined ? { type: info === undefined ? "missing" : "unreachable" } : publicStatus(service);
|
|
73
|
+
const next = ownerHeld && service === undefined ? { type: "unresponsive" } : current;
|
|
74
|
+
yield* Effect.sync(() => {
|
|
75
|
+
if (sameStatus(reported, next))
|
|
76
|
+
return;
|
|
77
|
+
reported = next;
|
|
78
|
+
options.onStatus?.(next);
|
|
79
|
+
});
|
|
80
|
+
if (service !== undefined) {
|
|
81
|
+
ownerHeld = false;
|
|
82
|
+
spawnDelay = 5_000;
|
|
83
|
+
const compatible = !service.legacy && (options.version === undefined || service.version === options.version);
|
|
84
|
+
if (compatible && service.status.type === "ready")
|
|
85
|
+
return Option.some(service);
|
|
86
|
+
if (compatible && service.status.type === "failed")
|
|
87
|
+
return yield* new FailedError({ message: service.status.message, action: service.status.action });
|
|
88
|
+
if (compatible || service.status.type === "stopping")
|
|
89
|
+
return Option.none();
|
|
90
|
+
yield* announce("version-mismatch", service.info);
|
|
91
|
+
yield* kill(service, options, options.version).pipe(Effect.ignore);
|
|
92
|
+
lastSpawn = 0;
|
|
93
|
+
return Option.none();
|
|
94
|
+
}
|
|
95
|
+
else if (lastSpawn === 0 && info !== undefined)
|
|
96
|
+
lastSpawn = Date.now();
|
|
97
|
+
const failure = [...contenders].map(contenderFailure).find((error) => error !== undefined);
|
|
98
|
+
if (failure !== undefined)
|
|
99
|
+
return yield* Effect.fail(failure);
|
|
100
|
+
const finished = [...contenders].filter(contenderFinished);
|
|
101
|
+
if (finished.some((item) => item.child.exitCode === 0)) {
|
|
102
|
+
ownerHeld = true;
|
|
103
|
+
spawnDelay = Math.min(spawnDelay * 2, 30_000);
|
|
104
|
+
}
|
|
105
|
+
finished.forEach((item) => contenders.delete(item));
|
|
106
|
+
// Keep one candidate plus one lock probe so a pre-lock stall cannot block recovery.
|
|
107
|
+
if (contenders.size < 2 && Date.now() - lastSpawn >= spawnDelay) {
|
|
108
|
+
yield* announce("missing");
|
|
109
|
+
contenders.add(yield* spawnContender);
|
|
110
|
+
lastSpawn = Date.now();
|
|
111
|
+
}
|
|
112
|
+
return Option.none();
|
|
113
|
+
}).pipe(Effect.repeat({ until: Option.isSome, schedule: Schedule.spaced("1 second") }));
|
|
114
|
+
return Option.getOrThrow(found).endpoint;
|
|
46
115
|
});
|
|
47
|
-
|
|
48
|
-
|
|
116
|
+
function sameStatus(left, right) {
|
|
117
|
+
if (left?.type !== right.type)
|
|
118
|
+
return false;
|
|
119
|
+
if (right.type === "failed")
|
|
120
|
+
return (left.type === "failed" &&
|
|
121
|
+
left.version === right.version &&
|
|
122
|
+
left.message === right.message &&
|
|
123
|
+
left.action === right.action);
|
|
124
|
+
if (right.type === "stopping")
|
|
125
|
+
return left.type === "stopping" && left.version === right.version && left.targetVersion === right.targetVersion;
|
|
126
|
+
if (right.type === "starting" || right.type === "ready")
|
|
127
|
+
return left.type === right.type && left.version === right.version;
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
function contenderFailure(contender) {
|
|
131
|
+
const error = contender.error();
|
|
132
|
+
if (error !== undefined)
|
|
133
|
+
return error;
|
|
134
|
+
if (contender.child.exitCode !== null && contender.child.exitCode !== 0)
|
|
135
|
+
return new Error(`Server process exited with code ${contender.child.exitCode}`);
|
|
136
|
+
if (contender.child.signalCode !== null)
|
|
137
|
+
return new Error(`Server process terminated by ${contender.child.signalCode}`);
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
function contenderFinished(contender) {
|
|
141
|
+
return contender.error() !== undefined || contender.child.exitCode !== null || contender.child.signalCode !== null;
|
|
142
|
+
}
|
|
143
|
+
export const stop = Effect.fn("service.stop")(function* (options = {}, metadata = {}) {
|
|
49
144
|
const existing = yield* find(options);
|
|
50
145
|
if (existing !== undefined)
|
|
51
|
-
yield* kill(existing
|
|
52
|
-
yield* fs.remove(options.file ?? fallback()).pipe(Effect.ignore);
|
|
146
|
+
yield* kill(existing, options, metadata.targetVersion);
|
|
53
147
|
});
|
|
54
148
|
function fallback() {
|
|
55
149
|
const state = process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state");
|
|
@@ -68,7 +162,7 @@ export const Info = Schema.Struct({
|
|
|
68
162
|
password: Schema.optional(Schema.String),
|
|
69
163
|
});
|
|
70
164
|
const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Info));
|
|
71
|
-
const decodeHealth = Schema.decodeUnknownOption(
|
|
165
|
+
const decodeHealth = Schema.decodeUnknownOption(ServiceStatus.Health);
|
|
72
166
|
const decodeLegacyHealth = Schema.decodeUnknownOption(Schema.Struct({ healthy: Schema.Literal(true) }));
|
|
73
167
|
// A missing or corrupt file means no valid info; callers treat both
|
|
74
168
|
// the same (the registering server self-evicts, clients rediscover).
|
|
@@ -79,7 +173,7 @@ const read = Effect.fnUntraced(function* (file) {
|
|
|
79
173
|
return undefined;
|
|
80
174
|
return yield* decode(text.value).pipe(Effect.option, Effect.map(Option.getOrUndefined));
|
|
81
175
|
});
|
|
82
|
-
const probe = Effect.fnUntraced(function* (info,
|
|
176
|
+
const probe = Effect.fnUntraced(function* (info, allowLegacy = false) {
|
|
83
177
|
const endpoint = {
|
|
84
178
|
url: info.url,
|
|
85
179
|
auth: info.password === undefined
|
|
@@ -90,7 +184,7 @@ const probe = Effect.fnUntraced(function* (info, version, allowLegacy = false) {
|
|
|
90
184
|
headers: headers(endpoint),
|
|
91
185
|
signal: AbortSignal.timeout(2_000),
|
|
92
186
|
})).pipe(Effect.option, Effect.map(Option.getOrUndefined));
|
|
93
|
-
if (response === undefined
|
|
187
|
+
if (response === undefined)
|
|
94
188
|
return undefined;
|
|
95
189
|
const body = yield* Effect.tryPromise(() => response.json()).pipe(Effect.option, Effect.map(Option.getOrUndefined));
|
|
96
190
|
const health = decodeHealth(body);
|
|
@@ -99,25 +193,35 @@ const probe = Effect.fnUntraced(function* (info, version, allowLegacy = false) {
|
|
|
99
193
|
return undefined;
|
|
100
194
|
if (info.version !== undefined && health.value.version !== info.version)
|
|
101
195
|
return undefined;
|
|
102
|
-
if (
|
|
196
|
+
if (info.id !== undefined && health.value.instanceID !== undefined && health.value.instanceID !== info.id)
|
|
103
197
|
return undefined;
|
|
104
|
-
return {
|
|
198
|
+
return {
|
|
199
|
+
info,
|
|
200
|
+
endpoint,
|
|
201
|
+
version: health.value.version,
|
|
202
|
+
status: health.value.status,
|
|
203
|
+
legacy: false,
|
|
204
|
+
};
|
|
105
205
|
}
|
|
106
206
|
if (!allowLegacy ||
|
|
107
207
|
Option.isNone(decodeLegacyHealth(body)) ||
|
|
108
208
|
(typeof body === "object" && body !== null && ("version" in body || "pid" in body)))
|
|
109
209
|
return undefined;
|
|
110
|
-
return { info, endpoint };
|
|
210
|
+
return { info, endpoint, status: { type: "ready" }, legacy: true };
|
|
211
|
+
});
|
|
212
|
+
const registered = Effect.fnUntraced(function* (file, allowLegacy = false) {
|
|
213
|
+
const info = yield* read(file);
|
|
214
|
+
if (info === undefined)
|
|
215
|
+
return { info: undefined, service: undefined };
|
|
216
|
+
return { info, service: yield* probe(info, allowLegacy) };
|
|
111
217
|
});
|
|
112
|
-
// Health-checked lookup without the version gate:
|
|
218
|
+
// Health-checked lookup without the version gate: status operations must be
|
|
113
219
|
// able to see (and replace or stop) a server from a different version.
|
|
114
220
|
const find = Effect.fnUntraced(function* (options) {
|
|
115
|
-
|
|
116
|
-
if (info === undefined)
|
|
117
|
-
return undefined;
|
|
118
|
-
return yield* probe(info, undefined, true);
|
|
221
|
+
return (yield* registered(options.file, true)).service;
|
|
119
222
|
});
|
|
120
|
-
// 50ms cadence bounded at ~5s, shared by stop escalation and start
|
|
223
|
+
// 50ms cadence bounded at ~5s, shared by stop escalation and each start
|
|
224
|
+
// discovery window.
|
|
121
225
|
const poll = Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)));
|
|
122
226
|
const signal = (pid, name) => Effect.try({ try: () => process.kill(pid, name), catch: (cause) => cause }).pipe(Effect.ignore);
|
|
123
227
|
const stopped = Effect.fnUntraced(function* (pid) {
|
|
@@ -129,20 +233,43 @@ const stopped = Effect.fnUntraced(function* (pid) {
|
|
|
129
233
|
function same(left, right) {
|
|
130
234
|
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid;
|
|
131
235
|
}
|
|
132
|
-
const kill = Effect.fnUntraced(function* (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const current = yield* find(options);
|
|
136
|
-
if (current === undefined || !same(current.info, info))
|
|
236
|
+
const kill = Effect.fnUntraced(function* (service, options, targetVersion) {
|
|
237
|
+
const requested = yield* requestStop(service, targetVersion);
|
|
238
|
+
if (requested === "rejected")
|
|
137
239
|
return;
|
|
138
|
-
|
|
139
|
-
|
|
240
|
+
if (requested === "unsupported") {
|
|
241
|
+
// A stale registration may point at a reused PID. Authenticate again
|
|
242
|
+
// immediately before the legacy signal fallback.
|
|
243
|
+
const current = yield* find(options);
|
|
244
|
+
if (current === undefined || !same(current.info, service.info))
|
|
245
|
+
return;
|
|
246
|
+
yield* signal(service.info.pid, "SIGTERM");
|
|
247
|
+
}
|
|
248
|
+
const done = yield* stopped(service.info.pid).pipe(Effect.retry(poll), Effect.option);
|
|
140
249
|
if (Option.isSome(done))
|
|
141
250
|
return;
|
|
142
251
|
const latest = yield* find(options);
|
|
143
|
-
if (latest === undefined || !same(latest.info, info))
|
|
252
|
+
if (latest === undefined || !same(latest.info, service.info))
|
|
144
253
|
return;
|
|
145
|
-
yield* signal(info.pid, "SIGKILL");
|
|
146
|
-
yield* stopped(info.pid).pipe(Effect.retry(poll));
|
|
254
|
+
yield* signal(service.info.pid, "SIGKILL");
|
|
255
|
+
yield* stopped(service.info.pid).pipe(Effect.retry(poll));
|
|
256
|
+
});
|
|
257
|
+
const decodeStopResponse = Schema.decodeUnknownOption(ServiceStatus.StopResponse);
|
|
258
|
+
const requestStop = Effect.fnUntraced(function* (service, targetVersion) {
|
|
259
|
+
if (service.info.id === undefined || service.legacy)
|
|
260
|
+
return "unsupported";
|
|
261
|
+
const response = yield* Effect.tryPromise(() => fetch(new URL("/api/service/stop", service.info.url), {
|
|
262
|
+
method: "POST",
|
|
263
|
+
headers: { ...headers(service.endpoint), "content-type": "application/json" },
|
|
264
|
+
body: JSON.stringify({ instanceID: service.info.id, targetVersion }),
|
|
265
|
+
signal: AbortSignal.timeout(2_000),
|
|
266
|
+
})).pipe(Effect.option, Effect.map(Option.getOrUndefined));
|
|
267
|
+
if (response === undefined || response.status === 404 || response.status === 405)
|
|
268
|
+
return "unsupported";
|
|
269
|
+
const body = yield* Effect.tryPromise(() => response.json()).pipe(Effect.option, Effect.map(Option.getOrUndefined));
|
|
270
|
+
const decoded = decodeStopResponse(body);
|
|
271
|
+
if (!response.ok || Option.isNone(decoded) || !decoded.value.accepted)
|
|
272
|
+
return "rejected";
|
|
273
|
+
return "accepted";
|
|
147
274
|
});
|
|
148
275
|
export * as Service from "./service.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HealthStopInput, ServerGetOutput, LocationGetInput, LocationGetOutput, AgentListInput, AgentListOutput, PluginListInput, PluginListOutput, SessionListInput, SessionCreateInput, SessionGetInput, SessionRemoveInput, SessionForkInput, SessionSwitchAgentInput, SessionSwitchModelInput, SessionRenameInput, SessionMoveInput, SessionPromptInput, SessionCommandInput, SessionSkillInput, SessionSyntheticInput, SessionShellInput, SessionCompactInput, SessionWaitInput, SessionRevertStageInput, SessionRevertClearInput, SessionRevertCommitInput, SessionContextInput, SessionPendingListInput, SessionInstructionsEntryListInput, SessionInstructionsEntryPutInput, SessionInstructionsEntryRemoveInput, SessionLogInput, SessionLogOutput, SessionInterruptInput, SessionBackgroundInput, SessionMessageInput, MessageListInput, ModelListInput, ModelListOutput, ModelDefaultInput, ModelDefaultOutput, GenerateTextInput, ProviderListInput, ProviderListOutput, ProviderGetInput, ProviderGetOutput, IntegrationListInput, IntegrationListOutput, IntegrationGetInput, IntegrationGetOutput, IntegrationConnectKeyInput, IntegrationConnectOauthInput, IntegrationConnectOauthOutput, IntegrationAttemptStatusInput, IntegrationAttemptStatusOutput, IntegrationAttemptCompleteInput, IntegrationAttemptCancelInput, McpListInput, McpListOutput, McpResourceCatalogInput, McpResourceCatalogOutput, CredentialUpdateInput, CredentialRemoveInput, ProjectListOutput, ProjectCurrentInput, ProjectDirectoriesInput, FormRequestListInput, FormRequestListOutput, FormListInput, FormCreateInput, FormGetInput, FormStateInput, FormReplyInput, FormCancelInput, PermissionRequestListInput, PermissionRequestListOutput, PermissionSavedListInput, PermissionSavedRemoveInput, PermissionCreateInput, PermissionListInput, PermissionGetInput, PermissionReplyInput, FileReadInput, FileReadOutput, FileListInput, FileListOutput, FileFindInput, FileFindOutput, CommandListInput, CommandListOutput, SkillListInput, SkillListOutput, EventSubscribeOutput, PtyListInput, PtyListOutput, PtyCreateInput, PtyCreateOutput, PtyGetInput, PtyGetOutput, PtyUpdateInput, PtyUpdateOutput, PtyRemoveInput, ShellListInput, ShellListOutput, ShellCreateInput, ShellCreateOutput, ShellGetInput, ShellGetOutput, ShellTimeoutInput, ShellTimeoutOutput, ShellOutputInput, ShellOutputOutput, ShellRemoveInput, QuestionRequestListInput, QuestionRequestListOutput, QuestionListInput, QuestionReplyInput, QuestionRejectInput, ReferenceListInput, ReferenceListOutput, ProjectCopyCreateInput, ProjectCopyRemoveInput, ProjectCopyRefreshInput, VcsStatusInput, VcsStatusOutput, VcsDiffInput, VcsDiffOutput, DebugLocationListOutput, DebugLocationEvictInput } from "./types";
|
|
2
2
|
export interface ClientOptions {
|
|
3
3
|
readonly baseUrl: string;
|
|
4
4
|
readonly fetch?: typeof globalThis.fetch;
|
|
@@ -10,7 +10,8 @@ export interface RequestOptions {
|
|
|
10
10
|
}
|
|
11
11
|
export declare function make(options: ClientOptions): {
|
|
12
12
|
health: {
|
|
13
|
-
get: (requestOptions?: RequestOptions) => Promise<
|
|
13
|
+
get: (requestOptions?: RequestOptions) => Promise<import("./types").ServiceHealth>;
|
|
14
|
+
stop: (input: HealthStopInput, requestOptions?: RequestOptions) => Promise<import("./types").ServiceStopResponse>;
|
|
14
15
|
};
|
|
15
16
|
server: {
|
|
16
17
|
get: (requestOptions?: RequestOptions) => Promise<ServerGetOutput>;
|
|
@@ -129,6 +129,14 @@ export function make(options) {
|
|
|
129
129
|
return {
|
|
130
130
|
health: {
|
|
131
131
|
get: (requestOptions) => request({ method: "GET", path: `/api/health`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, requestOptions),
|
|
132
|
+
stop: (input, requestOptions) => request({
|
|
133
|
+
method: "POST",
|
|
134
|
+
path: `/api/service/stop`,
|
|
135
|
+
body: { instanceID: input["instanceID"], targetVersion: input["targetVersion"] },
|
|
136
|
+
successStatus: 200,
|
|
137
|
+
declaredStatuses: [401, 400],
|
|
138
|
+
empty: false,
|
|
139
|
+
}, requestOptions),
|
|
132
140
|
},
|
|
133
141
|
server: {
|
|
134
142
|
get: (requestOptions) => request({ method: "GET", path: `/api/server`, successStatus: 200, declaredStatuses: [401, 400], empty: false }, requestOptions),
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
export type JsonValue = null | boolean | number | string | Array<JsonValue> | {
|
|
2
2
|
[key: string]: JsonValue;
|
|
3
3
|
};
|
|
4
|
+
export type ServiceStatus = {
|
|
5
|
+
type: "starting";
|
|
6
|
+
} | {
|
|
7
|
+
type: "ready";
|
|
8
|
+
} | {
|
|
9
|
+
type: "stopping";
|
|
10
|
+
targetVersion?: string | null;
|
|
11
|
+
} | {
|
|
12
|
+
type: "failed";
|
|
13
|
+
message: string;
|
|
14
|
+
action: string;
|
|
15
|
+
};
|
|
16
|
+
export type ServiceStopResponse = {
|
|
17
|
+
accepted: boolean;
|
|
18
|
+
};
|
|
4
19
|
export type ModelRef = {
|
|
5
20
|
id: string;
|
|
6
21
|
providerID: string;
|
|
@@ -737,6 +752,13 @@ export type VcsFileStatus = {
|
|
|
737
752
|
deletions: number;
|
|
738
753
|
status: "added" | "deleted" | "modified";
|
|
739
754
|
};
|
|
755
|
+
export type ServiceHealth = {
|
|
756
|
+
healthy: true;
|
|
757
|
+
version: string;
|
|
758
|
+
pid: number;
|
|
759
|
+
instanceID?: string | null;
|
|
760
|
+
status?: ServiceStatus;
|
|
761
|
+
};
|
|
740
762
|
export type SessionMessageModelSelected = {
|
|
741
763
|
id: string;
|
|
742
764
|
metadata?: {
|
|
@@ -3151,11 +3173,18 @@ export type ProjectCopyError = {
|
|
|
3151
3173
|
};
|
|
3152
3174
|
};
|
|
3153
3175
|
export declare const isProjectCopyError: (value: unknown) => value is ProjectCopyError;
|
|
3154
|
-
export type HealthGetOutput =
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3176
|
+
export type HealthGetOutput = ServiceHealth;
|
|
3177
|
+
export type HealthStopInput = {
|
|
3178
|
+
readonly instanceID: {
|
|
3179
|
+
readonly instanceID: string;
|
|
3180
|
+
readonly targetVersion?: string | undefined;
|
|
3181
|
+
}["instanceID"];
|
|
3182
|
+
readonly targetVersion?: {
|
|
3183
|
+
readonly instanceID: string;
|
|
3184
|
+
readonly targetVersion?: string | undefined;
|
|
3185
|
+
}["targetVersion"];
|
|
3186
|
+
};
|
|
3187
|
+
export type HealthStopOutput = ServiceStopResponse;
|
|
3159
3188
|
export type ServerGetOutput = {
|
|
3160
3189
|
urls: Array<string>;
|
|
3161
3190
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@opencode-ai/client",
|
|
4
|
-
"version": "0.0.0-next-
|
|
4
|
+
"version": "0.0.0-next-15555",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"typecheck": "tsgo --noEmit"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@opencode-ai/schema": "0.0.0-next-
|
|
49
|
-
"@opencode-ai/protocol": "0.0.0-next-
|
|
48
|
+
"@opencode-ai/schema": "0.0.0-next-15555",
|
|
49
|
+
"@opencode-ai/protocol": "0.0.0-next-15555"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"effect": "4.0.0-beta.83"
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@effect/platform-node": "4.0.0-beta.83",
|
|
61
|
-
"@opencode-ai/httpapi-codegen": "0.0.0-next-
|
|
61
|
+
"@opencode-ai/httpapi-codegen": "0.0.0-next-15555",
|
|
62
62
|
"@tsconfig/bun": "1.0.9",
|
|
63
63
|
"@types/bun": "1.3.13",
|
|
64
64
|
"@typescript/native-preview": "7.0.0-dev.20251207.1",
|