gdharness 1.0.29 → 1.0.31

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.
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/keeper.ts
4
+ import { spawn } from "node:child_process";
5
+ import { closeSync, openSync as openSync2 } from "node:fs";
6
+
7
+ // src/outside.ts
8
+ import { fileURLToPath } from "node:url";
9
+ function withChanges(base, changes) {
10
+ const env = { ...base };
11
+ for (const [name, value] of Object.entries(changes)) {
12
+ if (value === null) {
13
+ delete env[name];
14
+ } else {
15
+ env[name] = value;
16
+ }
17
+ }
18
+ return env;
19
+ }
20
+ function sendSpec(child, spec) {
21
+ child.stdin?.end(JSON.stringify(spec), "utf8");
22
+ }
23
+ async function receivedSpec() {
24
+ const chunks = [];
25
+ for await (const chunk of process.stdin) {
26
+ chunks.push(chunk);
27
+ }
28
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
29
+ }
30
+ var KEEPER_SCRIPT = fileURLToPath(new URL("./keeper.js", import.meta.url));
31
+ function readLaunched(printed) {
32
+ const line = printed.split(`
33
+ `).find((one) => one.trim() !== "")?.trim() ?? "";
34
+ const pid = /^pid (\d+)$/.exec(line);
35
+ if (pid?.[1] !== undefined) {
36
+ return { pid: Number(pid[1]) };
37
+ }
38
+ return { error: line.replace(/^error /, "") || "the launcher said nothing" };
39
+ }
40
+
41
+ // src/run-record.ts
42
+ import { createHash } from "node:crypto";
43
+ import {
44
+ mkdirSync,
45
+ openSync,
46
+ readdirSync,
47
+ readFileSync,
48
+ realpathSync,
49
+ rmSync,
50
+ statSync,
51
+ writeFileSync
52
+ } from "node:fs";
53
+ import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
54
+
55
+ // src/process-children.ts
56
+ import { execFile, execFileSync, spawnSync } from "node:child_process";
57
+ import { promisify } from "node:util";
58
+ var run = promisify(execFile);
59
+
60
+ // src/runtime-client.ts
61
+ import { tmpdir } from "node:os";
62
+ import { dirname, join, resolve } from "node:path";
63
+
64
+ // src/launch.ts
65
+ function envValue(name, variables = process.env) {
66
+ const value = variables[name];
67
+ return value === undefined || value === "" ? undefined : value;
68
+ }
69
+
70
+ // src/runtime-client.ts
71
+ function runtimeDirectory(variables = process.env) {
72
+ const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
73
+ if (explicit) {
74
+ return explicit;
75
+ }
76
+ const perUser = envValue("XDG_RUNTIME_DIR", variables);
77
+ return join(perUser ?? tmpdir(), "gdharness");
78
+ }
79
+ var ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
80
+ var verdicts = new Map;
81
+ var LATE_ANSWER_MS = 30 * 60000;
82
+ var lateAnswers = new Map;
83
+
84
+ // src/run-record.ts
85
+ function runsDirectory() {
86
+ return join2(runtimeDirectory(), "runs");
87
+ }
88
+ function realAsFarAsItExists(path) {
89
+ const missing = [];
90
+ let at = path;
91
+ for (;; ) {
92
+ try {
93
+ return join2(realpathSync.native(at), ...missing);
94
+ } catch {
95
+ const parent = dirname2(at);
96
+ if (parent === at) {
97
+ return path;
98
+ }
99
+ missing.unshift(basename(at));
100
+ at = parent;
101
+ }
102
+ }
103
+ }
104
+ function projectKey(projectPath) {
105
+ const real = realAsFarAsItExists(resolve2(projectPath)).replace(/[\\/]+$/, "");
106
+ const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
107
+ return createHash("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
108
+ }
109
+ function runRecordPath(projectPath, directory = runtimeDirectory()) {
110
+ return join2(directory, "runs", `note-${projectKey(projectPath)}.json`);
111
+ }
112
+ function writeRunRecord(record) {
113
+ mkdirSync(runsDirectory(), { recursive: true });
114
+ writeFileSync(runRecordPath(record.projectPath), JSON.stringify(record, null, 2), "utf8");
115
+ }
116
+ function recordRunEnded(projectPath, pid, ending) {
117
+ const path = runRecordPath(projectPath);
118
+ const record = recordAt(path);
119
+ if (record === null || record.pid !== pid) {
120
+ return;
121
+ }
122
+ const written = {
123
+ ...record,
124
+ ...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
125
+ ...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal }
126
+ };
127
+ try {
128
+ writeFileSync(path, JSON.stringify(written, null, 2), "utf8");
129
+ } catch {}
130
+ }
131
+ function recordAt(path) {
132
+ let parsed;
133
+ try {
134
+ parsed = JSON.parse(readFileSync(path, "utf8"));
135
+ } catch {
136
+ return null;
137
+ }
138
+ if (!parsed || typeof parsed !== "object") {
139
+ return null;
140
+ }
141
+ const fields = parsed;
142
+ const pid = fields["pid"];
143
+ const transcript = fields["transcript"];
144
+ const startedAt = fields["startedAt"];
145
+ if (typeof pid !== "number" || typeof transcript !== "string" || typeof startedAt !== "number") {
146
+ return null;
147
+ }
148
+ const args = fields["arguments"];
149
+ return {
150
+ pid,
151
+ transcript,
152
+ startedAt,
153
+ projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
154
+ arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
155
+ ...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
156
+ ...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
157
+ ...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
158
+ };
159
+ }
160
+
161
+ // src/keeper.ts
162
+ async function startDetached(spec, output) {
163
+ return await new Promise((resolve) => {
164
+ const child = spawn(spec.command, [...spec.args], {
165
+ stdio: ["ignore", output, output],
166
+ detached: true,
167
+ env: withChanges(process.env, spec.envChanges)
168
+ });
169
+ child.once("error", (error) => {
170
+ resolve({ error: error.message });
171
+ });
172
+ child.once("spawn", () => {
173
+ resolve(child.pid === undefined ? { error: "the operating system gave it no pid" } : { child, pid: child.pid });
174
+ });
175
+ });
176
+ }
177
+ async function launch() {
178
+ const spec = await receivedSpec();
179
+ if (spec.run === undefined) {
180
+ const started = await startDetached(spec, "ignore");
181
+ if ("error" in started) {
182
+ process.stdout.write(`error ${started.error}
183
+ `);
184
+ } else {
185
+ started.child.unref();
186
+ process.stdout.write(`pid ${started.pid}
187
+ `);
188
+ }
189
+ process.exit(0);
190
+ }
191
+ const keeper = spawn(process.execPath, [KEEPER_SCRIPT, "keep"], {
192
+ stdio: ["pipe", "pipe", "pipe"],
193
+ detached: true,
194
+ windowsHide: true
195
+ });
196
+ let printed = "";
197
+ let complained = "";
198
+ keeper.once("error", (error) => {
199
+ process.stdout.write(`error the keeper did not start: ${error.message}
200
+ `);
201
+ process.exit(0);
202
+ });
203
+ sendSpec(keeper, spec);
204
+ keeper.stderr.on("data", (chunk) => {
205
+ complained += chunk.toString();
206
+ });
207
+ keeper.stdout.on("data", (chunk) => {
208
+ printed += chunk.toString();
209
+ if (!printed.includes(`
210
+ `)) {
211
+ return;
212
+ }
213
+ const launched = readLaunched(printed);
214
+ process.stdout.write("pid" in launched ? `pid ${launched.pid}
215
+ ` : `error ${launched.error}
216
+ `);
217
+ keeper.unref();
218
+ process.exit(0);
219
+ });
220
+ keeper.once("close", () => {
221
+ process.stdout.write(`error the keeper ended before it said anything${complained ? `: ${complained.trim()}` : ""}
222
+ `);
223
+ process.exit(0);
224
+ });
225
+ }
226
+ async function keep() {
227
+ const spec = await receivedSpec();
228
+ if (spec.run === undefined) {
229
+ process.stdout.write(`error a keeper was started without a run
230
+ `);
231
+ process.exit(1);
232
+ }
233
+ const run = spec.run;
234
+ const transcript = openSync2(run.transcript, "a");
235
+ let started;
236
+ try {
237
+ started = await startDetached(spec, transcript);
238
+ } finally {
239
+ closeSync(transcript);
240
+ }
241
+ if ("error" in started) {
242
+ process.stdout.write(`error ${started.error}
243
+ `);
244
+ process.exit(1);
245
+ }
246
+ const { child, pid } = started;
247
+ writeRunRecord({
248
+ pid,
249
+ transcript: run.transcript,
250
+ startedAt: run.startedAt,
251
+ projectPath: run.projectPath,
252
+ arguments: spec.args,
253
+ command: spec.command
254
+ });
255
+ child.once("exit", (code, signal) => {
256
+ recordRunEnded(run.projectPath, pid, { exitCode: code, exitSignal: code === null ? signal : null });
257
+ process.exit(0);
258
+ });
259
+ process.stdout.write(`pid ${pid}
260
+ `);
261
+ process.stdout.end();
262
+ }
263
+ var mode = process.argv[2];
264
+ if (mode === "launch") {
265
+ await launch();
266
+ } else if (mode === "keep") {
267
+ await keep();
268
+ } else {
269
+ process.stderr.write(`keeper.js launch|keep, with the spec on stdin
270
+ `);
271
+ process.exit(2);
272
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.0.29",
3
+ "version": "1.0.31",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",