gdharness 0.12.18 → 0.13.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/build/cli.js +174 -56
- package/build/godot/operations/project_config.gd +38 -1
- package/build/index.js +147 -53
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -7187,6 +7187,9 @@ function announced(line) {
|
|
|
7187
7187
|
}
|
|
7188
7188
|
return { severity: match[2] === "WARNING" ? "warning" : "error", text: match[3] ?? "" };
|
|
7189
7189
|
}
|
|
7190
|
+
function forAnswer(entries) {
|
|
7191
|
+
return entries.map((entry) => entry.detail.length === 0 ? { index: entry.index, severity: entry.severity, source: entry.source, text: entry.text } : entry);
|
|
7192
|
+
}
|
|
7190
7193
|
|
|
7191
7194
|
class GameLog {
|
|
7192
7195
|
entries = [];
|
|
@@ -15037,6 +15040,11 @@ function readPositiveNumber(params, key) {
|
|
|
15037
15040
|
const number = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
|
|
15038
15041
|
return typeof number === "number" && Number.isFinite(number) && number > 0 ? number : undefined;
|
|
15039
15042
|
}
|
|
15043
|
+
function readNonNegativeNumber(params, key) {
|
|
15044
|
+
const value = params[key];
|
|
15045
|
+
const number = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
|
|
15046
|
+
return typeof number === "number" && Number.isFinite(number) && number >= 0 ? number : undefined;
|
|
15047
|
+
}
|
|
15040
15048
|
function readBoolean(params, key) {
|
|
15041
15049
|
const value = params[key];
|
|
15042
15050
|
if (typeof value === "boolean") {
|
|
@@ -15291,6 +15299,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
15291
15299
|
parameters: {
|
|
15292
15300
|
projectPath: PROJECT_PATH,
|
|
15293
15301
|
setting: { type: "string", description: 'Setting path, such as "display/window/size/viewport_width".' },
|
|
15302
|
+
prefix: {
|
|
15303
|
+
type: "string",
|
|
15304
|
+
ops: ["get"],
|
|
15305
|
+
description: 'get: answer with every setting whose name starts with this, and the type the engine registers for each, instead of one setting by name. "debug/gdscript/warnings/" answers the whole family. The type matters: a family of levels can hold a setting that is a bool, and writing a level over it looks like it worked.'
|
|
15306
|
+
},
|
|
15294
15307
|
value: {
|
|
15295
15308
|
blank: true,
|
|
15296
15309
|
description: 'The value to write. Engine types may be tagged, {"_type": "Vector2", "x": 1, "y": 2}. "" writes an empty string.'
|
|
@@ -15312,7 +15325,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15312
15325
|
},
|
|
15313
15326
|
requires: ["projectPath"],
|
|
15314
15327
|
operations: {
|
|
15315
|
-
get: { summary: "read one setting", requires: [
|
|
15328
|
+
get: { summary: "read one setting, or every setting under prefix", requires: [] },
|
|
15316
15329
|
set: { summary: "write one setting", requires: ["setting", "value"] },
|
|
15317
15330
|
add_autoload: { summary: "register an autoload singleton", requires: ["name", "path"] },
|
|
15318
15331
|
remove_autoload: { summary: "unregister an autoload", requires: ["name"] },
|
|
@@ -15719,7 +15732,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15719
15732
|
runtimeWaitMs: {
|
|
15720
15733
|
type: "number",
|
|
15721
15734
|
ops: ["start"],
|
|
15722
|
-
description: "start: how long to wait for the game to announce its runtime before answering. Default 5000. Worth raising for a project that takes longer than that to reach its first frame, which is what leaves runtime listening false on a game whose runtime arrives a moment later."
|
|
15735
|
+
description: "start: how long to wait for the game to announce its runtime before answering. Default 5000. Worth raising for a project that takes longer than that to reach its first frame, which is what leaves runtime listening false on a game whose runtime arrives a moment later. 0 does not wait at all, which is the one to pass for a scene that announces nothing by construction, such as a bench that prints and quits."
|
|
15723
15736
|
},
|
|
15724
15737
|
frames: {
|
|
15725
15738
|
type: "number",
|
|
@@ -15728,15 +15741,16 @@ var init_tool_definitions = __esm(() => {
|
|
|
15728
15741
|
},
|
|
15729
15742
|
timeoutMs: {
|
|
15730
15743
|
type: "number",
|
|
15731
|
-
ops: ["check"],
|
|
15732
|
-
description: "check: how long to give the boot before it is called hung. Default 60000."
|
|
15744
|
+
ops: ["check", "wait"],
|
|
15745
|
+
description: "check: how long to give the boot before it is called hung. Default 60000. wait: how long to wait for the run to end before answering anyway. Default 600000."
|
|
15733
15746
|
}
|
|
15734
15747
|
},
|
|
15735
15748
|
requires: ["projectPath"],
|
|
15736
15749
|
operations: {
|
|
15737
15750
|
start: { summary: "run the project until it quits or is stopped", requires: [] },
|
|
15738
15751
|
stop: { summary: "end the run and answer with what it printed last", requires: [] },
|
|
15739
|
-
check: { summary: "boot headless, quit after a few frames, and report the verdict", requires: [] }
|
|
15752
|
+
check: { summary: "boot headless, quit after a few frames, and report the verdict", requires: [] },
|
|
15753
|
+
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] }
|
|
15740
15754
|
},
|
|
15741
15755
|
defaultOperation: "start"
|
|
15742
15756
|
},
|
|
@@ -15753,6 +15767,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
15753
15767
|
type: "boolean",
|
|
15754
15768
|
description: "Only entries printed since the previous editor_output. Default false."
|
|
15755
15769
|
},
|
|
15770
|
+
cpu: {
|
|
15771
|
+
type: "boolean",
|
|
15772
|
+
description: "Also answer with cpuSeconds, the processor time the run has used. Default false, because asking costs a subprocess. Worth it when elapsedMs is climbing and nothing is being printed: processor time standing still is a run that has stopped doing anything, rather than one that is slow."
|
|
15773
|
+
},
|
|
15756
15774
|
contains: {
|
|
15757
15775
|
type: "string",
|
|
15758
15776
|
description: "Only entries mentioning this text, matched against everything the run has printed rather than against the entries this answer would otherwise carry: a line is found however much was printed after it."
|
|
@@ -16132,6 +16150,43 @@ var init_tool_definitions = __esm(() => {
|
|
|
16132
16150
|
SPECS_BY_NAME = dictionary(Object.fromEntries(TOOL_SPECS.map((spec) => [spec.name, spec])));
|
|
16133
16151
|
});
|
|
16134
16152
|
|
|
16153
|
+
// src/headless-operations.ts
|
|
16154
|
+
var HEADLESS_OPERATIONS;
|
|
16155
|
+
var init_headless_operations = __esm(() => {
|
|
16156
|
+
HEADLESS_OPERATIONS = dictionary({
|
|
16157
|
+
project_settings: dictionary({
|
|
16158
|
+
get: "get_project_setting",
|
|
16159
|
+
set: "set_project_setting",
|
|
16160
|
+
add_autoload: "add_autoload",
|
|
16161
|
+
remove_autoload: "remove_autoload",
|
|
16162
|
+
set_main_scene: "set_main_scene",
|
|
16163
|
+
add_input_action: "add_input_action",
|
|
16164
|
+
enable_plugin: "enable_plugin",
|
|
16165
|
+
disable_plugin: "disable_plugin",
|
|
16166
|
+
add_audio_bus: "create_audio_bus",
|
|
16167
|
+
set_audio_bus_effect: "set_audio_bus_effect",
|
|
16168
|
+
set_audio_bus_volume: "set_audio_bus_volume"
|
|
16169
|
+
}),
|
|
16170
|
+
project_import: dictionary({
|
|
16171
|
+
status: "get_import_status",
|
|
16172
|
+
options: "get_import_options",
|
|
16173
|
+
set_options: "set_import_options",
|
|
16174
|
+
reimport: "reimport_resource",
|
|
16175
|
+
uid: "get_uid",
|
|
16176
|
+
refresh_uids: "resave_resources",
|
|
16177
|
+
refresh_classes: "refresh_class_cache"
|
|
16178
|
+
}),
|
|
16179
|
+
project_export: dictionary({ list: "list_export_presets" }),
|
|
16180
|
+
script_edit: dictionary({ create: "create_script", modify: "modify_script" }),
|
|
16181
|
+
script_info: dictionary({ structure: "get_script_info" }),
|
|
16182
|
+
editor_classes: dictionary({
|
|
16183
|
+
query: "query_classes",
|
|
16184
|
+
info: "query_class_info",
|
|
16185
|
+
inheritance: "inspect_inheritance"
|
|
16186
|
+
})
|
|
16187
|
+
});
|
|
16188
|
+
});
|
|
16189
|
+
|
|
16135
16190
|
// node_modules/zod/v3/helpers/util.js
|
|
16136
16191
|
var util, objectUtil, ZodParsedType, getParsedType = (data) => {
|
|
16137
16192
|
const t = typeof data;
|
|
@@ -35800,6 +35855,45 @@ var init_lsp_client = __esm(() => {
|
|
|
35800
35855
|
init_ports();
|
|
35801
35856
|
});
|
|
35802
35857
|
|
|
35858
|
+
// src/process-time.ts
|
|
35859
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
35860
|
+
import { promisify as promisify5 } from "node:util";
|
|
35861
|
+
async function cpuSecondsOf(pid) {
|
|
35862
|
+
try {
|
|
35863
|
+
return process.platform === "win32" ? await askWindows(pid) : await askPosix(pid);
|
|
35864
|
+
} catch {
|
|
35865
|
+
return;
|
|
35866
|
+
}
|
|
35867
|
+
}
|
|
35868
|
+
async function askWindows(pid) {
|
|
35869
|
+
const { stdout } = await run5("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `(Get-Process -Id ${pid}).CPU`], { timeout: ASK_TIMEOUT_MS, windowsHide: true });
|
|
35870
|
+
const seconds = Number(stdout.trim().replace(",", "."));
|
|
35871
|
+
return Number.isFinite(seconds) ? seconds : undefined;
|
|
35872
|
+
}
|
|
35873
|
+
async function askPosix(pid) {
|
|
35874
|
+
const { stdout } = await run5("ps", ["-o", "time=", "-p", String(pid)], { timeout: ASK_TIMEOUT_MS });
|
|
35875
|
+
return secondsFromClock(stdout.trim());
|
|
35876
|
+
}
|
|
35877
|
+
function secondsFromClock(printed) {
|
|
35878
|
+
if (printed === "") {
|
|
35879
|
+
return;
|
|
35880
|
+
}
|
|
35881
|
+
const dashed = printed.includes("-");
|
|
35882
|
+
const days = dashed ? printed.slice(0, printed.indexOf("-")) : null;
|
|
35883
|
+
const clock = dashed ? printed.slice(printed.indexOf("-") + 1) : printed;
|
|
35884
|
+
const parts = clock.split(":").map((part) => Number(part));
|
|
35885
|
+
if (parts.length === 0 || parts.some((part) => !Number.isFinite(part))) {
|
|
35886
|
+
return;
|
|
35887
|
+
}
|
|
35888
|
+
const seconds = parts.reduce((total, part) => total * 60 + part, 0);
|
|
35889
|
+
const extra = days === null ? 0 : Number(days) * 86400;
|
|
35890
|
+
return Number.isFinite(extra) ? seconds + extra : undefined;
|
|
35891
|
+
}
|
|
35892
|
+
var run5, ASK_TIMEOUT_MS = 2000;
|
|
35893
|
+
var init_process_time = __esm(() => {
|
|
35894
|
+
run5 = promisify5(execFile5);
|
|
35895
|
+
});
|
|
35896
|
+
|
|
35803
35897
|
// src/project-scan.ts
|
|
35804
35898
|
import { existsSync as existsSync10, readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
|
|
35805
35899
|
import { join as join12 } from "node:path";
|
|
@@ -36355,7 +36449,7 @@ var init_setup = __esm(() => {
|
|
|
36355
36449
|
});
|
|
36356
36450
|
|
|
36357
36451
|
// src/server.ts
|
|
36358
|
-
import { execFile as
|
|
36452
|
+
import { execFile as execFile6, spawn } from "node:child_process";
|
|
36359
36453
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
36360
36454
|
import {
|
|
36361
36455
|
closeSync,
|
|
@@ -36374,7 +36468,7 @@ import { tmpdir as tmpdir5 } from "node:os";
|
|
|
36374
36468
|
import { basename as basename3, dirname as dirname9, join as join16, normalize as normalize3 } from "node:path";
|
|
36375
36469
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
36376
36470
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
36377
|
-
import { promisify as
|
|
36471
|
+
import { promisify as promisify6 } from "node:util";
|
|
36378
36472
|
function patienceForFrames(frames, atLeast) {
|
|
36379
36473
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
36380
36474
|
}
|
|
@@ -37003,9 +37097,15 @@ class GodotServer {
|
|
|
37003
37097
|
case "editor_launch":
|
|
37004
37098
|
return op === "restart" ? await this.handleRestartEditor() : await this.handleLaunchEditor(args);
|
|
37005
37099
|
case "editor_run":
|
|
37006
|
-
|
|
37100
|
+
if (op === "stop") {
|
|
37101
|
+
return await this.handleStopProject();
|
|
37102
|
+
}
|
|
37103
|
+
if (op === "wait") {
|
|
37104
|
+
return await this.handleWaitForRun(args);
|
|
37105
|
+
}
|
|
37106
|
+
return await this.handleRunProject(args, op);
|
|
37007
37107
|
case "editor_output":
|
|
37008
|
-
return this.handleGetDebugOutput(args);
|
|
37108
|
+
return await this.handleGetDebugOutput(args);
|
|
37009
37109
|
case "editor_status":
|
|
37010
37110
|
return await this.handleEditorStatus();
|
|
37011
37111
|
case "editor_rescan":
|
|
@@ -37376,7 +37476,7 @@ class GodotServer {
|
|
|
37376
37476
|
const log = new GameLog;
|
|
37377
37477
|
let exitCode = 0;
|
|
37378
37478
|
try {
|
|
37379
|
-
const { stdout, stderr } = await
|
|
37479
|
+
const { stdout, stderr } = await run6(engine.value, exportArgs, { timeout: 300000 });
|
|
37380
37480
|
log.append("stdout", stdout);
|
|
37381
37481
|
log.append("stderr", stderr);
|
|
37382
37482
|
} catch (error) {
|
|
@@ -37400,7 +37500,7 @@ class GodotServer {
|
|
|
37400
37500
|
exitCode,
|
|
37401
37501
|
errors: log.count("error"),
|
|
37402
37502
|
warnings: log.count("warning"),
|
|
37403
|
-
entries: problems.entries
|
|
37503
|
+
entries: forAnswer(problems.entries)
|
|
37404
37504
|
};
|
|
37405
37505
|
if (!verdict.exported) {
|
|
37406
37506
|
return {
|
|
@@ -37517,7 +37617,7 @@ class GodotServer {
|
|
|
37517
37617
|
exitCode,
|
|
37518
37618
|
hung,
|
|
37519
37619
|
arguments: cmdArgs,
|
|
37520
|
-
entries: printed.slice(0, 60)
|
|
37620
|
+
entries: forAnswer(printed.slice(0, 60))
|
|
37521
37621
|
}, null, 2)
|
|
37522
37622
|
}
|
|
37523
37623
|
],
|
|
@@ -37571,7 +37671,7 @@ class GodotServer {
|
|
|
37571
37671
|
}
|
|
37572
37672
|
async godotVersion(godotPath) {
|
|
37573
37673
|
try {
|
|
37574
|
-
const { stdout } = await
|
|
37674
|
+
const { stdout } = await run6(godotPath, ["--version"], { timeout: 1e4 });
|
|
37575
37675
|
return stdout.trim();
|
|
37576
37676
|
} catch (error) {
|
|
37577
37677
|
this.logDebug(`Godot did not answer --version: ${errorMessage(error)}`);
|
|
@@ -37866,7 +37966,7 @@ class GodotServer {
|
|
|
37866
37966
|
platform: process.platform,
|
|
37867
37967
|
variables: process.env
|
|
37868
37968
|
});
|
|
37869
|
-
const runtimeWaitMs =
|
|
37969
|
+
const runtimeWaitMs = readNonNegativeNumber(args, "runtimeWaitMs") ?? ANNOUNCE_BUDGET_MS;
|
|
37870
37970
|
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
37871
37971
|
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0) {
|
|
37872
37972
|
return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
|
|
@@ -38188,10 +38288,24 @@ class GodotServer {
|
|
|
38188
38288
|
frames,
|
|
38189
38289
|
errors,
|
|
38190
38290
|
warnings,
|
|
38191
|
-
entries: boot.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries
|
|
38291
|
+
entries: forAnswer(boot.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
38192
38292
|
});
|
|
38193
38293
|
}
|
|
38194
|
-
|
|
38294
|
+
async handleWaitForRun(args) {
|
|
38295
|
+
const run = this.currentRun();
|
|
38296
|
+
if (!run) {
|
|
38297
|
+
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
38298
|
+
}
|
|
38299
|
+
const budgetMs = readPositiveNumber(args, "timeoutMs") ?? WAIT_FOR_RUN_MS;
|
|
38300
|
+
const until = Date.now() + budgetMs;
|
|
38301
|
+
while (stillRunning(run) && Date.now() < until) {
|
|
38302
|
+
this.drainEditorOutput(run);
|
|
38303
|
+
this.drainTranscript(run);
|
|
38304
|
+
await delay3(RUN_POLL_MS);
|
|
38305
|
+
}
|
|
38306
|
+
return await this.handleGetDebugOutput(args, stillRunning(run) ? budgetMs : undefined);
|
|
38307
|
+
}
|
|
38308
|
+
async handleGetDebugOutput(args, waitedMs) {
|
|
38195
38309
|
const run = this.currentRun();
|
|
38196
38310
|
if (!run) {
|
|
38197
38311
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
@@ -38210,14 +38324,19 @@ class GodotServer {
|
|
|
38210
38324
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
38211
38325
|
});
|
|
38212
38326
|
const halt = run.throughEditor ? this.dapClient?.whereItStopped() ?? null : null;
|
|
38327
|
+
const wanted = readBoolean(args, "cpu") ?? false;
|
|
38328
|
+
const cpuSeconds = wanted && run.pid !== null && stillRunning(run) ? await cpuSecondsOf(run.pid) : undefined;
|
|
38213
38329
|
const notes = [];
|
|
38330
|
+
if (waitedMs !== undefined) {
|
|
38331
|
+
notes.push(`The wait of ${waitedMs}ms ran out and the run is still going, so this is what it had printed by then rather than everything it will print. editor_run wait again to keep waiting.`);
|
|
38332
|
+
}
|
|
38214
38333
|
if (run.endedUnwatched === true) {
|
|
38215
38334
|
notes.push("This run outlived the server that started it and is over now, so its exit code was never collected. Everything it printed is below, read back from its transcript.");
|
|
38216
38335
|
}
|
|
38217
38336
|
if (typeof run.endedHere === "string") {
|
|
38218
38337
|
notes.push(`This run was ended here, by ${run.endedHere}.`);
|
|
38219
38338
|
} else if (!stillRunning(run) && !run.throughEditor && run.exitCode !== null) {
|
|
38220
|
-
notes.push("Nothing here ended this run: it stopped on its own or something outside this server stopped it.");
|
|
38339
|
+
notes.push(run.exitCode === 0 ? "This run quit on its own, cleanly: exit code 0." : "Nothing here ended this run: it stopped on its own or something outside this server stopped it.");
|
|
38221
38340
|
}
|
|
38222
38341
|
if (run.transcript !== null && selected.omitted > 0) {
|
|
38223
38342
|
notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: running above is asked of the operating system, and a transcript that stops growing is a run between prints.`);
|
|
@@ -38235,9 +38354,12 @@ class GodotServer {
|
|
|
38235
38354
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
|
38236
38355
|
project: !run.throughEditor && run.process === null ? run.projectPath ?? undefined : undefined,
|
|
38237
38356
|
transcript: run.transcript ?? undefined,
|
|
38357
|
+
startedAt: new Date(run.startedAt).toISOString(),
|
|
38358
|
+
elapsedMs: Date.now() - run.startedAt,
|
|
38359
|
+
cpuSeconds,
|
|
38238
38360
|
note: notes.length > 0 ? notes.join(" ") : undefined,
|
|
38239
38361
|
omitted: selected.omitted,
|
|
38240
|
-
entries: selected.entries
|
|
38362
|
+
entries: forAnswer(selected.entries)
|
|
38241
38363
|
});
|
|
38242
38364
|
}
|
|
38243
38365
|
async handleStopProject() {
|
|
@@ -38257,7 +38379,7 @@ class GodotServer {
|
|
|
38257
38379
|
errors: stopped.log.count("error"),
|
|
38258
38380
|
warnings: stopped.log.count("warning"),
|
|
38259
38381
|
clean: stopped.log.count("error") === 0,
|
|
38260
|
-
entries: stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries
|
|
38382
|
+
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
38261
38383
|
});
|
|
38262
38384
|
}
|
|
38263
38385
|
async handleRescanFilesystem(args) {
|
|
@@ -38442,7 +38564,7 @@ async function runGodotServer() {
|
|
|
38442
38564
|
const server = new GodotServer;
|
|
38443
38565
|
await server.run();
|
|
38444
38566
|
}
|
|
38445
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250,
|
|
38567
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run6, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
38446
38568
|
var init_server3 = __esm(() => {
|
|
38447
38569
|
init_mcp();
|
|
38448
38570
|
init_stdio2();
|
|
@@ -38455,11 +38577,13 @@ var init_server3 = __esm(() => {
|
|
|
38455
38577
|
init_godot_bridge();
|
|
38456
38578
|
init_godot_path();
|
|
38457
38579
|
init_headless();
|
|
38580
|
+
init_headless_operations();
|
|
38458
38581
|
init_issues();
|
|
38459
38582
|
init_junit();
|
|
38460
38583
|
init_lsp_client();
|
|
38461
38584
|
init_paths();
|
|
38462
38585
|
init_ports();
|
|
38586
|
+
init_process_time();
|
|
38463
38587
|
init_project_scan();
|
|
38464
38588
|
init_resources();
|
|
38465
38589
|
init_run_record();
|
|
@@ -38468,7 +38592,7 @@ var init_server3 = __esm(() => {
|
|
|
38468
38592
|
init_setup();
|
|
38469
38593
|
init_tool_definitions();
|
|
38470
38594
|
init_update_check();
|
|
38471
|
-
|
|
38595
|
+
run6 = promisify6(execFile6);
|
|
38472
38596
|
__dirname2 = dirname9(fileURLToPath4(import.meta.url));
|
|
38473
38597
|
VALUE_NAMES = {
|
|
38474
38598
|
string: "a string",
|
|
@@ -38480,44 +38604,19 @@ var init_server3 = __esm(() => {
|
|
|
38480
38604
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
38481
38605
|
"Point projectPath at the project the file belongs to"
|
|
38482
38606
|
];
|
|
38483
|
-
PROJECT_FILE_ARGUMENTS = [
|
|
38607
|
+
PROJECT_FILE_ARGUMENTS = [
|
|
38608
|
+
"scenePath",
|
|
38609
|
+
"scriptPath",
|
|
38610
|
+
"resourcePath",
|
|
38611
|
+
"newPath",
|
|
38612
|
+
"path",
|
|
38613
|
+
"script"
|
|
38614
|
+
];
|
|
38484
38615
|
DEBUG_STATE_CALLS = dictionary({
|
|
38485
38616
|
stack: "dap_get_stack_trace",
|
|
38486
38617
|
output: "dap_get_output",
|
|
38487
38618
|
variables: "dap_get_variables"
|
|
38488
38619
|
});
|
|
38489
|
-
HEADLESS_OPERATIONS = dictionary({
|
|
38490
|
-
project_settings: dictionary({
|
|
38491
|
-
get: "get_project_setting",
|
|
38492
|
-
set: "set_project_setting",
|
|
38493
|
-
add_autoload: "add_autoload",
|
|
38494
|
-
remove_autoload: "remove_autoload",
|
|
38495
|
-
set_main_scene: "set_main_scene",
|
|
38496
|
-
add_input_action: "add_input_action",
|
|
38497
|
-
enable_plugin: "enable_plugin",
|
|
38498
|
-
disable_plugin: "disable_plugin",
|
|
38499
|
-
add_audio_bus: "create_audio_bus",
|
|
38500
|
-
set_audio_bus_effect: "set_audio_bus_effect",
|
|
38501
|
-
set_audio_bus_volume: "set_audio_bus_volume"
|
|
38502
|
-
}),
|
|
38503
|
-
project_import: dictionary({
|
|
38504
|
-
status: "get_import_status",
|
|
38505
|
-
options: "get_import_options",
|
|
38506
|
-
set_options: "set_import_options",
|
|
38507
|
-
reimport: "reimport_resource",
|
|
38508
|
-
uid: "get_uid",
|
|
38509
|
-
refresh_uids: "resave_resources",
|
|
38510
|
-
refresh_classes: "refresh_class_cache"
|
|
38511
|
-
}),
|
|
38512
|
-
project_export: dictionary({ list: "list_export_presets" }),
|
|
38513
|
-
script_edit: dictionary({ create: "create_script", modify: "modify_script" }),
|
|
38514
|
-
script_info: dictionary({ structure: "get_script_info" }),
|
|
38515
|
-
editor_classes: dictionary({
|
|
38516
|
-
query: "query_classes",
|
|
38517
|
-
info: "query_class_info",
|
|
38518
|
-
inheritance: "inspect_inheritance"
|
|
38519
|
-
})
|
|
38520
|
-
});
|
|
38521
38620
|
PROJECT_INFO_SECTIONS = dictionary({
|
|
38522
38621
|
autoloads: { operation: "list_autoloads", params: () => ({}) },
|
|
38523
38622
|
plugins: { operation: "list_plugins", params: () => ({}) },
|
|
@@ -39840,6 +39939,7 @@ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as read
|
|
|
39840
39939
|
import { basename, dirname as dirname4, join as join9 } from "node:path";
|
|
39841
39940
|
|
|
39842
39941
|
// src/tool-reference.ts
|
|
39942
|
+
init_headless_operations();
|
|
39843
39943
|
init_tool_definitions();
|
|
39844
39944
|
function namedType(declared) {
|
|
39845
39945
|
if (typeof declared === "string") {
|
|
@@ -39850,6 +39950,22 @@ function namedType(declared) {
|
|
|
39850
39950
|
}
|
|
39851
39951
|
return "any";
|
|
39852
39952
|
}
|
|
39953
|
+
function headlessSection() {
|
|
39954
|
+
const byTool = Object.entries(HEADLESS_OPERATIONS).map(([tool, ops]) => `\`${tool}\` (${Object.keys(ops).join(", ")})`).sort();
|
|
39955
|
+
return [
|
|
39956
|
+
"## What a gate can call",
|
|
39957
|
+
"",
|
|
39958
|
+
"These answer from a short-lived headless engine: no editor window, no port, no running game,",
|
|
39959
|
+
"and nothing left behind. They are the only calls worth putting in a push gate, because every",
|
|
39960
|
+
"other tool needs an editor somebody has open or a game somebody is running.",
|
|
39961
|
+
"",
|
|
39962
|
+
...byTool.map((line) => `- ${line}`),
|
|
39963
|
+
"",
|
|
39964
|
+
"Everything else needs the editor (`editor_*`, `debug_*`, `scene_*`, `resource_*`) or a running",
|
|
39965
|
+
"game (`runtime_*`).",
|
|
39966
|
+
""
|
|
39967
|
+
];
|
|
39968
|
+
}
|
|
39853
39969
|
function renderToolsMarkdown() {
|
|
39854
39970
|
const lines = [
|
|
39855
39971
|
"# Tools",
|
|
@@ -39860,7 +39976,8 @@ function renderToolsMarkdown() {
|
|
|
39860
39976
|
"Every call takes `projectPath`, except the `runtime_*` and `debug_*` tools, where it picks",
|
|
39861
39977
|
"between running games. Answers are read from the engine after the change, not echoed from the",
|
|
39862
39978
|
"request. Engine stderr comes back under `engine_messages`.",
|
|
39863
|
-
""
|
|
39979
|
+
"",
|
|
39980
|
+
...headlessSection()
|
|
39864
39981
|
];
|
|
39865
39982
|
for (const tool of TOOL_SPECS) {
|
|
39866
39983
|
lines.push(`## ${tool.name}`, "", tool.description, "");
|
|
@@ -39951,7 +40068,8 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
39951
40068
|
| Answer a dialog | \`runtime_input click\` on its button, or \`key\` Escape to dismiss it: an [AcceptDialog] reads that key itself and never asks the InputMap, so \`ui_cancel\` leaves it standing |
|
|
39952
40069
|
| Press a bound key | \`runtime_input action\` or \`key\`, a whole press unless you hold it |
|
|
39953
40070
|
| Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
|
|
39954
|
-
| Wait for something | \`runtime_wait\`, never a sleep; \`until\` with \`says\` for a panel that rebuilds its own labels |
|
|
40071
|
+
| Wait for something in a running game | \`runtime_wait\`, never a sleep; \`until\` with \`says\` for a panel that rebuilds its own labels |
|
|
40072
|
+
| Wait for a run to finish | \`editor_run wait\`, never a sleep or a shell loop on the pid: it answers as \`editor_output\` does, once the run is over |
|
|
39955
40073
|
| A picture, for a person who asked to see one | \`runtime_capture\` |
|
|
39956
40074
|
|
|
39957
40075
|
These need the runtime autoload, which \`gdharness setup\` registers. \`runtime_capture\` needs a
|
|
@@ -12,9 +12,15 @@ func _init(p_log: Log) -> void:
|
|
|
12
12
|
_log = p_log
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
# Get a project setting value
|
|
15
|
+
# Get a project setting value, or every setting under a prefix.
|
|
16
16
|
func get_project_setting(params: Dictionary) -> Dictionary:
|
|
17
|
+
var prefix: String = str(params.get("prefix", ""))
|
|
18
|
+
if not prefix.is_empty():
|
|
19
|
+
return _settings_under(prefix)
|
|
20
|
+
|
|
17
21
|
var setting_path: String = str(params.get("setting", ""))
|
|
22
|
+
if setting_path.is_empty():
|
|
23
|
+
return _log.failure("setting is required, or prefix to read every setting under one")
|
|
18
24
|
|
|
19
25
|
_log.info("Getting project setting: " + setting_path)
|
|
20
26
|
|
|
@@ -31,6 +37,37 @@ func get_project_setting(params: Dictionary) -> Dictionary:
|
|
|
31
37
|
return result
|
|
32
38
|
|
|
33
39
|
|
|
40
|
+
# Every setting whose name starts with [prefix], with the type the engine registers for each.
|
|
41
|
+
#
|
|
42
|
+
# A caller after a whole family, "everything under debug/gdscript/warnings", had no way to ask: a get
|
|
43
|
+
# takes one name, and the engine is the only thing that knows what the family contains. The way round
|
|
44
|
+
# it was to walk ProjectSettings.get_property_list() from a script written for the purpose, which is
|
|
45
|
+
# an engine start to answer a question the engine was already open for.
|
|
46
|
+
#
|
|
47
|
+
# The type is here because it is the half a name hides. A setting sitting among a family of levels
|
|
48
|
+
# can be a bool, and a caller that assumes otherwise writes a level over it and sees nothing go
|
|
49
|
+
# wrong: 2 is as true as true is. Reading it out of the property list is how that is caught.
|
|
50
|
+
func _settings_under(prefix: String) -> Dictionary:
|
|
51
|
+
_log.info("Listing project settings under: " + prefix)
|
|
52
|
+
|
|
53
|
+
var found: Array[Dictionary] = []
|
|
54
|
+
for property: Dictionary in ProjectSettings.get_property_list():
|
|
55
|
+
var name: String = str(property.get("name", ""))
|
|
56
|
+
if not name.begins_with(prefix):
|
|
57
|
+
continue
|
|
58
|
+
var kind: int = Read.as_int(property.get("type", TYPE_NIL), TYPE_NIL)
|
|
59
|
+
var value: Variant = ProjectSettings.get_setting(name)
|
|
60
|
+
var described: Dictionary = {
|
|
61
|
+
"setting": name,
|
|
62
|
+
"type": type_string(kind),
|
|
63
|
+
"value": _values.serialize_value(value),
|
|
64
|
+
}
|
|
65
|
+
found.append(described)
|
|
66
|
+
|
|
67
|
+
found.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a["setting"] < b["setting"])
|
|
68
|
+
return {"prefix": prefix, "count": found.size(), "settings": found}
|
|
69
|
+
|
|
70
|
+
|
|
34
71
|
# Set a project setting value
|
|
35
72
|
func set_project_setting(params: Dictionary) -> Dictionary:
|
|
36
73
|
var setting_path: String = str(params.get("setting", ""))
|
package/build/index.js
CHANGED
|
@@ -10138,7 +10138,7 @@ function defectReport(where, error, godotVersion) {
|
|
|
10138
10138
|
}
|
|
10139
10139
|
|
|
10140
10140
|
// src/server.ts
|
|
10141
|
-
import { execFile as
|
|
10141
|
+
import { execFile as execFile4, spawn } from "node:child_process";
|
|
10142
10142
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10143
10143
|
import {
|
|
10144
10144
|
closeSync,
|
|
@@ -10157,7 +10157,7 @@ import { tmpdir as tmpdir4 } from "node:os";
|
|
|
10157
10157
|
import { basename as basename2, dirname as dirname4, join as join10, normalize as normalize2 } from "node:path";
|
|
10158
10158
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10159
10159
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10160
|
-
import { promisify as
|
|
10160
|
+
import { promisify as promisify4 } from "node:util";
|
|
10161
10161
|
|
|
10162
10162
|
// node_modules/zod/v3/helpers/util.js
|
|
10163
10163
|
var util;
|
|
@@ -25450,6 +25450,9 @@ function announced(line) {
|
|
|
25450
25450
|
return { severity: match[2] === "WARNING" ? "warning" : "error", text: match[3] ?? "" };
|
|
25451
25451
|
}
|
|
25452
25452
|
var STREAMS = ["stdout", "stderr", "transcript"];
|
|
25453
|
+
function forAnswer(entries) {
|
|
25454
|
+
return entries.map((entry) => entry.detail.length === 0 ? { index: entry.index, severity: entry.severity, source: entry.source, text: entry.text } : entry);
|
|
25455
|
+
}
|
|
25453
25456
|
|
|
25454
25457
|
class GameLog {
|
|
25455
25458
|
entries = [];
|
|
@@ -26356,6 +26359,40 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
26356
26359
|
return { ok: true, payload, messages: problems(stderr) };
|
|
26357
26360
|
}
|
|
26358
26361
|
|
|
26362
|
+
// src/headless-operations.ts
|
|
26363
|
+
var HEADLESS_OPERATIONS = dictionary({
|
|
26364
|
+
project_settings: dictionary({
|
|
26365
|
+
get: "get_project_setting",
|
|
26366
|
+
set: "set_project_setting",
|
|
26367
|
+
add_autoload: "add_autoload",
|
|
26368
|
+
remove_autoload: "remove_autoload",
|
|
26369
|
+
set_main_scene: "set_main_scene",
|
|
26370
|
+
add_input_action: "add_input_action",
|
|
26371
|
+
enable_plugin: "enable_plugin",
|
|
26372
|
+
disable_plugin: "disable_plugin",
|
|
26373
|
+
add_audio_bus: "create_audio_bus",
|
|
26374
|
+
set_audio_bus_effect: "set_audio_bus_effect",
|
|
26375
|
+
set_audio_bus_volume: "set_audio_bus_volume"
|
|
26376
|
+
}),
|
|
26377
|
+
project_import: dictionary({
|
|
26378
|
+
status: "get_import_status",
|
|
26379
|
+
options: "get_import_options",
|
|
26380
|
+
set_options: "set_import_options",
|
|
26381
|
+
reimport: "reimport_resource",
|
|
26382
|
+
uid: "get_uid",
|
|
26383
|
+
refresh_uids: "resave_resources",
|
|
26384
|
+
refresh_classes: "refresh_class_cache"
|
|
26385
|
+
}),
|
|
26386
|
+
project_export: dictionary({ list: "list_export_presets" }),
|
|
26387
|
+
script_edit: dictionary({ create: "create_script", modify: "modify_script" }),
|
|
26388
|
+
script_info: dictionary({ structure: "get_script_info" }),
|
|
26389
|
+
editor_classes: dictionary({
|
|
26390
|
+
query: "query_classes",
|
|
26391
|
+
info: "query_class_info",
|
|
26392
|
+
inheritance: "inspect_inheritance"
|
|
26393
|
+
})
|
|
26394
|
+
});
|
|
26395
|
+
|
|
26359
26396
|
// src/issues.ts
|
|
26360
26397
|
import { createHash as createHash2 } from "node:crypto";
|
|
26361
26398
|
import process5 from "node:process";
|
|
@@ -27212,6 +27249,43 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
27212
27249
|
}
|
|
27213
27250
|
}
|
|
27214
27251
|
|
|
27252
|
+
// src/process-time.ts
|
|
27253
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
27254
|
+
import { promisify as promisify3 } from "node:util";
|
|
27255
|
+
var run3 = promisify3(execFile3);
|
|
27256
|
+
var ASK_TIMEOUT_MS = 2000;
|
|
27257
|
+
async function cpuSecondsOf(pid) {
|
|
27258
|
+
try {
|
|
27259
|
+
return process.platform === "win32" ? await askWindows(pid) : await askPosix(pid);
|
|
27260
|
+
} catch {
|
|
27261
|
+
return;
|
|
27262
|
+
}
|
|
27263
|
+
}
|
|
27264
|
+
async function askWindows(pid) {
|
|
27265
|
+
const { stdout } = await run3("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `(Get-Process -Id ${pid}).CPU`], { timeout: ASK_TIMEOUT_MS, windowsHide: true });
|
|
27266
|
+
const seconds = Number(stdout.trim().replace(",", "."));
|
|
27267
|
+
return Number.isFinite(seconds) ? seconds : undefined;
|
|
27268
|
+
}
|
|
27269
|
+
async function askPosix(pid) {
|
|
27270
|
+
const { stdout } = await run3("ps", ["-o", "time=", "-p", String(pid)], { timeout: ASK_TIMEOUT_MS });
|
|
27271
|
+
return secondsFromClock(stdout.trim());
|
|
27272
|
+
}
|
|
27273
|
+
function secondsFromClock(printed) {
|
|
27274
|
+
if (printed === "") {
|
|
27275
|
+
return;
|
|
27276
|
+
}
|
|
27277
|
+
const dashed = printed.includes("-");
|
|
27278
|
+
const days = dashed ? printed.slice(0, printed.indexOf("-")) : null;
|
|
27279
|
+
const clock = dashed ? printed.slice(printed.indexOf("-") + 1) : printed;
|
|
27280
|
+
const parts = clock.split(":").map((part) => Number(part));
|
|
27281
|
+
if (parts.length === 0 || parts.some((part) => !Number.isFinite(part))) {
|
|
27282
|
+
return;
|
|
27283
|
+
}
|
|
27284
|
+
const seconds = parts.reduce((total, part) => total * 60 + part, 0);
|
|
27285
|
+
const extra = days === null ? 0 : Number(days) * 86400;
|
|
27286
|
+
return Number.isFinite(extra) ? seconds + extra : undefined;
|
|
27287
|
+
}
|
|
27288
|
+
|
|
27215
27289
|
// src/project-scan.ts
|
|
27216
27290
|
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
27217
27291
|
import { join as join6 } from "node:path";
|
|
@@ -27562,6 +27636,11 @@ function readPositiveNumber(params, key) {
|
|
|
27562
27636
|
const number = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
|
|
27563
27637
|
return typeof number === "number" && Number.isFinite(number) && number > 0 ? number : undefined;
|
|
27564
27638
|
}
|
|
27639
|
+
function readNonNegativeNumber(params, key) {
|
|
27640
|
+
const value = params[key];
|
|
27641
|
+
const number = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
|
|
27642
|
+
return typeof number === "number" && Number.isFinite(number) && number >= 0 ? number : undefined;
|
|
27643
|
+
}
|
|
27565
27644
|
function readBoolean(params, key) {
|
|
27566
27645
|
const value = params[key];
|
|
27567
27646
|
if (typeof value === "boolean") {
|
|
@@ -28205,6 +28284,11 @@ var TOOL_SPECS = [
|
|
|
28205
28284
|
parameters: {
|
|
28206
28285
|
projectPath: PROJECT_PATH,
|
|
28207
28286
|
setting: { type: "string", description: 'Setting path, such as "display/window/size/viewport_width".' },
|
|
28287
|
+
prefix: {
|
|
28288
|
+
type: "string",
|
|
28289
|
+
ops: ["get"],
|
|
28290
|
+
description: 'get: answer with every setting whose name starts with this, and the type the engine registers for each, instead of one setting by name. "debug/gdscript/warnings/" answers the whole family. The type matters: a family of levels can hold a setting that is a bool, and writing a level over it looks like it worked.'
|
|
28291
|
+
},
|
|
28208
28292
|
value: {
|
|
28209
28293
|
blank: true,
|
|
28210
28294
|
description: 'The value to write. Engine types may be tagged, {"_type": "Vector2", "x": 1, "y": 2}. "" writes an empty string.'
|
|
@@ -28226,7 +28310,7 @@ var TOOL_SPECS = [
|
|
|
28226
28310
|
},
|
|
28227
28311
|
requires: ["projectPath"],
|
|
28228
28312
|
operations: {
|
|
28229
|
-
get: { summary: "read one setting", requires: [
|
|
28313
|
+
get: { summary: "read one setting, or every setting under prefix", requires: [] },
|
|
28230
28314
|
set: { summary: "write one setting", requires: ["setting", "value"] },
|
|
28231
28315
|
add_autoload: { summary: "register an autoload singleton", requires: ["name", "path"] },
|
|
28232
28316
|
remove_autoload: { summary: "unregister an autoload", requires: ["name"] },
|
|
@@ -28633,7 +28717,7 @@ var TOOL_SPECS = [
|
|
|
28633
28717
|
runtimeWaitMs: {
|
|
28634
28718
|
type: "number",
|
|
28635
28719
|
ops: ["start"],
|
|
28636
|
-
description: "start: how long to wait for the game to announce its runtime before answering. Default 5000. Worth raising for a project that takes longer than that to reach its first frame, which is what leaves runtime listening false on a game whose runtime arrives a moment later."
|
|
28720
|
+
description: "start: how long to wait for the game to announce its runtime before answering. Default 5000. Worth raising for a project that takes longer than that to reach its first frame, which is what leaves runtime listening false on a game whose runtime arrives a moment later. 0 does not wait at all, which is the one to pass for a scene that announces nothing by construction, such as a bench that prints and quits."
|
|
28637
28721
|
},
|
|
28638
28722
|
frames: {
|
|
28639
28723
|
type: "number",
|
|
@@ -28642,15 +28726,16 @@ var TOOL_SPECS = [
|
|
|
28642
28726
|
},
|
|
28643
28727
|
timeoutMs: {
|
|
28644
28728
|
type: "number",
|
|
28645
|
-
ops: ["check"],
|
|
28646
|
-
description: "check: how long to give the boot before it is called hung. Default 60000."
|
|
28729
|
+
ops: ["check", "wait"],
|
|
28730
|
+
description: "check: how long to give the boot before it is called hung. Default 60000. wait: how long to wait for the run to end before answering anyway. Default 600000."
|
|
28647
28731
|
}
|
|
28648
28732
|
},
|
|
28649
28733
|
requires: ["projectPath"],
|
|
28650
28734
|
operations: {
|
|
28651
28735
|
start: { summary: "run the project until it quits or is stopped", requires: [] },
|
|
28652
28736
|
stop: { summary: "end the run and answer with what it printed last", requires: [] },
|
|
28653
|
-
check: { summary: "boot headless, quit after a few frames, and report the verdict", requires: [] }
|
|
28737
|
+
check: { summary: "boot headless, quit after a few frames, and report the verdict", requires: [] },
|
|
28738
|
+
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] }
|
|
28654
28739
|
},
|
|
28655
28740
|
defaultOperation: "start"
|
|
28656
28741
|
},
|
|
@@ -28667,6 +28752,10 @@ var TOOL_SPECS = [
|
|
|
28667
28752
|
type: "boolean",
|
|
28668
28753
|
description: "Only entries printed since the previous editor_output. Default false."
|
|
28669
28754
|
},
|
|
28755
|
+
cpu: {
|
|
28756
|
+
type: "boolean",
|
|
28757
|
+
description: "Also answer with cpuSeconds, the processor time the run has used. Default false, because asking costs a subprocess. Worth it when elapsedMs is climbing and nothing is being printed: processor time standing still is a run that has stopped doing anything, rather than one that is slow."
|
|
28758
|
+
},
|
|
28670
28759
|
contains: {
|
|
28671
28760
|
type: "string",
|
|
28672
28761
|
description: "Only entries mentioning this text, matched against everything the run has printed rather than against the entries this answer would otherwise carry: a line is found however much was printed after it."
|
|
@@ -29092,7 +29181,7 @@ function buildToolDefinitions() {
|
|
|
29092
29181
|
// src/server.ts
|
|
29093
29182
|
var UPDATE_NOTICE_EVERY = 500;
|
|
29094
29183
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
29095
|
-
var
|
|
29184
|
+
var run4 = promisify4(execFile4);
|
|
29096
29185
|
var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
|
|
29097
29186
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
29098
29187
|
var SLOWEST_FRAME_RATE = 20;
|
|
@@ -29198,11 +29287,20 @@ function wrongTypes(spec, args) {
|
|
|
29198
29287
|
}
|
|
29199
29288
|
var BRIDGE_RETRY_MS = 2000;
|
|
29200
29289
|
var SUCCESSOR_CHECK_MS = 1e4;
|
|
29290
|
+
var WAIT_FOR_RUN_MS = 600000;
|
|
29291
|
+
var RUN_POLL_MS = 250;
|
|
29201
29292
|
var PATH_SOLUTIONS = [
|
|
29202
29293
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
29203
29294
|
"Point projectPath at the project the file belongs to"
|
|
29204
29295
|
];
|
|
29205
|
-
var PROJECT_FILE_ARGUMENTS = [
|
|
29296
|
+
var PROJECT_FILE_ARGUMENTS = [
|
|
29297
|
+
"scenePath",
|
|
29298
|
+
"scriptPath",
|
|
29299
|
+
"resourcePath",
|
|
29300
|
+
"newPath",
|
|
29301
|
+
"path",
|
|
29302
|
+
"script"
|
|
29303
|
+
];
|
|
29206
29304
|
function escapingPropertyValue(properties) {
|
|
29207
29305
|
if (typeof properties !== "object" || properties === null) {
|
|
29208
29306
|
return;
|
|
@@ -29222,38 +29320,6 @@ var DEBUG_STATE_CALLS = dictionary({
|
|
|
29222
29320
|
output: "dap_get_output",
|
|
29223
29321
|
variables: "dap_get_variables"
|
|
29224
29322
|
});
|
|
29225
|
-
var HEADLESS_OPERATIONS = dictionary({
|
|
29226
|
-
project_settings: dictionary({
|
|
29227
|
-
get: "get_project_setting",
|
|
29228
|
-
set: "set_project_setting",
|
|
29229
|
-
add_autoload: "add_autoload",
|
|
29230
|
-
remove_autoload: "remove_autoload",
|
|
29231
|
-
set_main_scene: "set_main_scene",
|
|
29232
|
-
add_input_action: "add_input_action",
|
|
29233
|
-
enable_plugin: "enable_plugin",
|
|
29234
|
-
disable_plugin: "disable_plugin",
|
|
29235
|
-
add_audio_bus: "create_audio_bus",
|
|
29236
|
-
set_audio_bus_effect: "set_audio_bus_effect",
|
|
29237
|
-
set_audio_bus_volume: "set_audio_bus_volume"
|
|
29238
|
-
}),
|
|
29239
|
-
project_import: dictionary({
|
|
29240
|
-
status: "get_import_status",
|
|
29241
|
-
options: "get_import_options",
|
|
29242
|
-
set_options: "set_import_options",
|
|
29243
|
-
reimport: "reimport_resource",
|
|
29244
|
-
uid: "get_uid",
|
|
29245
|
-
refresh_uids: "resave_resources",
|
|
29246
|
-
refresh_classes: "refresh_class_cache"
|
|
29247
|
-
}),
|
|
29248
|
-
project_export: dictionary({ list: "list_export_presets" }),
|
|
29249
|
-
script_edit: dictionary({ create: "create_script", modify: "modify_script" }),
|
|
29250
|
-
script_info: dictionary({ structure: "get_script_info" }),
|
|
29251
|
-
editor_classes: dictionary({
|
|
29252
|
-
query: "query_classes",
|
|
29253
|
-
info: "query_class_info",
|
|
29254
|
-
inheritance: "inspect_inheritance"
|
|
29255
|
-
})
|
|
29256
|
-
});
|
|
29257
29323
|
var PROJECT_INFO_SECTIONS = dictionary({
|
|
29258
29324
|
autoloads: { operation: "list_autoloads", params: () => ({}) },
|
|
29259
29325
|
plugins: { operation: "list_plugins", params: () => ({}) },
|
|
@@ -29786,9 +29852,15 @@ class GodotServer {
|
|
|
29786
29852
|
case "editor_launch":
|
|
29787
29853
|
return op === "restart" ? await this.handleRestartEditor() : await this.handleLaunchEditor(args);
|
|
29788
29854
|
case "editor_run":
|
|
29789
|
-
|
|
29855
|
+
if (op === "stop") {
|
|
29856
|
+
return await this.handleStopProject();
|
|
29857
|
+
}
|
|
29858
|
+
if (op === "wait") {
|
|
29859
|
+
return await this.handleWaitForRun(args);
|
|
29860
|
+
}
|
|
29861
|
+
return await this.handleRunProject(args, op);
|
|
29790
29862
|
case "editor_output":
|
|
29791
|
-
return this.handleGetDebugOutput(args);
|
|
29863
|
+
return await this.handleGetDebugOutput(args);
|
|
29792
29864
|
case "editor_status":
|
|
29793
29865
|
return await this.handleEditorStatus();
|
|
29794
29866
|
case "editor_rescan":
|
|
@@ -30159,7 +30231,7 @@ class GodotServer {
|
|
|
30159
30231
|
const log = new GameLog;
|
|
30160
30232
|
let exitCode = 0;
|
|
30161
30233
|
try {
|
|
30162
|
-
const { stdout, stderr } = await
|
|
30234
|
+
const { stdout, stderr } = await run4(engine.value, exportArgs, { timeout: 300000 });
|
|
30163
30235
|
log.append("stdout", stdout);
|
|
30164
30236
|
log.append("stderr", stderr);
|
|
30165
30237
|
} catch (error) {
|
|
@@ -30183,7 +30255,7 @@ class GodotServer {
|
|
|
30183
30255
|
exitCode,
|
|
30184
30256
|
errors: log.count("error"),
|
|
30185
30257
|
warnings: log.count("warning"),
|
|
30186
|
-
entries: problems.entries
|
|
30258
|
+
entries: forAnswer(problems.entries)
|
|
30187
30259
|
};
|
|
30188
30260
|
if (!verdict.exported) {
|
|
30189
30261
|
return {
|
|
@@ -30300,7 +30372,7 @@ class GodotServer {
|
|
|
30300
30372
|
exitCode,
|
|
30301
30373
|
hung,
|
|
30302
30374
|
arguments: cmdArgs,
|
|
30303
|
-
entries: printed.slice(0, 60)
|
|
30375
|
+
entries: forAnswer(printed.slice(0, 60))
|
|
30304
30376
|
}, null, 2)
|
|
30305
30377
|
}
|
|
30306
30378
|
],
|
|
@@ -30354,7 +30426,7 @@ class GodotServer {
|
|
|
30354
30426
|
}
|
|
30355
30427
|
async godotVersion(godotPath) {
|
|
30356
30428
|
try {
|
|
30357
|
-
const { stdout } = await
|
|
30429
|
+
const { stdout } = await run4(godotPath, ["--version"], { timeout: 1e4 });
|
|
30358
30430
|
return stdout.trim();
|
|
30359
30431
|
} catch (error) {
|
|
30360
30432
|
this.logDebug(`Godot did not answer --version: ${errorMessage(error)}`);
|
|
@@ -30649,7 +30721,7 @@ class GodotServer {
|
|
|
30649
30721
|
platform: process.platform,
|
|
30650
30722
|
variables: process.env
|
|
30651
30723
|
});
|
|
30652
|
-
const runtimeWaitMs =
|
|
30724
|
+
const runtimeWaitMs = readNonNegativeNumber(args, "runtimeWaitMs") ?? ANNOUNCE_BUDGET_MS;
|
|
30653
30725
|
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
30654
30726
|
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0) {
|
|
30655
30727
|
return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
|
|
@@ -30971,10 +31043,24 @@ class GodotServer {
|
|
|
30971
31043
|
frames,
|
|
30972
31044
|
errors,
|
|
30973
31045
|
warnings,
|
|
30974
|
-
entries: boot.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries
|
|
31046
|
+
entries: forAnswer(boot.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
30975
31047
|
});
|
|
30976
31048
|
}
|
|
30977
|
-
|
|
31049
|
+
async handleWaitForRun(args) {
|
|
31050
|
+
const run = this.currentRun();
|
|
31051
|
+
if (!run) {
|
|
31052
|
+
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
31053
|
+
}
|
|
31054
|
+
const budgetMs = readPositiveNumber(args, "timeoutMs") ?? WAIT_FOR_RUN_MS;
|
|
31055
|
+
const until = Date.now() + budgetMs;
|
|
31056
|
+
while (stillRunning(run) && Date.now() < until) {
|
|
31057
|
+
this.drainEditorOutput(run);
|
|
31058
|
+
this.drainTranscript(run);
|
|
31059
|
+
await delay3(RUN_POLL_MS);
|
|
31060
|
+
}
|
|
31061
|
+
return await this.handleGetDebugOutput(args, stillRunning(run) ? budgetMs : undefined);
|
|
31062
|
+
}
|
|
31063
|
+
async handleGetDebugOutput(args, waitedMs) {
|
|
30978
31064
|
const run = this.currentRun();
|
|
30979
31065
|
if (!run) {
|
|
30980
31066
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
@@ -30993,14 +31079,19 @@ class GodotServer {
|
|
|
30993
31079
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
30994
31080
|
});
|
|
30995
31081
|
const halt = run.throughEditor ? this.dapClient?.whereItStopped() ?? null : null;
|
|
31082
|
+
const wanted = readBoolean(args, "cpu") ?? false;
|
|
31083
|
+
const cpuSeconds = wanted && run.pid !== null && stillRunning(run) ? await cpuSecondsOf(run.pid) : undefined;
|
|
30996
31084
|
const notes = [];
|
|
31085
|
+
if (waitedMs !== undefined) {
|
|
31086
|
+
notes.push(`The wait of ${waitedMs}ms ran out and the run is still going, so this is what it had printed by then rather than everything it will print. editor_run wait again to keep waiting.`);
|
|
31087
|
+
}
|
|
30997
31088
|
if (run.endedUnwatched === true) {
|
|
30998
31089
|
notes.push("This run outlived the server that started it and is over now, so its exit code was never collected. Everything it printed is below, read back from its transcript.");
|
|
30999
31090
|
}
|
|
31000
31091
|
if (typeof run.endedHere === "string") {
|
|
31001
31092
|
notes.push(`This run was ended here, by ${run.endedHere}.`);
|
|
31002
31093
|
} else if (!stillRunning(run) && !run.throughEditor && run.exitCode !== null) {
|
|
31003
|
-
notes.push("Nothing here ended this run: it stopped on its own or something outside this server stopped it.");
|
|
31094
|
+
notes.push(run.exitCode === 0 ? "This run quit on its own, cleanly: exit code 0." : "Nothing here ended this run: it stopped on its own or something outside this server stopped it.");
|
|
31004
31095
|
}
|
|
31005
31096
|
if (run.transcript !== null && selected.omitted > 0) {
|
|
31006
31097
|
notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: running above is asked of the operating system, and a transcript that stops growing is a run between prints.`);
|
|
@@ -31018,9 +31109,12 @@ class GodotServer {
|
|
|
31018
31109
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
|
31019
31110
|
project: !run.throughEditor && run.process === null ? run.projectPath ?? undefined : undefined,
|
|
31020
31111
|
transcript: run.transcript ?? undefined,
|
|
31112
|
+
startedAt: new Date(run.startedAt).toISOString(),
|
|
31113
|
+
elapsedMs: Date.now() - run.startedAt,
|
|
31114
|
+
cpuSeconds,
|
|
31021
31115
|
note: notes.length > 0 ? notes.join(" ") : undefined,
|
|
31022
31116
|
omitted: selected.omitted,
|
|
31023
|
-
entries: selected.entries
|
|
31117
|
+
entries: forAnswer(selected.entries)
|
|
31024
31118
|
});
|
|
31025
31119
|
}
|
|
31026
31120
|
async handleStopProject() {
|
|
@@ -31040,7 +31134,7 @@ class GodotServer {
|
|
|
31040
31134
|
errors: stopped.log.count("error"),
|
|
31041
31135
|
warnings: stopped.log.count("warning"),
|
|
31042
31136
|
clean: stopped.log.count("error") === 0,
|
|
31043
|
-
entries: stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries
|
|
31137
|
+
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
31044
31138
|
});
|
|
31045
31139
|
}
|
|
31046
31140
|
async handleRescanFilesystem(args) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
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",
|