gdharness 1.0.28 → 1.0.30

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,222 @@
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
+ var KEEPER_SCRIPT = fileURLToPath(new URL("./keeper.js", import.meta.url));
10
+ function decodeSpec(encoded) {
11
+ return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
12
+ }
13
+ function readLaunched(printed) {
14
+ const line = printed.split(`
15
+ `).find((one) => one.trim() !== "")?.trim() ?? "";
16
+ const pid = /^pid (\d+)$/.exec(line);
17
+ if (pid?.[1] !== undefined) {
18
+ return { pid: Number(pid[1]) };
19
+ }
20
+ return { error: line.replace(/^error /, "") || "the launcher said nothing" };
21
+ }
22
+
23
+ // src/run-record.ts
24
+ import { mkdirSync, openSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
25
+ import { basename, join as join2 } from "node:path";
26
+
27
+ // src/process-children.ts
28
+ import { execFile, execFileSync, spawnSync } from "node:child_process";
29
+ import { promisify } from "node:util";
30
+ var run = promisify(execFile);
31
+
32
+ // src/runtime-client.ts
33
+ import { tmpdir } from "node:os";
34
+ import { dirname, join, resolve } from "node:path";
35
+
36
+ // src/launch.ts
37
+ function envValue(name, variables = process.env) {
38
+ const value = variables[name];
39
+ return value === undefined || value === "" ? undefined : value;
40
+ }
41
+
42
+ // src/runtime-client.ts
43
+ function runtimeDirectory(variables = process.env) {
44
+ const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
45
+ if (explicit) {
46
+ return explicit;
47
+ }
48
+ const perUser = envValue("XDG_RUNTIME_DIR", variables);
49
+ return join(perUser ?? tmpdir(), "gdharness");
50
+ }
51
+ var ERROR_REPORT_KEEP_MS = 60 * 60 * 1000;
52
+ var verdicts = new Map;
53
+ var LATE_ANSWER_MS = 30 * 60000;
54
+ var lateAnswers = new Map;
55
+
56
+ // src/run-record.ts
57
+ function runsDirectory() {
58
+ return join2(runtimeDirectory(), "runs");
59
+ }
60
+ function recordPath() {
61
+ return join2(runsDirectory(), "run.json");
62
+ }
63
+ function writeRunRecord(record) {
64
+ mkdirSync(runsDirectory(), { recursive: true });
65
+ writeFileSync(recordPath(), JSON.stringify(record, null, 2), "utf8");
66
+ }
67
+ function recordRunEnded(pid, ending) {
68
+ const path = recordPath();
69
+ const record = recordAt(path);
70
+ if (record === null || record.pid !== pid) {
71
+ return;
72
+ }
73
+ const written = {
74
+ ...record,
75
+ ...ending.exitCode === null ? {} : { exitCode: ending.exitCode },
76
+ ...ending.exitSignal === null ? {} : { exitSignal: ending.exitSignal }
77
+ };
78
+ try {
79
+ writeFileSync(path, JSON.stringify(written, null, 2), "utf8");
80
+ } catch {}
81
+ }
82
+ function recordAt(path) {
83
+ let parsed;
84
+ try {
85
+ parsed = JSON.parse(readFileSync(path, "utf8"));
86
+ } catch {
87
+ return null;
88
+ }
89
+ if (!parsed || typeof parsed !== "object") {
90
+ return null;
91
+ }
92
+ const fields = parsed;
93
+ const pid = fields["pid"];
94
+ const transcript = fields["transcript"];
95
+ const startedAt = fields["startedAt"];
96
+ if (typeof pid !== "number" || typeof transcript !== "string" || typeof startedAt !== "number") {
97
+ return null;
98
+ }
99
+ const args = fields["arguments"];
100
+ return {
101
+ pid,
102
+ transcript,
103
+ startedAt,
104
+ projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
105
+ arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
106
+ ...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
107
+ ...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
108
+ ...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {}
109
+ };
110
+ }
111
+
112
+ // src/keeper.ts
113
+ async function startDetached(spec, output) {
114
+ return await new Promise((resolve) => {
115
+ const child = spawn(spec.command, [...spec.args], {
116
+ stdio: ["ignore", output, output],
117
+ detached: true,
118
+ ...spec.env === undefined ? {} : { env: spec.env }
119
+ });
120
+ child.once("error", (error) => {
121
+ resolve({ error: error.message });
122
+ });
123
+ child.once("spawn", () => {
124
+ resolve(child.pid === undefined ? { error: "the operating system gave it no pid" } : { child, pid: child.pid });
125
+ });
126
+ });
127
+ }
128
+ async function launch(encoded) {
129
+ const spec = decodeSpec(encoded);
130
+ if (spec.run === undefined) {
131
+ const started = await startDetached(spec, "ignore");
132
+ if ("error" in started) {
133
+ process.stdout.write(`error ${started.error}
134
+ `);
135
+ } else {
136
+ started.child.unref();
137
+ process.stdout.write(`pid ${started.pid}
138
+ `);
139
+ }
140
+ process.exit(0);
141
+ }
142
+ const keeper = spawn(process.execPath, [KEEPER_SCRIPT, "keep", encoded], {
143
+ stdio: ["ignore", "pipe", "pipe"],
144
+ detached: true,
145
+ windowsHide: true
146
+ });
147
+ let printed = "";
148
+ let complained = "";
149
+ keeper.once("error", (error) => {
150
+ process.stdout.write(`error the keeper did not start: ${error.message}
151
+ `);
152
+ process.exit(0);
153
+ });
154
+ keeper.stderr.on("data", (chunk) => {
155
+ complained += chunk.toString();
156
+ });
157
+ keeper.stdout.on("data", (chunk) => {
158
+ printed += chunk.toString();
159
+ if (!printed.includes(`
160
+ `)) {
161
+ return;
162
+ }
163
+ const launched = readLaunched(printed);
164
+ process.stdout.write("pid" in launched ? `pid ${launched.pid}
165
+ ` : `error ${launched.error}
166
+ `);
167
+ keeper.unref();
168
+ process.exit(0);
169
+ });
170
+ keeper.once("close", () => {
171
+ process.stdout.write(`error the keeper ended before it said anything${complained ? `: ${complained.trim()}` : ""}
172
+ `);
173
+ process.exit(0);
174
+ });
175
+ }
176
+ async function keep(encoded) {
177
+ const spec = decodeSpec(encoded);
178
+ if (spec.run === undefined) {
179
+ process.stdout.write(`error a keeper was started without a run
180
+ `);
181
+ process.exit(1);
182
+ }
183
+ const run = spec.run;
184
+ const transcript = openSync2(run.transcript, "a");
185
+ let started;
186
+ try {
187
+ started = await startDetached(spec, transcript);
188
+ } finally {
189
+ closeSync(transcript);
190
+ }
191
+ if ("error" in started) {
192
+ process.stdout.write(`error ${started.error}
193
+ `);
194
+ process.exit(1);
195
+ }
196
+ const { child, pid } = started;
197
+ writeRunRecord({
198
+ pid,
199
+ transcript: run.transcript,
200
+ startedAt: run.startedAt,
201
+ projectPath: run.projectPath,
202
+ arguments: spec.args,
203
+ command: spec.command
204
+ });
205
+ child.once("exit", (code, signal) => {
206
+ recordRunEnded(pid, { exitCode: code, exitSignal: code === null ? signal : null });
207
+ process.exit(0);
208
+ });
209
+ process.stdout.write(`pid ${pid}
210
+ `);
211
+ process.stdout.end();
212
+ }
213
+ var [mode, encoded] = process.argv.slice(2);
214
+ if (mode === "launch" && encoded !== undefined) {
215
+ await launch(encoded);
216
+ } else if (mode === "keep" && encoded !== undefined) {
217
+ await keep(encoded);
218
+ } else {
219
+ process.stderr.write(`keeper.js launch|keep <spec>
220
+ `);
221
+ process.exit(2);
222
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
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",