@opencode-cockpit/protocol 0.1.4 → 0.2.0
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/build.js +32 -0
- package/dist/contract.js +6 -0
- package/dist/daemon.js +51 -0
- package/dist/framing.js +27 -0
- package/dist/index.js +19 -0
- package/dist/paths.js +16 -0
- package/dist/rpc.js +53 -0
- package/dist/shell/common.js +13 -0
- package/dist/shell/contract.js +55 -0
- package/dist/shell/index.js +6 -0
- package/dist/shell/info.js +33 -0
- package/dist/shell/params.js +132 -0
- package/dist/shell/watch.js +37 -0
- package/package.json +11 -4
- package/types/build.d.ts +6 -0
- package/{src/contract.ts → types/contract.d.ts} +10 -17
- package/types/daemon.d.ts +80 -0
- package/types/framing.d.ts +10 -0
- package/types/index.d.ts +754 -0
- package/types/paths.d.ts +12 -0
- package/types/rpc.d.ts +63 -0
- package/types/shell/common.d.ts +16 -0
- package/types/shell/contract.d.ts +698 -0
- package/types/shell/index.d.ts +6 -0
- package/types/shell/info.d.ts +48 -0
- package/types/shell/params.d.ts +215 -0
- package/types/shell/watch.d.ts +46 -0
- package/src/build.ts +0 -32
- package/src/daemon.ts +0 -48
- package/src/framing.ts +0 -26
- package/src/index.ts +0 -21
- package/src/paths.ts +0 -25
- package/src/rpc.ts +0 -96
- package/src/shell.ts +0 -187
package/dist/build.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname, join, relative } from "node:path";
|
|
4
|
+
const SOURCE = /\.(ts|tsx|js|mjs|json)$/;
|
|
5
|
+
const SKIP = new Set(["node_modules", "test", "dist"]);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Identity of the daemon code that `entry` would run: a hash of every source file in the directory
|
|
9
|
+
* the entry lives in. Daemon and client compute it the same way, so a client can tell when a
|
|
10
|
+
* running daemon was started from different code.
|
|
11
|
+
*/
|
|
12
|
+
export function daemonBuildId(entry, version) {
|
|
13
|
+
const hash = createHash("sha256").update(version);
|
|
14
|
+
// A checkout runs from `src/`, a published install from `dist/`. Hash that whole directory so
|
|
15
|
+
// any change to the daemon's code yields a new id, and accept either the directory itself or a
|
|
16
|
+
// file inside it, so daemon and client always agree.
|
|
17
|
+
const root = statSync(entry).isDirectory() ? entry : dirname(entry);
|
|
18
|
+
const files = walk(root);
|
|
19
|
+
for (const file of files.sort()) {
|
|
20
|
+
hash.update(relative(root, file)).update("\0").update(readFileSync(file)).update("\0");
|
|
21
|
+
}
|
|
22
|
+
return `${version}+${hash.digest("hex").slice(0, 12)}`;
|
|
23
|
+
}
|
|
24
|
+
function walk(dir) {
|
|
25
|
+
const out = [];
|
|
26
|
+
for (const name of readdirSync(dir)) {
|
|
27
|
+
if (SKIP.has(name)) continue;
|
|
28
|
+
const path = join(dir, name);
|
|
29
|
+
if (statSync(path).isDirectory()) out.push(...walk(path));else if (SOURCE.test(name)) out.push(path);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
package/dist/contract.js
ADDED
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { method } from "./contract.js";
|
|
3
|
+
export const ClientInfo = z.object({
|
|
4
|
+
name: z.string().min(1),
|
|
5
|
+
version: z.string().min(1),
|
|
6
|
+
pid: z.number().int().optional()
|
|
7
|
+
});
|
|
8
|
+
export const Version = z.object({
|
|
9
|
+
major: z.number().int(),
|
|
10
|
+
minor: z.number().int()
|
|
11
|
+
});
|
|
12
|
+
export const HelloResult = z.object({
|
|
13
|
+
daemonVersion: z.string(),
|
|
14
|
+
/** Content identity of the running daemon code (see daemonBuildId). */
|
|
15
|
+
build: z.string().optional(),
|
|
16
|
+
protocol: Version,
|
|
17
|
+
modules: z.array(z.string()),
|
|
18
|
+
pid: z.number().int(),
|
|
19
|
+
startedAt: z.number()
|
|
20
|
+
});
|
|
21
|
+
export const StatusResult = z.object({
|
|
22
|
+
pid: z.number().int(),
|
|
23
|
+
uptimeMs: z.number(),
|
|
24
|
+
clients: z.number().int(),
|
|
25
|
+
modules: z.array(z.object({
|
|
26
|
+
name: z.string(),
|
|
27
|
+
busy: z.boolean()
|
|
28
|
+
}))
|
|
29
|
+
});
|
|
30
|
+
export const daemonContract = {
|
|
31
|
+
"daemon.hello": method(z.object({
|
|
32
|
+
client: ClientInfo,
|
|
33
|
+
protocol: Version
|
|
34
|
+
}), HelloResult),
|
|
35
|
+
"daemon.status": method(z.object({}).optional(), StatusResult),
|
|
36
|
+
"daemon.shutdown": method(z.object({
|
|
37
|
+
force: z.boolean().optional()
|
|
38
|
+
}).optional(), z.object({
|
|
39
|
+
accepted: z.boolean()
|
|
40
|
+
})),
|
|
41
|
+
"events.subscribe": method(z.object({
|
|
42
|
+
topics: z.array(z.string().min(1)).min(1)
|
|
43
|
+
}), z.object({
|
|
44
|
+
topics: z.array(z.string())
|
|
45
|
+
})),
|
|
46
|
+
"events.unsubscribe": method(z.object({
|
|
47
|
+
topics: z.array(z.string().min(1)).min(1)
|
|
48
|
+
}), z.object({
|
|
49
|
+
topics: z.array(z.string())
|
|
50
|
+
}))
|
|
51
|
+
};
|
package/dist/framing.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** NDJSON framing shared by daemon and client. */
|
|
2
|
+
|
|
3
|
+
const encoder = new TextEncoder();
|
|
4
|
+
export function encodeFrame(message) {
|
|
5
|
+
return encoder.encode(`${JSON.stringify(message)}\n`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Accumulates chunks and yields complete lines. Bounded to protect against runaway peers. */
|
|
9
|
+
export class LineDecoder {
|
|
10
|
+
decoder = new TextDecoder();
|
|
11
|
+
pending = "";
|
|
12
|
+
constructor(maxLineBytes = 16 * 1024 * 1024) {
|
|
13
|
+
this.maxLineBytes = maxLineBytes;
|
|
14
|
+
}
|
|
15
|
+
push(chunk) {
|
|
16
|
+
this.pending += this.decoder.decode(chunk, {
|
|
17
|
+
stream: true
|
|
18
|
+
});
|
|
19
|
+
const parts = this.pending.split("\n");
|
|
20
|
+
this.pending = parts.pop() ?? "";
|
|
21
|
+
if (this.pending.length > this.maxLineBytes) {
|
|
22
|
+
this.pending = "";
|
|
23
|
+
throw new Error(`frame exceeds ${this.maxLineBytes} bytes`);
|
|
24
|
+
}
|
|
25
|
+
return parts.filter(line => line.length > 0);
|
|
26
|
+
}
|
|
27
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { daemonContract } from "./daemon.js";
|
|
2
|
+
import { shellContract, shellEvents } from "./shell/index.js";
|
|
3
|
+
export * from "./build.js";
|
|
4
|
+
export * from "./contract.js";
|
|
5
|
+
export * from "./daemon.js";
|
|
6
|
+
export * from "./framing.js";
|
|
7
|
+
export * from "./paths.js";
|
|
8
|
+
export * from "./rpc.js";
|
|
9
|
+
export * as shell from "./shell/index.js";
|
|
10
|
+
|
|
11
|
+
/** Every method the daemon serves. Adding a module means spreading its contract here. */
|
|
12
|
+
export const contract = {
|
|
13
|
+
...daemonContract,
|
|
14
|
+
...shellContract
|
|
15
|
+
};
|
|
16
|
+
/** Every event topic the daemon emits. */
|
|
17
|
+
export const events = {
|
|
18
|
+
...shellEvents
|
|
19
|
+
};
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Filesystem layout shared by daemon and clients. Pure: creates nothing.
|
|
5
|
+
* Kept short because unix socket paths are limited to 104 bytes on macOS.
|
|
6
|
+
*/
|
|
7
|
+
export function resolvePaths(env = process.env) {
|
|
8
|
+
const home = env.COCKPIT_HOME ?? join(env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "opencode-cockpit");
|
|
9
|
+
return {
|
|
10
|
+
home,
|
|
11
|
+
socket: join(home, "cockpitd.sock"),
|
|
12
|
+
pidFile: join(home, "cockpitd.pid"),
|
|
13
|
+
lockFile: join(home, "spawn.lock"),
|
|
14
|
+
logFile: join(home, "cockpitd.log")
|
|
15
|
+
};
|
|
16
|
+
}
|
package/dist/rpc.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Wire envelope: JSON-RPC 2.0, one JSON object per line (ADR 0002). */
|
|
2
|
+
|
|
3
|
+
/** Bump MAJOR on breaking changes to methods, events or framing. */
|
|
4
|
+
export const PROTOCOL_VERSION = {
|
|
5
|
+
major: 1,
|
|
6
|
+
minor: 2
|
|
7
|
+
};
|
|
8
|
+
export const ErrorCode = {
|
|
9
|
+
ParseError: -32700,
|
|
10
|
+
InvalidRequest: -32600,
|
|
11
|
+
MethodNotFound: -32601,
|
|
12
|
+
InvalidParams: -32602,
|
|
13
|
+
InternalError: -32603,
|
|
14
|
+
// Application range
|
|
15
|
+
NotFound: -32001,
|
|
16
|
+
InvalidState: -32002,
|
|
17
|
+
ProtocolMismatch: -32003,
|
|
18
|
+
SpawnFailed: -32004,
|
|
19
|
+
ShuttingDown: -32005
|
|
20
|
+
};
|
|
21
|
+
export class RpcError extends Error {
|
|
22
|
+
constructor(code, message, data) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.data = data;
|
|
26
|
+
this.name = "RpcError";
|
|
27
|
+
}
|
|
28
|
+
toShape() {
|
|
29
|
+
return this.data === undefined ? {
|
|
30
|
+
code: this.code,
|
|
31
|
+
message: this.message
|
|
32
|
+
} : {
|
|
33
|
+
code: this.code,
|
|
34
|
+
message: this.message,
|
|
35
|
+
data: this.data
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
static from(shape) {
|
|
39
|
+
return new RpcError(shape.code, shape.message, shape.data);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Method name for daemon → client notifications. */
|
|
44
|
+
export const EVENT_METHOD = "event";
|
|
45
|
+
export function isRequest(msg) {
|
|
46
|
+
return "method" in msg && "id" in msg && msg.id !== undefined;
|
|
47
|
+
}
|
|
48
|
+
export function isNotification(msg) {
|
|
49
|
+
return "method" in msg && !("id" in msg);
|
|
50
|
+
}
|
|
51
|
+
export function isResponse(msg) {
|
|
52
|
+
return !("method" in msg) && ("result" in msg || "error" in msg);
|
|
53
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/** Identity and ownership, shared by every shell message. */
|
|
4
|
+
export const ShellId = z.string().regex(/^sh_[a-z2-7]{8}$/, "expected sh_ followed by 8 base32 chars");
|
|
5
|
+
export const Owner = z.object({
|
|
6
|
+
/** Absolute project directory the shell belongs to. */
|
|
7
|
+
project: z.string().min(1),
|
|
8
|
+
/** OpenCode session that started it, when started by an agent. */
|
|
9
|
+
session: z.string().min(1).optional(),
|
|
10
|
+
/** Opaque id of the client instance that started it; used to route notifications to one place. */
|
|
11
|
+
instance: z.string().min(1).optional()
|
|
12
|
+
});
|
|
13
|
+
export const ShellStatus = z.enum(["running", "exited", "killed", "failed"]);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { method } from "../contract.js";
|
|
3
|
+
import { ShellId } from "./common.js";
|
|
4
|
+
import { ShellInfo } from "./info.js";
|
|
5
|
+
import { AttachParams, ClearParams, IdParams, ListParams, ReadParams, ReadResult, ResizeParams, ScreenResult, StartParams, StopParams, WaitParams, WaitResult, WriteParams } from "./params.js";
|
|
6
|
+
import { WatchParams, WatchRule, WatchStatus } from "./watch.js";
|
|
7
|
+
export const shellContract = {
|
|
8
|
+
"shell.start": method(StartParams, ShellInfo),
|
|
9
|
+
"shell.list": method(ListParams, z.array(ShellInfo)),
|
|
10
|
+
"shell.get": method(IdParams, ShellInfo),
|
|
11
|
+
"shell.read": method(ReadParams, ReadResult),
|
|
12
|
+
"shell.screen": method(IdParams, ScreenResult),
|
|
13
|
+
"shell.write": method(WriteParams, z.object({
|
|
14
|
+
bytes: z.number().int()
|
|
15
|
+
})),
|
|
16
|
+
"shell.resize": method(ResizeParams, z.object({})),
|
|
17
|
+
"shell.wait": method(WaitParams, WaitResult),
|
|
18
|
+
"shell.stop": method(StopParams, ShellInfo),
|
|
19
|
+
"shell.restart": method(IdParams, ShellInfo),
|
|
20
|
+
"shell.remove": method(IdParams, z.object({})),
|
|
21
|
+
"shell.clear": method(ClearParams, z.object({
|
|
22
|
+
removed: z.array(ShellId)
|
|
23
|
+
})),
|
|
24
|
+
"shell.watch": method(WatchParams, ShellInfo),
|
|
25
|
+
"shell.unwatch": method(IdParams, ShellInfo),
|
|
26
|
+
"shell.presets": method(z.object({}).optional(), z.array(z.object({
|
|
27
|
+
name: z.string(),
|
|
28
|
+
match: z.string().optional(),
|
|
29
|
+
rule: WatchRule
|
|
30
|
+
}))),
|
|
31
|
+
"shell.attach": method(AttachParams, z.object({
|
|
32
|
+
offset: z.number().int(),
|
|
33
|
+
replay: z.string()
|
|
34
|
+
})),
|
|
35
|
+
"shell.detach": method(IdParams, z.object({}))
|
|
36
|
+
};
|
|
37
|
+
export const shellEvents = {
|
|
38
|
+
"shell.started": ShellInfo,
|
|
39
|
+
"shell.exited": ShellInfo,
|
|
40
|
+
"shell.removed": z.object({
|
|
41
|
+
id: ShellId
|
|
42
|
+
}),
|
|
43
|
+
"shell.output": z.object({
|
|
44
|
+
id: ShellId,
|
|
45
|
+
offset: z.number().int(),
|
|
46
|
+
data: z.string()
|
|
47
|
+
}),
|
|
48
|
+
/** Emitted only when a watcher's status changes, never per line. */
|
|
49
|
+
"shell.watch": z.object({
|
|
50
|
+
info: ShellInfo,
|
|
51
|
+
previous: WatchStatus,
|
|
52
|
+
current: WatchStatus,
|
|
53
|
+
summary: z.string().optional()
|
|
54
|
+
})
|
|
55
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Owner, ShellId, ShellStatus } from "./common.js";
|
|
3
|
+
import { WatchState } from "./watch.js";
|
|
4
|
+
export const ShellInfo = z.object({
|
|
5
|
+
id: ShellId,
|
|
6
|
+
title: z.string(),
|
|
7
|
+
command: z.string(),
|
|
8
|
+
args: z.array(z.string()),
|
|
9
|
+
cwd: z.string(),
|
|
10
|
+
owner: Owner,
|
|
11
|
+
status: ShellStatus,
|
|
12
|
+
run: z.number().int().positive(),
|
|
13
|
+
pid: z.number().int().optional(),
|
|
14
|
+
exitCode: z.number().int().optional(),
|
|
15
|
+
signal: z.string().optional(),
|
|
16
|
+
error: z.string().optional(),
|
|
17
|
+
/** Set when a run ends: the last error-looking line of the run, else its last line. */
|
|
18
|
+
summary: z.string().optional(),
|
|
19
|
+
startedAt: z.number(),
|
|
20
|
+
endedAt: z.number().optional(),
|
|
21
|
+
cols: z.number().int(),
|
|
22
|
+
rows: z.number().int(),
|
|
23
|
+
lines: z.object({
|
|
24
|
+
first: z.number().int(),
|
|
25
|
+
last: z.number().int()
|
|
26
|
+
}),
|
|
27
|
+
/** Absolute raw byte offset written so far (for UI attach/replay). */
|
|
28
|
+
bytes: z.number().int(),
|
|
29
|
+
/** Health reported by this shell's watcher, when one is attached. */
|
|
30
|
+
watch: WatchState.optional(),
|
|
31
|
+
/** File this shell's clean log is written to, when logging was requested. */
|
|
32
|
+
logFile: z.string().optional()
|
|
33
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Owner, ShellId, ShellStatus } from "./common.js";
|
|
3
|
+
import { ShellInfo } from "./info.js";
|
|
4
|
+
const Dimension = z.number().int().min(2).max(1000);
|
|
5
|
+
export const StartParams = z.object({
|
|
6
|
+
command: z.string().min(1),
|
|
7
|
+
args: z.array(z.string()).default([]),
|
|
8
|
+
cwd: z.string().min(1),
|
|
9
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
10
|
+
title: z.string().min(1).max(200).optional(),
|
|
11
|
+
cols: Dimension.default(120),
|
|
12
|
+
rows: Dimension.default(32),
|
|
13
|
+
owner: Owner,
|
|
14
|
+
/** Stop the shell automatically after this long, however busy it is. */
|
|
15
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
16
|
+
/**
|
|
17
|
+
* Stop the shell after this much silence. Never a default: a dev server is idle by definition,
|
|
18
|
+
* and killing one for being quiet would be wrong.
|
|
19
|
+
*/
|
|
20
|
+
idleTimeoutMs: z.number().int().positive().optional(),
|
|
21
|
+
/** Also write the clean log to a file, for debugging after the buffer has evicted old lines. */
|
|
22
|
+
logFile: z.boolean().default(false),
|
|
23
|
+
/**
|
|
24
|
+
* Restart a finished shell with the same command, args, cwd, project and session instead of
|
|
25
|
+
* creating a new one. Repeated runs then share one id and one log.
|
|
26
|
+
*/
|
|
27
|
+
reuse: z.boolean().default(false)
|
|
28
|
+
});
|
|
29
|
+
export const ClearParams = z.object({
|
|
30
|
+
owner: Owner.partial().optional(),
|
|
31
|
+
/** Only remove shells that finished at least this long ago. */
|
|
32
|
+
finishedBeforeMs: z.number().int().min(0).optional()
|
|
33
|
+
}).default({});
|
|
34
|
+
export const ListParams = z.object({
|
|
35
|
+
owner: Owner.partial().optional(),
|
|
36
|
+
includeExited: z.boolean().default(true)
|
|
37
|
+
}).default({
|
|
38
|
+
includeExited: true
|
|
39
|
+
});
|
|
40
|
+
export const IdParams = z.object({
|
|
41
|
+
id: ShellId
|
|
42
|
+
});
|
|
43
|
+
export const LogLine = z.object({
|
|
44
|
+
n: z.number().int(),
|
|
45
|
+
text: z.string()
|
|
46
|
+
});
|
|
47
|
+
export const ReadParams = z.object({
|
|
48
|
+
id: ShellId,
|
|
49
|
+
/** Cursor: return only lines numbered strictly greater than this. */
|
|
50
|
+
after: z.number().int().min(0).optional(),
|
|
51
|
+
/** When no cursor is given, return the last N lines. */
|
|
52
|
+
tail: z.number().int().positive().max(10_000).default(100),
|
|
53
|
+
limit: z.number().int().positive().max(10_000).default(500),
|
|
54
|
+
grep: z.string().min(1).max(500).optional(),
|
|
55
|
+
ignoreCase: z.boolean().default(false)
|
|
56
|
+
});
|
|
57
|
+
export const ReadResult = z.object({
|
|
58
|
+
lines: z.array(LogLine),
|
|
59
|
+
firstLine: z.number().int(),
|
|
60
|
+
lastLine: z.number().int(),
|
|
61
|
+
/** Pass as `after` to continue. */
|
|
62
|
+
nextCursor: z.number().int(),
|
|
63
|
+
/** True when requested lines were already evicted. */
|
|
64
|
+
truncated: z.boolean(),
|
|
65
|
+
/** True when more lines exist beyond `limit`. */
|
|
66
|
+
hasMore: z.boolean(),
|
|
67
|
+
status: ShellStatus
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/** A run of characters sharing one style, so a UI can repaint colour without parsing escapes. */
|
|
71
|
+
export const ScreenRun = z.object({
|
|
72
|
+
text: z.string(),
|
|
73
|
+
/** Resolved "#rrggbb"; absent means the viewer's default foreground. */
|
|
74
|
+
fg: z.string().optional(),
|
|
75
|
+
bg: z.string().optional(),
|
|
76
|
+
bold: z.boolean().optional(),
|
|
77
|
+
dim: z.boolean().optional(),
|
|
78
|
+
italic: z.boolean().optional(),
|
|
79
|
+
underline: z.boolean().optional()
|
|
80
|
+
});
|
|
81
|
+
export const ScreenResult = z.object({
|
|
82
|
+
text: z.string(),
|
|
83
|
+
cols: z.number().int(),
|
|
84
|
+
rows: z.number().int(),
|
|
85
|
+
cursor: z.object({
|
|
86
|
+
x: z.number().int(),
|
|
87
|
+
y: z.number().int()
|
|
88
|
+
}),
|
|
89
|
+
/** The same rows as `text`, carrying colour. */
|
|
90
|
+
styled: z.array(z.array(ScreenRun)).optional()
|
|
91
|
+
});
|
|
92
|
+
export const WriteParams = z.object({
|
|
93
|
+
id: ShellId,
|
|
94
|
+
data: z.string().max(1_000_000)
|
|
95
|
+
});
|
|
96
|
+
export const ResizeParams = z.object({
|
|
97
|
+
id: ShellId,
|
|
98
|
+
cols: Dimension,
|
|
99
|
+
rows: Dimension
|
|
100
|
+
});
|
|
101
|
+
export const WaitUntil = z.object({
|
|
102
|
+
pattern: z.string().min(1).max(500).optional(),
|
|
103
|
+
ignoreCase: z.boolean().optional(),
|
|
104
|
+
exit: z.boolean().optional(),
|
|
105
|
+
idleMs: z.number().int().positive().optional(),
|
|
106
|
+
port: z.number().int().min(1).max(65_535).optional(),
|
|
107
|
+
host: z.string().optional()
|
|
108
|
+
}).refine(u => u.pattern !== undefined || u.exit || u.idleMs !== undefined || u.port !== undefined, {
|
|
109
|
+
message: "until needs at least one of pattern, exit, idleMs, port"
|
|
110
|
+
});
|
|
111
|
+
export const WaitParams = z.object({
|
|
112
|
+
id: ShellId,
|
|
113
|
+
until: WaitUntil,
|
|
114
|
+
timeoutMs: z.number().int().positive().max(3_600_000),
|
|
115
|
+
/** Only consider lines after this cursor for `pattern`. Defaults to the current last line. */
|
|
116
|
+
after: z.number().int().min(0).optional()
|
|
117
|
+
});
|
|
118
|
+
export const WaitReason = z.enum(["pattern", "exit", "idle", "port", "timeout"]);
|
|
119
|
+
export const WaitResult = z.object({
|
|
120
|
+
reason: WaitReason,
|
|
121
|
+
match: LogLine.optional(),
|
|
122
|
+
info: ShellInfo
|
|
123
|
+
});
|
|
124
|
+
export const StopParams = z.object({
|
|
125
|
+
id: ShellId,
|
|
126
|
+
signal: z.enum(["SIGTERM", "SIGINT", "SIGHUP", "SIGKILL"]).default("SIGTERM"),
|
|
127
|
+
graceMs: z.number().int().min(0).max(60_000).default(3000)
|
|
128
|
+
});
|
|
129
|
+
export const AttachParams = z.object({
|
|
130
|
+
id: ShellId,
|
|
131
|
+
fromOffset: z.number().int().min(0).optional()
|
|
132
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ShellId } from "./common.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A watch rule is three regexes over a shell's log, not a parser: `done` marks the end of a run
|
|
6
|
+
* (a compile, a test pass), `fail` and `ok` say how it went. Presets are just named rules, so a
|
|
7
|
+
* new tool is a table entry or a rule in the caller's config — never new code.
|
|
8
|
+
*/
|
|
9
|
+
export const WatchRule = z.object({
|
|
10
|
+
/** A run finished, e.g. "Found 3 errors." Without it, `idleSeconds` ends the run. */
|
|
11
|
+
done: z.string().min(1).max(500).optional(),
|
|
12
|
+
/** Something in this run failed. */
|
|
13
|
+
fail: z.string().min(1).max(500).optional(),
|
|
14
|
+
/** This run was clean; beats `fail` only when `fail` never matched. */
|
|
15
|
+
ok: z.string().min(1).max(500).optional(),
|
|
16
|
+
ignoreCase: z.boolean().optional(),
|
|
17
|
+
/** Treat this much silence as the end of a run when `done` is absent. */
|
|
18
|
+
idleSeconds: z.number().positive().max(3600).optional()
|
|
19
|
+
});
|
|
20
|
+
export const WatchStatus = z.enum(["ok", "fail", "pending", "unknown"]);
|
|
21
|
+
export const WatchState = z.object({
|
|
22
|
+
/** Preset that produced the rule, when one was used. */
|
|
23
|
+
preset: z.string().optional(),
|
|
24
|
+
status: WatchStatus,
|
|
25
|
+
/** Line that decided the current status. */
|
|
26
|
+
summary: z.string().optional(),
|
|
27
|
+
/** Completed runs seen since watching started. */
|
|
28
|
+
runs: z.number().int(),
|
|
29
|
+
/** When the status last changed. */
|
|
30
|
+
since: z.number()
|
|
31
|
+
});
|
|
32
|
+
export const WatchParams = z.object({
|
|
33
|
+
id: ShellId,
|
|
34
|
+
/** Named rule, or "auto" to pick one from the command. Ignored when `rule` is given. */
|
|
35
|
+
preset: z.string().min(1).optional(),
|
|
36
|
+
rule: WatchRule.optional()
|
|
37
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencode-cockpit/protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Wire protocol, method and event contracts for cockpitd (opencode-cockpit)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,11 +20,18 @@
|
|
|
20
20
|
"protocol"
|
|
21
21
|
],
|
|
22
22
|
"exports": {
|
|
23
|
-
".":
|
|
24
|
-
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./types/index.d.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./shell": {
|
|
28
|
+
"types": "./types/shell/index.d.ts",
|
|
29
|
+
"default": "./dist/shell/index.js"
|
|
30
|
+
}
|
|
25
31
|
},
|
|
26
32
|
"files": [
|
|
27
|
-
"
|
|
33
|
+
"dist",
|
|
34
|
+
"types",
|
|
28
35
|
"README.md",
|
|
29
36
|
"LICENSE"
|
|
30
37
|
],
|
package/types/build.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity of the daemon code that `entry` would run: a hash of every source file in the directory
|
|
3
|
+
* the entry lives in. Daemon and client compute it the same way, so a client can tell when a
|
|
4
|
+
* running daemon was started from different code.
|
|
5
|
+
*/
|
|
6
|
+
export declare function daemonBuildId(entry: string, version: string): string;
|
|
@@ -1,20 +1,13 @@
|
|
|
1
|
-
import type { z } from "zod"
|
|
2
|
-
|
|
1
|
+
import type { z } from "zod";
|
|
3
2
|
export interface MethodSpec<P extends z.ZodType = z.ZodType, R extends z.ZodType = z.ZodType> {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
params: P;
|
|
4
|
+
result: R;
|
|
6
5
|
}
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export type Contract = Record<string, MethodSpec>
|
|
14
|
-
export type EventContract = Record<string, z.ZodType>
|
|
15
|
-
|
|
16
|
-
export type ParamsOf<C extends Contract, M extends keyof C> = z.input<C[M]["params"]>
|
|
17
|
-
export type ResultOf<C extends Contract, M extends keyof C> = z.output<C[M]["result"]>
|
|
18
|
-
export type EventOf<E extends EventContract, T extends keyof E> = z.output<E[T]>
|
|
6
|
+
export declare const method: <P extends z.ZodType, R extends z.ZodType>(params: P, result: R) => MethodSpec<P, R>;
|
|
7
|
+
export type Contract = Record<string, MethodSpec>;
|
|
8
|
+
export type EventContract = Record<string, z.ZodType>;
|
|
9
|
+
export type ParamsOf<C extends Contract, M extends keyof C> = z.input<C[M]["params"]>;
|
|
10
|
+
export type ResultOf<C extends Contract, M extends keyof C> = z.output<C[M]["result"]>;
|
|
11
|
+
export type EventOf<E extends EventContract, T extends keyof E> = z.output<E[T]>;
|
|
19
12
|
/** Params after schema parsing (defaults applied): what handlers receive. */
|
|
20
|
-
export type ParsedParamsOf<C extends Contract, M extends keyof C> = z.output<C[M]["params"]
|
|
13
|
+
export type ParsedParamsOf<C extends Contract, M extends keyof C> = z.output<C[M]["params"]>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const ClientInfo: z.ZodObject<{
|
|
3
|
+
name: z.ZodString;
|
|
4
|
+
version: z.ZodString;
|
|
5
|
+
pid: z.ZodOptional<z.ZodNumber>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export declare const Version: z.ZodObject<{
|
|
8
|
+
major: z.ZodNumber;
|
|
9
|
+
minor: z.ZodNumber;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
export declare const HelloResult: z.ZodObject<{
|
|
12
|
+
daemonVersion: z.ZodString;
|
|
13
|
+
build: z.ZodOptional<z.ZodString>;
|
|
14
|
+
protocol: z.ZodObject<{
|
|
15
|
+
major: z.ZodNumber;
|
|
16
|
+
minor: z.ZodNumber;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
modules: z.ZodArray<z.ZodString>;
|
|
19
|
+
pid: z.ZodNumber;
|
|
20
|
+
startedAt: z.ZodNumber;
|
|
21
|
+
}, z.core.$strip>;
|
|
22
|
+
export declare const StatusResult: z.ZodObject<{
|
|
23
|
+
pid: z.ZodNumber;
|
|
24
|
+
uptimeMs: z.ZodNumber;
|
|
25
|
+
clients: z.ZodNumber;
|
|
26
|
+
modules: z.ZodArray<z.ZodObject<{
|
|
27
|
+
name: z.ZodString;
|
|
28
|
+
busy: z.ZodBoolean;
|
|
29
|
+
}, z.core.$strip>>;
|
|
30
|
+
}, z.core.$strip>;
|
|
31
|
+
export declare const daemonContract: {
|
|
32
|
+
"daemon.hello": import("./contract.ts").MethodSpec<z.ZodObject<{
|
|
33
|
+
client: z.ZodObject<{
|
|
34
|
+
name: z.ZodString;
|
|
35
|
+
version: z.ZodString;
|
|
36
|
+
pid: z.ZodOptional<z.ZodNumber>;
|
|
37
|
+
}, z.core.$strip>;
|
|
38
|
+
protocol: z.ZodObject<{
|
|
39
|
+
major: z.ZodNumber;
|
|
40
|
+
minor: z.ZodNumber;
|
|
41
|
+
}, z.core.$strip>;
|
|
42
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
43
|
+
daemonVersion: z.ZodString;
|
|
44
|
+
build: z.ZodOptional<z.ZodString>;
|
|
45
|
+
protocol: z.ZodObject<{
|
|
46
|
+
major: z.ZodNumber;
|
|
47
|
+
minor: z.ZodNumber;
|
|
48
|
+
}, z.core.$strip>;
|
|
49
|
+
modules: z.ZodArray<z.ZodString>;
|
|
50
|
+
pid: z.ZodNumber;
|
|
51
|
+
startedAt: z.ZodNumber;
|
|
52
|
+
}, z.core.$strip>>;
|
|
53
|
+
"daemon.status": import("./contract.ts").MethodSpec<z.ZodOptional<z.ZodObject<{}, z.core.$strip>>, z.ZodObject<{
|
|
54
|
+
pid: z.ZodNumber;
|
|
55
|
+
uptimeMs: z.ZodNumber;
|
|
56
|
+
clients: z.ZodNumber;
|
|
57
|
+
modules: z.ZodArray<z.ZodObject<{
|
|
58
|
+
name: z.ZodString;
|
|
59
|
+
busy: z.ZodBoolean;
|
|
60
|
+
}, z.core.$strip>>;
|
|
61
|
+
}, z.core.$strip>>;
|
|
62
|
+
"daemon.shutdown": import("./contract.ts").MethodSpec<z.ZodOptional<z.ZodObject<{
|
|
63
|
+
force: z.ZodOptional<z.ZodBoolean>;
|
|
64
|
+
}, z.core.$strip>>, z.ZodObject<{
|
|
65
|
+
accepted: z.ZodBoolean;
|
|
66
|
+
}, z.core.$strip>>;
|
|
67
|
+
"events.subscribe": import("./contract.ts").MethodSpec<z.ZodObject<{
|
|
68
|
+
topics: z.ZodArray<z.ZodString>;
|
|
69
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
70
|
+
topics: z.ZodArray<z.ZodString>;
|
|
71
|
+
}, z.core.$strip>>;
|
|
72
|
+
"events.unsubscribe": import("./contract.ts").MethodSpec<z.ZodObject<{
|
|
73
|
+
topics: z.ZodArray<z.ZodString>;
|
|
74
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
75
|
+
topics: z.ZodArray<z.ZodString>;
|
|
76
|
+
}, z.core.$strip>>;
|
|
77
|
+
};
|
|
78
|
+
export type HelloResult = z.output<typeof HelloResult>;
|
|
79
|
+
export type StatusResult = z.output<typeof StatusResult>;
|
|
80
|
+
export type ClientInfo = z.output<typeof ClientInfo>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** NDJSON framing shared by daemon and client. */
|
|
2
|
+
export declare function encodeFrame(message: unknown): Uint8Array;
|
|
3
|
+
/** Accumulates chunks and yields complete lines. Bounded to protect against runaway peers. */
|
|
4
|
+
export declare class LineDecoder {
|
|
5
|
+
private readonly maxLineBytes;
|
|
6
|
+
private readonly decoder;
|
|
7
|
+
private pending;
|
|
8
|
+
constructor(maxLineBytes?: number);
|
|
9
|
+
push(chunk: Uint8Array): string[];
|
|
10
|
+
}
|