@opencode-ai/client 0.0.0-next-14962 → 0.0.0-next-14971

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.
@@ -471,6 +471,8 @@ export declare const make: (options?: {
471
471
  health: {
472
472
  get: () => Effect.Effect<{
473
473
  readonly healthy: true;
474
+ readonly version: string;
475
+ readonly pid: number;
474
476
  }, import("@opencode-ai/protocol/errors").InvalidRequestError | Schema.SchemaError | import("@opencode-ai/protocol/errors").UnauthorizedError | HttpClientError.HttpClientError | ClientError, never>;
475
477
  };
476
478
  location: {
@@ -5,13 +5,15 @@ import { join } from "node:path";
5
5
  // Read-only lookup: registration file plus health check and version gate.
6
6
  // Never spawns; escalation to start() is the caller's policy.
7
7
  export const discover = Effect.fn("service.discover")(function* (options = {}) {
8
+ return (yield* discoverLocal(options))?.transport;
9
+ });
10
+ const discoverLocal = Effect.fnUntraced(function* (options) {
8
11
  const info = yield* read(options.file);
9
12
  if (info === undefined)
10
13
  return undefined;
11
14
  if (options.version !== undefined && info.version !== options.version)
12
15
  return undefined;
13
- const found = yield* probe(info);
14
- return found?.transport;
16
+ return yield* probe(info, options.version);
15
17
  });
16
18
  // Idempotent ensure-running: reuses a healthy compatible server, replaces a
17
19
  // version-mismatched one, and otherwise spawns the service command detached.
@@ -25,13 +27,19 @@ export const start = Effect.fn("service.start")(function* (options = {}) {
25
27
  const [command, ...args] = options.command ?? ["opencode", "serve", "--service"];
26
28
  if (command === undefined)
27
29
  return yield* Effect.fail(new Error("Missing service command"));
28
- yield* Effect.try({
30
+ const child = yield* Effect.try({
29
31
  try: () => {
30
- spawn(command, args, { detached: true, stdio: "ignore" }).unref();
32
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
33
+ child.unref();
34
+ return child;
31
35
  },
32
36
  catch: (cause) => new Error("Failed to start server", { cause }),
33
37
  });
34
- return yield* discover(options).pipe(Effect.flatMap((found) => found === undefined ? Effect.fail(new Error("Server is not ready")) : Effect.succeed(found)), Effect.retry(poll), Effect.mapError(() => new Error("Failed to start server")));
38
+ return yield* discoverLocal(options).pipe(Effect.flatMap((found) => found === undefined ? Effect.fail(new Error("Server is not ready")) : Effect.succeed(found)), Effect.retry(poll), Effect.tap((found) => found.info.pid === child.pid
39
+ ? Effect.void
40
+ : Effect.sync(() => {
41
+ child.kill("SIGTERM");
42
+ })), Effect.map((found) => found.transport), Effect.tapError(() => Effect.try({ try: () => child.kill("SIGTERM"), catch: () => undefined }).pipe(Effect.ignore)), Effect.mapError(() => new Error("Failed to start server")));
35
43
  });
36
44
  export const stop = Effect.fn("service.stop")(function* (options = {}) {
37
45
  const fs = yield* FileSystem.FileSystem;
@@ -55,6 +63,7 @@ export const Info = Schema.Struct({
55
63
  password: Schema.optional(Schema.String),
56
64
  });
57
65
  const decode = Schema.decodeUnknownEffect(Schema.fromJsonString(Info));
66
+ const decodeHealth = Schema.decodeUnknownEffect(Schema.Struct({ healthy: Schema.Literal(true), version: Schema.String, pid: Schema.Int }));
58
67
  // A missing or corrupt file means no valid info; callers treat both
59
68
  // the same (the registering server self-evicts, clients rediscover).
60
69
  const read = Effect.fnUntraced(function* (file) {
@@ -64,13 +73,20 @@ const read = Effect.fnUntraced(function* (file) {
64
73
  return undefined;
65
74
  return yield* decode(text.value).pipe(Effect.option, Effect.map(Option.getOrUndefined));
66
75
  });
67
- const probe = Effect.fnUntraced(function* (info) {
76
+ const probe = Effect.fnUntraced(function* (info, version) {
68
77
  const headers = info.password === undefined ? undefined : auth(info.password);
69
- const healthy = yield* Effect.tryPromise(() => fetch(new URL("/api/health", info.url), {
78
+ const response = yield* Effect.tryPromise(() => fetch(new URL("/api/health", info.url), {
70
79
  headers,
71
80
  signal: AbortSignal.timeout(2_000),
72
- })).pipe(Effect.map((response) => response.ok), Effect.orElseSucceed(() => false));
73
- if (!healthy)
81
+ })).pipe(Effect.option, Effect.map(Option.getOrUndefined));
82
+ if (response === undefined || !response.ok)
83
+ return undefined;
84
+ const health = yield* Effect.tryPromise(() => response.json()).pipe(Effect.flatMap(decodeHealth), Effect.option, Effect.map(Option.getOrUndefined));
85
+ if (health?.pid !== info.pid)
86
+ return undefined;
87
+ if (info.version !== undefined && health.version !== info.version)
88
+ return undefined;
89
+ if (version !== undefined && health.version !== version)
74
90
  return undefined;
75
91
  return { info, transport: { url: info.url, headers } };
76
92
  });
@@ -131,6 +131,8 @@ export type ProjectCopyError = {
131
131
  export declare const isProjectCopyError: (value: unknown) => value is ProjectCopyError;
132
132
  export type HealthGetOutput = {
133
133
  readonly healthy: true;
134
+ readonly version: string;
135
+ readonly pid: number;
134
136
  };
135
137
  export type LocationGetInput = {
136
138
  readonly location?: {
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-14962",
4
+ "version": "0.0.0-next-14971",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -41,8 +41,8 @@
41
41
  "typecheck": "tsgo --noEmit"
42
42
  },
43
43
  "dependencies": {
44
- "@opencode-ai/schema": "0.0.0-next-14962",
45
- "@opencode-ai/protocol": "0.0.0-next-14962"
44
+ "@opencode-ai/schema": "0.0.0-next-14971",
45
+ "@opencode-ai/protocol": "0.0.0-next-14971"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "effect": "4.0.0-beta.83"
@@ -54,7 +54,7 @@
54
54
  },
55
55
  "devDependencies": {
56
56
  "@effect/platform-node": "4.0.0-beta.83",
57
- "@opencode-ai/httpapi-codegen": "0.0.0-next-14962",
57
+ "@opencode-ai/httpapi-codegen": "0.0.0-next-14971",
58
58
  "@tsconfig/bun": "1.0.9",
59
59
  "@types/bun": "1.3.13",
60
60
  "@typescript/native-preview": "7.0.0-dev.20251207.1",