@opencode-cockpit/daemon 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/core/daemon.js +193 -0
- package/dist/core/errors.js +4 -0
- package/dist/core/logger.js +45 -0
- package/dist/core/module.js +1 -0
- package/dist/core/router.js +33 -0
- package/dist/core/server.js +211 -0
- package/dist/index.js +3 -0
- package/dist/main.js +41 -0
- package/dist/modules/index.js +5 -0
- package/dist/modules/shell/ids.js +7 -0
- package/dist/modules/shell/methods.js +186 -0
- package/dist/modules/shell/module.js +259 -0
- package/dist/modules/shell/output/line-log.js +76 -0
- package/dist/modules/shell/output/normalizer.js +161 -0
- package/dist/modules/shell/output/palette.js +19 -0
- package/dist/modules/shell/output/raw-ring.js +49 -0
- package/dist/modules/shell/output/screen.js +102 -0
- package/dist/modules/shell/port-probe.js +30 -0
- package/dist/modules/shell/pty.js +59 -0
- package/dist/modules/shell/registry.js +83 -0
- package/dist/modules/shell/shell.js +242 -0
- package/dist/modules/shell/wait.js +107 -0
- package/dist/modules/shell/watch/presets.js +299 -0
- package/dist/modules/shell/watch/watcher.js +82 -0
- package/package.json +12 -5
- package/types/core/daemon.d.ts +36 -0
- package/types/core/errors.d.ts +4 -0
- package/types/core/logger.d.ts +11 -0
- package/types/core/module.d.ts +37 -0
- package/types/core/router.d.ts +11 -0
- package/types/core/server.d.ts +49 -0
- package/{src/index.ts → types/index.d.ts} +5 -5
- package/types/main.d.ts +2 -0
- package/types/modules/index.d.ts +7 -0
- package/types/modules/shell/ids.d.ts +1 -0
- package/types/modules/shell/methods.d.ts +7 -0
- package/types/modules/shell/module.d.ts +68 -0
- package/types/modules/shell/output/line-log.d.ts +36 -0
- package/types/modules/shell/output/normalizer.d.ts +32 -0
- package/types/modules/shell/output/palette.d.ts +2 -0
- package/types/modules/shell/output/raw-ring.d.ts +19 -0
- package/types/modules/shell/output/screen.d.ts +12 -0
- package/types/modules/shell/port-probe.d.ts +2 -0
- package/types/modules/shell/pty.d.ts +33 -0
- package/types/modules/shell/registry.d.ts +17 -0
- package/types/modules/shell/shell.d.ts +89 -0
- package/types/modules/shell/wait.d.ts +12 -0
- package/types/modules/shell/watch/presets.d.ts +17 -0
- package/types/modules/shell/watch/watcher.d.ts +43 -0
- package/src/core/daemon.ts +0 -197
- package/src/core/errors.ts +0 -6
- package/src/core/logger.ts +0 -44
- package/src/core/module.ts +0 -45
- package/src/core/router.ts +0 -40
- package/src/core/server.ts +0 -223
- package/src/main.ts +0 -36
- package/src/modules/index.ts +0 -11
- package/src/modules/shell/ids.ts +0 -8
- package/src/modules/shell/module.ts +0 -323
- package/src/modules/shell/output/line-log.ts +0 -92
- package/src/modules/shell/output/normalizer.ts +0 -172
- package/src/modules/shell/output/raw-ring.ts +0 -46
- package/src/modules/shell/output/screen.ts +0 -44
- package/src/modules/shell/port-probe.ts +0 -30
- package/src/modules/shell/pty.ts +0 -98
- package/src/modules/shell/registry.ts +0 -86
- package/src/modules/shell/shell.ts +0 -252
- package/src/modules/shell/wait.ts +0 -91
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch rules for common tools, as data rather than parsers: `done` marks the end of a run, `fail`
|
|
3
|
+
* and `ok` say how it went. They are deliberately conservative — matching a tool's summary line,
|
|
4
|
+
* not its full output — and a caller can always pass its own rule instead. Adding a tool is a row
|
|
5
|
+
* here, and a wrong guess costs a status, never a crash.
|
|
6
|
+
*/
|
|
7
|
+
export const PRESETS = [
|
|
8
|
+
// TypeScript and linting
|
|
9
|
+
{
|
|
10
|
+
name: "tsc",
|
|
11
|
+
match: "\\btsc\\b",
|
|
12
|
+
rule: {
|
|
13
|
+
done: "Found \\d+ errors?|Watching for file changes",
|
|
14
|
+
fail: "error TS\\d+",
|
|
15
|
+
ok: "Found 0 errors"
|
|
16
|
+
}
|
|
17
|
+
}, {
|
|
18
|
+
name: "eslint",
|
|
19
|
+
match: "\\beslint\\b",
|
|
20
|
+
rule: {
|
|
21
|
+
done: "\\d+ problems?|Done in |^\\s*$",
|
|
22
|
+
fail: "\\d+ problems? \\(\\d*[1-9]\\d* error",
|
|
23
|
+
ok: "0 problems"
|
|
24
|
+
}
|
|
25
|
+
}, {
|
|
26
|
+
name: "biome",
|
|
27
|
+
match: "\\bbiome\\b",
|
|
28
|
+
rule: {
|
|
29
|
+
done: "Checked \\d+ file|Found \\d+ (?:error|warning)",
|
|
30
|
+
fail: "Found \\d+ errors?",
|
|
31
|
+
ok: "No fixes applied|Checked \\d+ files?"
|
|
32
|
+
}
|
|
33
|
+
}, {
|
|
34
|
+
name: "prettier",
|
|
35
|
+
match: "\\bprettier\\b",
|
|
36
|
+
rule: {
|
|
37
|
+
done: "\\d+ms$",
|
|
38
|
+
fail: "\\[error\\]",
|
|
39
|
+
ok: "\\(unchanged\\)"
|
|
40
|
+
}
|
|
41
|
+
}, {
|
|
42
|
+
name: "mypy",
|
|
43
|
+
match: "\\bmypy\\b",
|
|
44
|
+
rule: {
|
|
45
|
+
done: "Success: no issues|Found \\d+ errors?",
|
|
46
|
+
fail: "Found \\d+ errors?",
|
|
47
|
+
ok: "Success: no issues"
|
|
48
|
+
}
|
|
49
|
+
}, {
|
|
50
|
+
name: "ruff",
|
|
51
|
+
match: "\\bruff\\b",
|
|
52
|
+
rule: {
|
|
53
|
+
done: "Found \\d+ error|All checks passed",
|
|
54
|
+
fail: "Found \\d+ errors?",
|
|
55
|
+
ok: "All checks passed"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
// Test runners
|
|
59
|
+
{
|
|
60
|
+
name: "vitest",
|
|
61
|
+
match: "\\bvitest\\b",
|
|
62
|
+
rule: {
|
|
63
|
+
done: "Test Files\\s+\\d|Test Files\\s+\\w",
|
|
64
|
+
fail: "Test Files.*failed",
|
|
65
|
+
ok: "Test Files.*passed"
|
|
66
|
+
}
|
|
67
|
+
}, {
|
|
68
|
+
name: "jest",
|
|
69
|
+
match: "\\bjest\\b",
|
|
70
|
+
rule: {
|
|
71
|
+
done: "^Tests:\\s",
|
|
72
|
+
fail: "Tests:.*\\d+ failed",
|
|
73
|
+
ok: "Tests:.*passed"
|
|
74
|
+
}
|
|
75
|
+
}, {
|
|
76
|
+
name: "mocha",
|
|
77
|
+
match: "\\bmocha\\b",
|
|
78
|
+
rule: {
|
|
79
|
+
done: "\\d+ (?:passing|failing)",
|
|
80
|
+
fail: "[1-9]\\d* failing",
|
|
81
|
+
ok: "\\d+ passing"
|
|
82
|
+
}
|
|
83
|
+
}, {
|
|
84
|
+
name: "bun-test",
|
|
85
|
+
match: "bun\\s+(?:--\\S+\\s+)*test\\b",
|
|
86
|
+
rule: {
|
|
87
|
+
done: "Ran \\d+ tests?",
|
|
88
|
+
fail: "[1-9]\\d* fail",
|
|
89
|
+
ok: "\\b0 fail"
|
|
90
|
+
}
|
|
91
|
+
}, {
|
|
92
|
+
name: "deno-test",
|
|
93
|
+
match: "deno\\s+test\\b",
|
|
94
|
+
rule: {
|
|
95
|
+
done: "test result:",
|
|
96
|
+
fail: "FAILED",
|
|
97
|
+
ok: "test result: ok"
|
|
98
|
+
}
|
|
99
|
+
}, {
|
|
100
|
+
name: "pytest",
|
|
101
|
+
match: "\\bpytest\\b",
|
|
102
|
+
rule: {
|
|
103
|
+
done: "=+ .*(?:passed|failed|error|no tests ran).* =+",
|
|
104
|
+
fail: "\\d+ (?:failed|error)",
|
|
105
|
+
ok: "\\d+ passed"
|
|
106
|
+
}
|
|
107
|
+
}, {
|
|
108
|
+
name: "rspec",
|
|
109
|
+
match: "\\brspec\\b",
|
|
110
|
+
rule: {
|
|
111
|
+
done: "\\d+ examples?, \\d+ failures?",
|
|
112
|
+
fail: "[1-9]\\d* failures?",
|
|
113
|
+
ok: " 0 failures"
|
|
114
|
+
}
|
|
115
|
+
}, {
|
|
116
|
+
name: "phpunit",
|
|
117
|
+
match: "phpunit",
|
|
118
|
+
rule: {
|
|
119
|
+
done: "^OK \\(|FAILURES!|ERRORS!",
|
|
120
|
+
fail: "FAILURES!|ERRORS!",
|
|
121
|
+
ok: "^OK \\("
|
|
122
|
+
}
|
|
123
|
+
}, {
|
|
124
|
+
name: "playwright",
|
|
125
|
+
match: "playwright\\s+test",
|
|
126
|
+
rule: {
|
|
127
|
+
done: "\\d+ (?:passed|failed)",
|
|
128
|
+
fail: "[1-9]\\d* failed",
|
|
129
|
+
ok: "\\d+ passed"
|
|
130
|
+
}
|
|
131
|
+
}, {
|
|
132
|
+
name: "cypress",
|
|
133
|
+
match: "\\bcypress\\b",
|
|
134
|
+
rule: {
|
|
135
|
+
done: "All specs passed|\\d+ of \\d+ failed",
|
|
136
|
+
fail: "\\d+ of \\d+ failed",
|
|
137
|
+
ok: "All specs passed"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
// Dev servers and bundlers
|
|
141
|
+
{
|
|
142
|
+
name: "vite",
|
|
143
|
+
match: "\\bvite\\b",
|
|
144
|
+
rule: {
|
|
145
|
+
done: "ready in|page reload|hmr update|built in",
|
|
146
|
+
fail: "Internal server error|Rollup failed|error during build",
|
|
147
|
+
ok: "ready in|built in"
|
|
148
|
+
}
|
|
149
|
+
}, {
|
|
150
|
+
name: "next",
|
|
151
|
+
match: "\\bnext\\s+(?:dev|build|start)",
|
|
152
|
+
rule: {
|
|
153
|
+
done: "Compiled|Ready in|Creating an optimized",
|
|
154
|
+
fail: "Failed to compile|⨯",
|
|
155
|
+
ok: "Compiled successfully|✓ Compiled|Ready in"
|
|
156
|
+
}
|
|
157
|
+
}, {
|
|
158
|
+
name: "nuxt",
|
|
159
|
+
match: "\\bnuxt\\b",
|
|
160
|
+
rule: {
|
|
161
|
+
done: "Nuxt .*ready|built in|✔ (?:Client|Server)",
|
|
162
|
+
fail: "ERROR|✖",
|
|
163
|
+
ok: "ready in|built in"
|
|
164
|
+
}
|
|
165
|
+
}, {
|
|
166
|
+
name: "astro",
|
|
167
|
+
match: "\\bastro\\b",
|
|
168
|
+
rule: {
|
|
169
|
+
done: "watching for file changes|Complete!|ready in",
|
|
170
|
+
fail: "error",
|
|
171
|
+
ok: "Complete!|ready in",
|
|
172
|
+
ignoreCase: true
|
|
173
|
+
}
|
|
174
|
+
}, {
|
|
175
|
+
name: "angular",
|
|
176
|
+
match: "\\bng\\s+(?:serve|build|test)",
|
|
177
|
+
rule: {
|
|
178
|
+
done: "Application bundle generation complete|Compiled successfully|Build at",
|
|
179
|
+
fail: "Error:|ERROR",
|
|
180
|
+
ok: "Compiled successfully|generation complete"
|
|
181
|
+
}
|
|
182
|
+
}, {
|
|
183
|
+
name: "webpack",
|
|
184
|
+
match: "\\b(?:webpack|rspack)\\b",
|
|
185
|
+
rule: {
|
|
186
|
+
done: "compiled|webpack \\d",
|
|
187
|
+
fail: "ERROR in|compiled with \\d+ error",
|
|
188
|
+
ok: "compiled successfully"
|
|
189
|
+
}
|
|
190
|
+
}, {
|
|
191
|
+
name: "esbuild",
|
|
192
|
+
match: "\\besbuild\\b",
|
|
193
|
+
rule: {
|
|
194
|
+
done: "build finished|Done in",
|
|
195
|
+
fail: "✘ \\[ERROR\\]",
|
|
196
|
+
ok: "build finished"
|
|
197
|
+
}
|
|
198
|
+
}, {
|
|
199
|
+
name: "tsup",
|
|
200
|
+
match: "\\btsup\\b",
|
|
201
|
+
rule: {
|
|
202
|
+
done: "Build success|⚡️ Build",
|
|
203
|
+
fail: "error",
|
|
204
|
+
ok: "Build success",
|
|
205
|
+
ignoreCase: true
|
|
206
|
+
}
|
|
207
|
+
}, {
|
|
208
|
+
name: "turbo",
|
|
209
|
+
match: "\\bturbo\\b",
|
|
210
|
+
rule: {
|
|
211
|
+
done: "Tasks:\\s+\\d+ successful",
|
|
212
|
+
fail: "ERROR run failed|Tasks:.*\\d+ failed",
|
|
213
|
+
ok: "Tasks:\\s+\\d+ successful"
|
|
214
|
+
}
|
|
215
|
+
}, {
|
|
216
|
+
name: "metro",
|
|
217
|
+
match: "expo\\s+start|react-native\\s+start|\\bmetro\\b",
|
|
218
|
+
rule: {
|
|
219
|
+
done: "Bundled|BUNDLE",
|
|
220
|
+
fail: "error:|Failed building",
|
|
221
|
+
ok: "Bundled",
|
|
222
|
+
ignoreCase: true
|
|
223
|
+
}
|
|
224
|
+
}, {
|
|
225
|
+
name: "storybook",
|
|
226
|
+
match: "storybook",
|
|
227
|
+
rule: {
|
|
228
|
+
done: "started|built",
|
|
229
|
+
fail: "ERR!|Error:",
|
|
230
|
+
ok: "started|built"
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
// Compiled languages and infrastructure
|
|
234
|
+
{
|
|
235
|
+
name: "cargo",
|
|
236
|
+
match: "\\bcargo\\b",
|
|
237
|
+
rule: {
|
|
238
|
+
done: "Finished|error\\[|error:|test result:",
|
|
239
|
+
fail: "^error(?:\\[|:)|test result: FAILED",
|
|
240
|
+
ok: "Finished|test result: ok"
|
|
241
|
+
}
|
|
242
|
+
}, {
|
|
243
|
+
name: "go",
|
|
244
|
+
match: "\\bgo\\s+(?:build|test|run|vet)",
|
|
245
|
+
rule: {
|
|
246
|
+
done: "^(?:ok|FAIL|PASS|\\?)\\s",
|
|
247
|
+
fail: "^FAIL|\\.go:\\d+:",
|
|
248
|
+
ok: "^ok\\s"
|
|
249
|
+
}
|
|
250
|
+
}, {
|
|
251
|
+
name: "dotnet",
|
|
252
|
+
match: "\\bdotnet\\s+(?:build|watch|test|run)",
|
|
253
|
+
rule: {
|
|
254
|
+
done: "Build succeeded|Build FAILED|Passed!|Failed!",
|
|
255
|
+
fail: "Build FAILED|Failed!|error [A-Z]+\\d+",
|
|
256
|
+
ok: "Build succeeded|Passed!"
|
|
257
|
+
}
|
|
258
|
+
}, {
|
|
259
|
+
name: "gradle",
|
|
260
|
+
match: "\\bgradlew?\\b",
|
|
261
|
+
rule: {
|
|
262
|
+
done: "BUILD SUCCESSFUL|BUILD FAILED",
|
|
263
|
+
fail: "BUILD FAILED",
|
|
264
|
+
ok: "BUILD SUCCESSFUL"
|
|
265
|
+
}
|
|
266
|
+
}, {
|
|
267
|
+
name: "maven",
|
|
268
|
+
match: "\\bmvn\\b",
|
|
269
|
+
rule: {
|
|
270
|
+
done: "BUILD SUCCESS|BUILD FAILURE",
|
|
271
|
+
fail: "BUILD FAILURE",
|
|
272
|
+
ok: "BUILD SUCCESS"
|
|
273
|
+
}
|
|
274
|
+
}, {
|
|
275
|
+
name: "docker-compose",
|
|
276
|
+
match: "docker[\\s-]compose",
|
|
277
|
+
rule: {
|
|
278
|
+
fail: "ERROR|exited with code [1-9]",
|
|
279
|
+
ok: "Started|healthy",
|
|
280
|
+
idleSeconds: 5
|
|
281
|
+
}
|
|
282
|
+
}, {
|
|
283
|
+
name: "terraform",
|
|
284
|
+
match: "\\bterraform\\b",
|
|
285
|
+
rule: {
|
|
286
|
+
done: "Apply complete|Plan:|Error:",
|
|
287
|
+
fail: "Error:",
|
|
288
|
+
ok: "Apply complete|No changes"
|
|
289
|
+
}
|
|
290
|
+
}];
|
|
291
|
+
const byName = new Map(PRESETS.map(preset => [preset.name, preset]));
|
|
292
|
+
export function presetByName(name) {
|
|
293
|
+
return byName.get(name);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** First preset whose `match` fits the command, e.g. `npm run dev` → nothing, `tsc --watch` → tsc. */
|
|
297
|
+
export function presetForCommand(command) {
|
|
298
|
+
return PRESETS.find(preset => preset.match && new RegExp(preset.match, "i").test(command));
|
|
299
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export function compileRule(rule) {
|
|
2
|
+
const flags = rule.ignoreCase ? "i" : "";
|
|
3
|
+
const compiled = {};
|
|
4
|
+
if (rule.done) compiled.done = new RegExp(rule.done, flags);
|
|
5
|
+
if (rule.fail) compiled.fail = new RegExp(rule.fail, flags);
|
|
6
|
+
if (rule.ok) compiled.ok = new RegExp(rule.ok, flags);
|
|
7
|
+
if (rule.idleSeconds) compiled.idleMs = Math.round(rule.idleSeconds * 1000);
|
|
8
|
+
return compiled;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Tracks the health of one shell from its log.
|
|
12
|
+
*
|
|
13
|
+
* A run ends when `done` matches (or, without it, after `idleSeconds` of silence); the run's status
|
|
14
|
+
* is then `fail` if anything matched `fail`, `ok` if anything matched `ok`, and otherwise whatever
|
|
15
|
+
* it was. Only status changes are reported — a watcher that recompiles a thousand times says
|
|
16
|
+
* nothing until something actually breaks or gets fixed. A new failing line during an already
|
|
17
|
+
* failing run is reported too, since it is different news.
|
|
18
|
+
*/
|
|
19
|
+
export class Watcher {
|
|
20
|
+
status = "pending";
|
|
21
|
+
runs = 0;
|
|
22
|
+
since = Date.now();
|
|
23
|
+
constructor(rule, preset) {
|
|
24
|
+
this.rule = rule;
|
|
25
|
+
this.preset = preset;
|
|
26
|
+
}
|
|
27
|
+
get idleMs() {
|
|
28
|
+
return this.rule.done ? undefined : this.rule.idleMs;
|
|
29
|
+
}
|
|
30
|
+
state() {
|
|
31
|
+
const state = {
|
|
32
|
+
status: this.status,
|
|
33
|
+
runs: this.runs,
|
|
34
|
+
since: this.since
|
|
35
|
+
};
|
|
36
|
+
if (this.preset) state.preset = this.preset;
|
|
37
|
+
if (this.summaryText) state.summary = this.summaryText;
|
|
38
|
+
return state;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Feeds one committed log line. Returns a change when the reported status or failure changed. */
|
|
42
|
+
line(text, now = Date.now()) {
|
|
43
|
+
if (this.rule.fail?.test(text)) this.sawFail = text.trim();else if (this.rule.ok?.test(text)) this.sawOk = text.trim();
|
|
44
|
+
if (!this.rule.done?.test(text)) return undefined;
|
|
45
|
+
return this.settle(now);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Ends the current run because output went quiet (rules without a `done` pattern). */
|
|
49
|
+
idle(now = Date.now()) {
|
|
50
|
+
if (this.rule.done) return undefined;
|
|
51
|
+
return this.settle(now);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The process ended: a watched program that stops is a failure unless it exited cleanly. */
|
|
55
|
+
exited(exitCode, signal, now = Date.now()) {
|
|
56
|
+
const clean = exitCode === 0 && !signal;
|
|
57
|
+
const summary = clean ? this.summaryText : `process ended ${signal ? `on ${signal}` : `with exit code ${exitCode ?? "?"}`}`;
|
|
58
|
+
return this.apply(clean ? this.sawFail ? "fail" : this.status === "pending" ? "ok" : this.status : "fail", summary, now);
|
|
59
|
+
}
|
|
60
|
+
settle(now) {
|
|
61
|
+
if (!this.sawFail && !this.sawOk && this.status === "pending") return undefined;
|
|
62
|
+
this.runs++;
|
|
63
|
+
const status = this.sawFail ? "fail" : this.sawOk ? "ok" : this.status;
|
|
64
|
+
const summary = this.sawFail ?? this.sawOk ?? this.summaryText;
|
|
65
|
+
this.sawFail = undefined;
|
|
66
|
+
this.sawOk = undefined;
|
|
67
|
+
return this.apply(status === "pending" ? "unknown" : status, summary, now);
|
|
68
|
+
}
|
|
69
|
+
apply(status, summary, now) {
|
|
70
|
+
const changed = status !== this.status || status === "fail" && summary !== this.summaryText;
|
|
71
|
+
const previous = this.status;
|
|
72
|
+
this.status = status;
|
|
73
|
+
this.summaryText = summary;
|
|
74
|
+
if (!changed) return undefined;
|
|
75
|
+
this.since = now;
|
|
76
|
+
return {
|
|
77
|
+
previous,
|
|
78
|
+
current: status,
|
|
79
|
+
summary
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencode-cockpit/daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "cockpitd: the process host behind opencode-cockpit (PTY shells, clean logs, wait conditions)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,12 +21,19 @@
|
|
|
21
21
|
"terminal"
|
|
22
22
|
],
|
|
23
23
|
"exports": {
|
|
24
|
-
".":
|
|
25
|
-
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./types/index.d.ts",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./main": {
|
|
29
|
+
"types": "./types/main.d.ts",
|
|
30
|
+
"default": "./dist/main.js"
|
|
31
|
+
},
|
|
26
32
|
"./package.json": "./package.json"
|
|
27
33
|
},
|
|
28
34
|
"files": [
|
|
29
|
-
"
|
|
35
|
+
"dist",
|
|
36
|
+
"types",
|
|
30
37
|
"README.md",
|
|
31
38
|
"LICENSE"
|
|
32
39
|
],
|
|
@@ -34,7 +41,7 @@
|
|
|
34
41
|
"access": "public"
|
|
35
42
|
},
|
|
36
43
|
"dependencies": {
|
|
37
|
-
"@opencode-cockpit/protocol": "0.
|
|
44
|
+
"@opencode-cockpit/protocol": "0.2.0",
|
|
38
45
|
"@xterm/headless": "6.0.0"
|
|
39
46
|
},
|
|
40
47
|
"engines": {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type CockpitPaths } from "@opencode-cockpit/protocol";
|
|
2
|
+
import { type Level, type Logger } from "./logger.ts";
|
|
3
|
+
import type { Module } from "./module.ts";
|
|
4
|
+
export interface DaemonOptions {
|
|
5
|
+
paths: CockpitPaths;
|
|
6
|
+
modules: Module[];
|
|
7
|
+
/** Shut down after this long with no clients and no busy module. 0 disables. */
|
|
8
|
+
idleTimeoutMs?: number;
|
|
9
|
+
logLevel?: Level;
|
|
10
|
+
/** Log to the log file (default) or stderr. */
|
|
11
|
+
logToFile?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare const DAEMON_VERSION: string;
|
|
14
|
+
/** Build id of this daemon's own code; computed once, matches what clients compute for the entry. */
|
|
15
|
+
export declare const DAEMON_BUILD: string;
|
|
16
|
+
export declare class Daemon {
|
|
17
|
+
private readonly options;
|
|
18
|
+
readonly log: Logger;
|
|
19
|
+
private readonly router;
|
|
20
|
+
private readonly server;
|
|
21
|
+
private readonly startedAt;
|
|
22
|
+
private idleTimer;
|
|
23
|
+
private idleCheck;
|
|
24
|
+
private stopping;
|
|
25
|
+
private resolveStopped;
|
|
26
|
+
/** Resolves once the daemon has fully shut down. */
|
|
27
|
+
readonly stopped: Promise<void>;
|
|
28
|
+
constructor(options: DaemonOptions);
|
|
29
|
+
start(): Promise<void>;
|
|
30
|
+
stop(reason?: string): Promise<void>;
|
|
31
|
+
private busy;
|
|
32
|
+
private refreshIdle;
|
|
33
|
+
/** Refuse to start if a live daemon owns the socket; otherwise clear a stale one. */
|
|
34
|
+
private claimSocket;
|
|
35
|
+
private registerCore;
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type Level = "debug" | "info" | "warn" | "error";
|
|
2
|
+
export interface Logger {
|
|
3
|
+
debug(msg: string, fields?: Record<string, unknown>): void;
|
|
4
|
+
info(msg: string, fields?: Record<string, unknown>): void;
|
|
5
|
+
warn(msg: string, fields?: Record<string, unknown>): void;
|
|
6
|
+
error(msg: string, fields?: Record<string, unknown>): void;
|
|
7
|
+
child(scope: string): Logger;
|
|
8
|
+
}
|
|
9
|
+
/** JSON-lines logger. Writes synchronously so the last lines survive a crash. */
|
|
10
|
+
export declare function createLogger(file: string | undefined, level?: Level, scope?: string): Logger;
|
|
11
|
+
export declare const silentLogger: Logger;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { MethodName, Methods, ParsedParamsOf, ResultOf } from "@opencode-cockpit/protocol";
|
|
2
|
+
import type { Logger } from "./logger.ts";
|
|
3
|
+
/** A connected client as seen by modules. */
|
|
4
|
+
export interface Peer {
|
|
5
|
+
readonly id: number;
|
|
6
|
+
readonly name: string;
|
|
7
|
+
/** Send an event to this peer only, regardless of its subscriptions. */
|
|
8
|
+
send(topic: string, data: unknown): void;
|
|
9
|
+
/** Run when the peer disconnects. */
|
|
10
|
+
onClose(fn: () => void): void;
|
|
11
|
+
/** Topic patterns: exact (`shell.exited`), namespace (`shell.*`) or everything (`*`). */
|
|
12
|
+
readonly topics: Set<string>;
|
|
13
|
+
greet(name: string): void;
|
|
14
|
+
}
|
|
15
|
+
export interface CallContext {
|
|
16
|
+
peer: Peer;
|
|
17
|
+
}
|
|
18
|
+
export interface ModuleContext {
|
|
19
|
+
log: Logger;
|
|
20
|
+
/** Broadcast to every peer subscribed to `topic`. */
|
|
21
|
+
emit(topic: string, data: unknown): void;
|
|
22
|
+
}
|
|
23
|
+
type Handler<M extends MethodName> = (params: ParsedParamsOf<Methods, M>, call: CallContext) => Promise<ResultOf<Methods, M>> | ResultOf<Methods, M>;
|
|
24
|
+
/** Handlers for the methods under one namespace, typed from the protocol contract. */
|
|
25
|
+
export type MethodTable<NS extends string> = {
|
|
26
|
+
[M in MethodName as M extends `${NS}.${infer Rest}` ? Rest : never]: Handler<M>;
|
|
27
|
+
};
|
|
28
|
+
export interface Module<NS extends string = string> {
|
|
29
|
+
readonly name: NS;
|
|
30
|
+
/** Typed per namespace; erased to a plain record when modules are handled generically. */
|
|
31
|
+
readonly methods: string extends NS ? object : MethodTable<NS>;
|
|
32
|
+
start(ctx: ModuleContext): Promise<void>;
|
|
33
|
+
stop(): Promise<void>;
|
|
34
|
+
/** While true the daemon will not shut down for idleness. */
|
|
35
|
+
busy(): boolean;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CallContext, Module } from "./module.ts";
|
|
2
|
+
type AnyHandler = (params: unknown, call: CallContext) => unknown;
|
|
3
|
+
/** Validates params against the protocol contract and dispatches to module handlers. */
|
|
4
|
+
export declare class Router {
|
|
5
|
+
private readonly handlers;
|
|
6
|
+
add(name: string, handler: AnyHandler): void;
|
|
7
|
+
addModule(module: Module): void;
|
|
8
|
+
has(name: string): boolean;
|
|
9
|
+
dispatch(name: string, params: unknown, call: CallContext): Promise<unknown>;
|
|
10
|
+
}
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Socket } from "bun";
|
|
2
|
+
import type { Logger } from "./logger.ts";
|
|
3
|
+
import type { Peer } from "./module.ts";
|
|
4
|
+
import type { Router } from "./router.ts";
|
|
5
|
+
interface ConnState {
|
|
6
|
+
peer: PeerImpl;
|
|
7
|
+
}
|
|
8
|
+
declare class PeerImpl implements Peer {
|
|
9
|
+
readonly id: number;
|
|
10
|
+
private readonly socket;
|
|
11
|
+
private readonly log;
|
|
12
|
+
name: string;
|
|
13
|
+
greeted: boolean;
|
|
14
|
+
readonly topics: Set<string>;
|
|
15
|
+
private readonly closers;
|
|
16
|
+
private queue;
|
|
17
|
+
private queued;
|
|
18
|
+
closed: boolean;
|
|
19
|
+
constructor(id: number, socket: Socket<ConnState>, log: Logger);
|
|
20
|
+
greet(name: string): void;
|
|
21
|
+
onClose(fn: () => void): void;
|
|
22
|
+
send(topic: string, data: unknown): void;
|
|
23
|
+
subscribed(topic: string): boolean;
|
|
24
|
+
write(message: unknown): void;
|
|
25
|
+
drain(): void;
|
|
26
|
+
close(): void;
|
|
27
|
+
private enqueue;
|
|
28
|
+
}
|
|
29
|
+
export interface RpcServerHooks {
|
|
30
|
+
onConnect(count: number): void;
|
|
31
|
+
onDisconnect(count: number): void;
|
|
32
|
+
}
|
|
33
|
+
export declare class RpcServer {
|
|
34
|
+
private readonly router;
|
|
35
|
+
private readonly hooks;
|
|
36
|
+
private readonly log;
|
|
37
|
+
private listener;
|
|
38
|
+
private readonly peers;
|
|
39
|
+
private nextId;
|
|
40
|
+
constructor(router: Router, hooks: RpcServerHooks, log: Logger);
|
|
41
|
+
get clientCount(): number;
|
|
42
|
+
listen(path: string): void;
|
|
43
|
+
broadcast(topic: string, data: unknown): void;
|
|
44
|
+
stop(): void;
|
|
45
|
+
private drop;
|
|
46
|
+
private handleLine;
|
|
47
|
+
private invoke;
|
|
48
|
+
}
|
|
49
|
+
export type { PeerImpl };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { DAEMON_BUILD, DAEMON_VERSION, Daemon, type DaemonOptions } from "./core/daemon.ts"
|
|
2
|
-
export type { CallContext, MethodTable, Module, ModuleContext, Peer } from "./core/module.ts"
|
|
3
|
-
export { createModules, type ModuleOptions } from "./modules/index.ts"
|
|
4
|
-
export { ShellModule, type ShellModuleOptions } from "./modules/shell/module.ts"
|
|
5
|
-
export type { PtyBackend, PtyProcess, PtySpawnOptions } from "./modules/shell/pty.ts"
|
|
1
|
+
export { DAEMON_BUILD, DAEMON_VERSION, Daemon, type DaemonOptions } from "./core/daemon.ts";
|
|
2
|
+
export type { CallContext, MethodTable, Module, ModuleContext, Peer } from "./core/module.ts";
|
|
3
|
+
export { createModules, type ModuleOptions } from "./modules/index.ts";
|
|
4
|
+
export { ShellModule, type ShellModuleOptions } from "./modules/shell/module.ts";
|
|
5
|
+
export type { PtyBackend, PtyProcess, PtySpawnOptions } from "./modules/shell/pty.ts";
|
package/types/main.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Module } from "../core/module.ts";
|
|
2
|
+
import { type ShellModuleOptions } from "./shell/module.ts";
|
|
3
|
+
export interface ModuleOptions {
|
|
4
|
+
shell?: ShellModuleOptions;
|
|
5
|
+
}
|
|
6
|
+
/** Every capability the daemon hosts. Add new modules here. */
|
|
7
|
+
export declare function createModules(options?: ModuleOptions): Module[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function newShellId(): string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MethodTable } from "../../core/module.ts";
|
|
2
|
+
import type { ShellModule } from "./module.ts";
|
|
3
|
+
/**
|
|
4
|
+
* The `shell.*` methods, kept apart from the module's lifecycle and bookkeeping so each file has
|
|
5
|
+
* one job: this one maps protocol calls onto the module, `module.ts` owns the shells.
|
|
6
|
+
*/
|
|
7
|
+
export declare function shellMethods(module: ShellModule): MethodTable<"shell">;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ShellInfo, StartParams } from "@opencode-cockpit/protocol/shell";
|
|
2
|
+
import type { MethodTable, Module, ModuleContext, Peer } from "../../core/module.ts";
|
|
3
|
+
import { type PtyBackend } from "./pty.ts";
|
|
4
|
+
import { Shell, type ShellLimits } from "./shell.ts";
|
|
5
|
+
export interface ShellModuleOptions {
|
|
6
|
+
backend?: PtyBackend;
|
|
7
|
+
limits?: Partial<ShellLimits>;
|
|
8
|
+
/** Oldest finished shells are forgotten beyond this many. */
|
|
9
|
+
maxFinished?: number;
|
|
10
|
+
/** Base environment for spawned processes (defaults to the daemon's). */
|
|
11
|
+
baseEnv?: Record<string, string | undefined>;
|
|
12
|
+
/** Coalesce output events per attached peer for this long. */
|
|
13
|
+
outputFlushMs?: number;
|
|
14
|
+
/** Where to record owned process groups so a restarted daemon can reap orphans. */
|
|
15
|
+
registryFile?: string;
|
|
16
|
+
/** Directory for per-shell log files, when a caller asks for one. */
|
|
17
|
+
logDir?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare class ShellModule implements Module<"shell"> {
|
|
20
|
+
private readonly options;
|
|
21
|
+
readonly name: "shell";
|
|
22
|
+
/** @internal */
|
|
23
|
+
readonly shells: Map<string, Shell>;
|
|
24
|
+
/** @internal */
|
|
25
|
+
readonly attachments: Map<string, () => void>;
|
|
26
|
+
private readonly backend;
|
|
27
|
+
private readonly limits;
|
|
28
|
+
private log;
|
|
29
|
+
private registry;
|
|
30
|
+
private readonly idleTimers;
|
|
31
|
+
/** @internal */
|
|
32
|
+
emit: ModuleContext["emit"];
|
|
33
|
+
constructor(options?: ShellModuleOptions);
|
|
34
|
+
start(ctx: ModuleContext): Promise<void>;
|
|
35
|
+
stop(): Promise<void>;
|
|
36
|
+
busy(): boolean;
|
|
37
|
+
readonly methods: MethodTable<"shell">;
|
|
38
|
+
/** @internal used by methods.ts */
|
|
39
|
+
startShell(params: StartParams): ShellInfo;
|
|
40
|
+
/** @internal used by methods.ts */
|
|
41
|
+
findReusable(params: StartParams): Shell | undefined;
|
|
42
|
+
/** @internal used by methods.ts */
|
|
43
|
+
spawn(shell: Shell): void;
|
|
44
|
+
private onExit;
|
|
45
|
+
/** @internal used by methods.ts */
|
|
46
|
+
attachStream(peer: Peer, shell: Shell): void;
|
|
47
|
+
/** @internal used by methods.ts */
|
|
48
|
+
detach(peer: Peer, id: string): void;
|
|
49
|
+
/**
|
|
50
|
+
* Rules without a `done` pattern end a run on silence, so poll those watchers; the check is a
|
|
51
|
+
* timestamp comparison, and only shells that need it are polled.
|
|
52
|
+
*/
|
|
53
|
+
/** @internal used by methods.ts */
|
|
54
|
+
armIdle(shell: Shell): void;
|
|
55
|
+
/** @internal used by methods.ts */
|
|
56
|
+
clearIdle(id: string): void;
|
|
57
|
+
/** @internal used by methods.ts */
|
|
58
|
+
forget(shell: Shell): void;
|
|
59
|
+
/** @internal used by methods.ts */
|
|
60
|
+
pruneFinished(): void;
|
|
61
|
+
/** @internal used by methods.ts */
|
|
62
|
+
require(id: string): Shell;
|
|
63
|
+
/** @internal used by methods.ts */
|
|
64
|
+
uniqueId(): string;
|
|
65
|
+
/** @internal used by methods.ts */
|
|
66
|
+
environment(extra: Record<string, string> | undefined): Record<string, string>;
|
|
67
|
+
}
|
|
68
|
+
export declare function compilePattern(pattern: string, ignoreCase: boolean): RegExp;
|