summer-engine 3.2.1 → 3.4.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +4 -2
- package/.codex-plugin/plugin.json +5 -3
- package/.cursor-plugin/plugin.json +4 -2
- package/.factory-plugin/plugin.json +4 -2
- package/CHANGELOG.md +61 -0
- package/README.md +7 -4
- package/assets/api-docs.json.gz +0 -0
- package/dist/cli/commands/install.d.ts +10 -0
- package/dist/cli/commands/install.js +55 -10
- package/dist/cli/commands/login.js +12 -0
- package/dist/cli/commands/publish.js +2 -1
- package/dist/cli/commands/tool.d.ts +7 -0
- package/dist/cli/commands/tool.js +36 -1
- package/dist/core/auth.js +3 -0
- package/dist/core/capabilities/audit/args.d.ts +14 -14
- package/dist/core/capabilities/capture-gameplay.d.ts +69 -0
- package/dist/core/capabilities/capture-gameplay.js +283 -0
- package/dist/core/capabilities/creator.js +1 -1
- package/dist/core/capabilities/doctor.d.ts +9 -1
- package/dist/core/capabilities/doctor.js +47 -4
- package/dist/core/capabilities/engine-run.d.ts +12 -0
- package/dist/core/capabilities/engine-run.js +61 -0
- package/dist/core/capabilities/export-engine-version.d.ts +15 -0
- package/dist/core/capabilities/export-engine-version.js +24 -0
- package/dist/core/capabilities/export-game.d.ts +107 -0
- package/dist/core/capabilities/export-game.js +445 -0
- package/dist/core/capabilities/export-presets.d.ts +50 -0
- package/dist/core/capabilities/export-presets.js +177 -0
- package/dist/core/capabilities/export-templates.d.ts +118 -0
- package/dist/core/capabilities/export-templates.js +291 -0
- package/dist/core/capabilities/image-generation.d.ts +9 -0
- package/dist/core/capabilities/image-generation.js +19 -1
- package/dist/core/capabilities/publish-build.d.ts +77 -0
- package/dist/core/capabilities/publish-build.js +500 -0
- package/dist/core/capabilities/publish-download.d.ts +37 -0
- package/dist/core/capabilities/publish-download.js +168 -0
- package/dist/core/capabilities/runtime-control.d.ts +10 -10
- package/dist/core/capabilities/summer-bundle.d.ts +49 -0
- package/dist/core/capabilities/summer-bundle.js +156 -0
- package/dist/core/capabilities/tool-dispatch.js +43 -1
- package/dist/core/capabilities/ui-control.d.ts +2 -2
- package/dist/core/config.js +2 -2
- package/dist/core/engine-install.d.ts +16 -0
- package/dist/core/engine-install.js +32 -2
- package/dist/core/hosted-mcp.d.ts +115 -0
- package/dist/core/hosted-mcp.js +164 -0
- package/dist/core/launch-posture.d.ts +3 -0
- package/dist/core/launch-posture.js +15 -2
- package/dist/core/library-read.d.ts +2 -2
- package/dist/core/oauth.d.ts +78 -0
- package/dist/core/oauth.js +309 -0
- package/dist/core/util/zip-write.d.ts +18 -0
- package/dist/core/util/zip-write.js +112 -0
- package/dist/core/util/zip.d.ts +22 -0
- package/dist/core/util/zip.js +140 -0
- package/dist/mcp/hosted-mount.d.ts +50 -0
- package/dist/mcp/hosted-mount.js +200 -0
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.js +14 -6
- package/dist/mcp/tools/creator-tools.js +64 -1
- package/dist/mcp/tools/generate-tools.js +12 -2
- package/gemini-extension.json +3 -1
- package/library/references/mcp-tools-reference/mcp-tools-reference.md +7 -3
- package/library/skills/README.md +6 -4
- package/library/skills/bounded-engine-runs/SKILL.md +96 -0
- package/library/skills/bounded-engine-runs/resource.yaml +29 -0
- package/library/skills/compatibility-renderer-traps/SKILL.md +78 -0
- package/library/skills/compatibility-renderer-traps/resource.yaml +28 -0
- package/library/skills/concept-art/SKILL.md +2 -2
- package/library/skills/export-and-ship/SKILL.md +4 -0
- package/library/skills/headless-scripting/resource.yaml +1 -0
- package/library/skills/in-game-purchases/SKILL.md +123 -118
- package/library/skills/in-game-purchases/resource.yaml +2 -2
- package/library/skills/multiplayer-publish/SKILL.md +5 -1
- package/library/skills/navigate-summer/SKILL.md +1 -1
- package/library/skills/web-compatibility-look/resource.yaml +1 -0
- package/library/tools/capture-gameplay/resource.yaml +73 -0
- package/library/tools/creator-publish/resource.yaml +4 -3
- package/library/tools/creator-releases/resource.yaml +1 -1
- package/library/tools/export-game/resource.yaml +72 -0
- package/library/tools/export-templates/resource.yaml +63 -0
- package/library/tools/generate-image/resource.yaml +15 -1
- package/library/tools/open/resource.yaml +1 -1
- package/library/tools/publish-build/resource.yaml +78 -0
- package/package.json +1 -1
- package/registry/generated/aliases.json +4 -0
- package/registry/generated/counts.json +3 -3
- package/registry/generated/gemini-extension.json +3 -1
- package/registry/generated/index.json +241 -19
- package/registry/generated/marketplace.claude.json +2 -2
- package/registry/generated/plugin.claude.json +4 -2
- package/registry/generated/plugin.codex.json +5 -3
- package/registry/generated/plugin.cursor.json +4 -2
- package/registry/generated/plugin.factory.json +4 -2
- package/registry/generated/skills-index.md +6 -4
- package/registry/generated/skills-registry.json +28 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type EngineRun } from "./engine-run.js";
|
|
2
|
+
/**
|
|
3
|
+
* summer_capture_gameplay: real gameplay frames without a running editor.
|
|
4
|
+
*
|
|
5
|
+
* Reuses the engine's offscreen verify instance (the one RunVerification and
|
|
6
|
+
* tests/autopilot spawn): a real renderer in a window parked offscreen with no
|
|
7
|
+
* focus, so it never shows on screen, unlike --headless, which has no pixels.
|
|
8
|
+
*
|
|
9
|
+
* <engine> --disable-crash-handler --summer-no-api --path <project> --resolution WxH
|
|
10
|
+
* [--scene res://...] --summer-verify <probe.gd>
|
|
11
|
+
* --summer-verify-out <dir> --summer-verify-max <s>
|
|
12
|
+
*
|
|
13
|
+
* The stock probe waits for the scene to draw and settle, saves PNG frames of
|
|
14
|
+
* the game viewport, and writes results.json. The frames are what the game
|
|
15
|
+
* really draws, HUD included: use them as store screenshots.
|
|
16
|
+
*/
|
|
17
|
+
export interface CaptureGameplayInput {
|
|
18
|
+
/** Project root (has project.godot). Defaults to the MCP's --project, then the working directory. */
|
|
19
|
+
project?: string;
|
|
20
|
+
/** Scene to start (res:// or uid://). Default: the project's main scene. */
|
|
21
|
+
scene?: string;
|
|
22
|
+
/** Window size, WIDTHxHEIGHT. Default 1920x1080; 1080x1920 for portrait. */
|
|
23
|
+
resolution?: string;
|
|
24
|
+
/** Frames to save. Default 1. */
|
|
25
|
+
frames?: number;
|
|
26
|
+
/** Seconds to let the game run before the first frame. Default 3. */
|
|
27
|
+
waitSeconds?: number;
|
|
28
|
+
/** Seconds between frames. Default 1. */
|
|
29
|
+
intervalSeconds?: number;
|
|
30
|
+
/** Output folder. Default <project>/.summer/captures/<time>/. */
|
|
31
|
+
out?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface CapturedFrame {
|
|
34
|
+
path: string;
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
}
|
|
38
|
+
export interface CaptureGameplayResult {
|
|
39
|
+
ok: true;
|
|
40
|
+
project: string;
|
|
41
|
+
scene: string;
|
|
42
|
+
resolution: string;
|
|
43
|
+
frames: CapturedFrame[];
|
|
44
|
+
out: string;
|
|
45
|
+
engine: string;
|
|
46
|
+
imported: boolean;
|
|
47
|
+
durationMs: number;
|
|
48
|
+
warnings?: string[];
|
|
49
|
+
next: string;
|
|
50
|
+
}
|
|
51
|
+
export interface CaptureGameplayDependencies {
|
|
52
|
+
findBinary: () => string | null;
|
|
53
|
+
run: (binary: string, args: string[], timeoutMs: number) => Promise<EngineRun>;
|
|
54
|
+
now: () => Date;
|
|
55
|
+
}
|
|
56
|
+
/** Width and height from a PNG's IHDR chunk, or null when the file is not a PNG. */
|
|
57
|
+
export declare function pngSize(bytes: Buffer): {
|
|
58
|
+
width: number;
|
|
59
|
+
height: number;
|
|
60
|
+
} | null;
|
|
61
|
+
/**
|
|
62
|
+
* The stock probe. Self-contained (extends Node, no base class to copy into the
|
|
63
|
+
* project); follows the verify-instance contract of assets/autopilot/probe_base.gd:
|
|
64
|
+
* the engine sets summer_out_dir and summer_max_seconds before _ready(), the
|
|
65
|
+
* first frames are empty until the scene has drawn twice, results.json is
|
|
66
|
+
* always written.
|
|
67
|
+
*/
|
|
68
|
+
export declare function captureProbeSource(frames: number, waitSeconds: number, intervalSeconds: number): string;
|
|
69
|
+
export declare function captureGameplay(input: CaptureGameplayInput, overrides?: Partial<CaptureGameplayDependencies>): Promise<CaptureGameplayResult>;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { platform } from "node:os";
|
|
4
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { findEngineBinary } from "../engine-install.js";
|
|
6
|
+
import { runEngine } from "./engine-run.js";
|
|
7
|
+
import { resolveExportProject } from "./export-game.js";
|
|
8
|
+
import { BuildToolError } from "./summer-bundle.js";
|
|
9
|
+
const defaultDependencies = {
|
|
10
|
+
findBinary: () => findEngineBinary(platform()),
|
|
11
|
+
run: runEngine,
|
|
12
|
+
now: () => new Date(),
|
|
13
|
+
};
|
|
14
|
+
const DEFAULT_RESOLUTION = "1920x1080";
|
|
15
|
+
const MAX_FRAMES = 10;
|
|
16
|
+
/** The engine clamps --summer-verify-max to 240 (main.cpp SUMMER_VERIFY_MAX_SECONDS). */
|
|
17
|
+
const ENGINE_MAX_SECONDS = 240;
|
|
18
|
+
const IMPORT_TIMEOUT_MS = 5 * 60_000;
|
|
19
|
+
const PROBE_FILE = "capture_probe.gd";
|
|
20
|
+
function invalid(message, recovery) {
|
|
21
|
+
return new BuildToolError("capture_args_invalid", message, recovery);
|
|
22
|
+
}
|
|
23
|
+
function parseResolution(value) {
|
|
24
|
+
const text = value?.trim() || DEFAULT_RESOLUTION;
|
|
25
|
+
const match = /^(\d{2,5})x(\d{2,5})$/i.exec(text);
|
|
26
|
+
if (!match)
|
|
27
|
+
throw invalid(`resolution "${text}" is not WIDTHxHEIGHT.`, 'Recovery: pass e.g. "1920x1080" (landscape) or "1080x1920" (portrait).');
|
|
28
|
+
const width = Number(match[1]);
|
|
29
|
+
const height = Number(match[2]);
|
|
30
|
+
if (width < 64 || height < 64 || width > 7680 || height > 7680) {
|
|
31
|
+
throw invalid(`resolution ${text} is out of range.`, "Recovery: use 64 to 7680 pixels per side.");
|
|
32
|
+
}
|
|
33
|
+
return { width, height, text: `${width}x${height}` };
|
|
34
|
+
}
|
|
35
|
+
function boundedNumber(value, fallback, min, max, name) {
|
|
36
|
+
if (value === undefined)
|
|
37
|
+
return fallback;
|
|
38
|
+
if (!Number.isFinite(value) || value < min || value > max) {
|
|
39
|
+
throw invalid(`${name} must be between ${min} and ${max}.`, `Recovery: pass ${name} in that range, or omit it.`);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
function checkScene(project, scene) {
|
|
44
|
+
const value = scene?.trim();
|
|
45
|
+
if (!value)
|
|
46
|
+
return undefined;
|
|
47
|
+
if (value.startsWith("uid://"))
|
|
48
|
+
return value;
|
|
49
|
+
if (!value.startsWith("res://") || !/\.(tscn|scn)$/i.test(value)) {
|
|
50
|
+
throw invalid(`scene "${value}" is not a res:// scene path.`, 'Recovery: pass a scene like "res://main.tscn", or omit scene to start the main scene.');
|
|
51
|
+
}
|
|
52
|
+
if (!existsSync(join(project, value.slice("res://".length)))) {
|
|
53
|
+
throw invalid(`${value} does not exist in ${project}.`, "Recovery: pass a scene file that exists, or omit scene to start the main scene.");
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function stamp(date) {
|
|
58
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z").replace("T", "-");
|
|
59
|
+
}
|
|
60
|
+
/** <project>/.summer/captures/, ignored by the editor and by git (like .summer/exports). */
|
|
61
|
+
async function capturesDir(project) {
|
|
62
|
+
const dir = join(project, ".summer", "captures");
|
|
63
|
+
await mkdir(dir, { recursive: true });
|
|
64
|
+
for (const [name, body] of [
|
|
65
|
+
[".gdignore", ""],
|
|
66
|
+
[".gitignore", "*\n"],
|
|
67
|
+
]) {
|
|
68
|
+
const file = join(dir, name);
|
|
69
|
+
if (!existsSync(file))
|
|
70
|
+
await writeFile(file, body);
|
|
71
|
+
}
|
|
72
|
+
return dir;
|
|
73
|
+
}
|
|
74
|
+
/** Width and height from a PNG's IHDR chunk, or null when the file is not a PNG. */
|
|
75
|
+
export function pngSize(bytes) {
|
|
76
|
+
if (bytes.length < 24 || bytes.readUInt32BE(0) !== 0x89504e47 || bytes.toString("ascii", 12, 16) !== "IHDR")
|
|
77
|
+
return null;
|
|
78
|
+
return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The stock probe. Self-contained (extends Node, no base class to copy into the
|
|
82
|
+
* project); follows the verify-instance contract of assets/autopilot/probe_base.gd:
|
|
83
|
+
* the engine sets summer_out_dir and summer_max_seconds before _ready(), the
|
|
84
|
+
* first frames are empty until the scene has drawn twice, results.json is
|
|
85
|
+
* always written.
|
|
86
|
+
*/
|
|
87
|
+
export function captureProbeSource(frames, waitSeconds, intervalSeconds) {
|
|
88
|
+
return `# Written by summer_capture_gameplay. Safe to delete.
|
|
89
|
+
# Fully typed: projects that treat GDScript warnings as errors must still load it.
|
|
90
|
+
extends Node
|
|
91
|
+
|
|
92
|
+
var summer_out_dir: String = ""
|
|
93
|
+
var summer_max_seconds: int = 20
|
|
94
|
+
|
|
95
|
+
const FRAMES: int = ${frames}
|
|
96
|
+
const WAIT_SECONDS: float = ${waitSeconds.toFixed(3)}
|
|
97
|
+
const INTERVAL_SECONDS: float = ${intervalSeconds.toFixed(3)}
|
|
98
|
+
|
|
99
|
+
var _shots: Array = []
|
|
100
|
+
var _warnings: Array = []
|
|
101
|
+
var _start_ms: int = 0
|
|
102
|
+
var _done: bool = false
|
|
103
|
+
|
|
104
|
+
func _ready() -> void:
|
|
105
|
+
\tprocess_mode = Node.PROCESS_MODE_ALWAYS
|
|
106
|
+
\t_start_ms = Time.get_ticks_msec()
|
|
107
|
+
\tget_tree().create_timer(float(summer_max_seconds), true).timeout.connect(_on_deadline)
|
|
108
|
+
\t_run.call_deferred()
|
|
109
|
+
|
|
110
|
+
func _on_deadline() -> void:
|
|
111
|
+
\t_finish(false)
|
|
112
|
+
|
|
113
|
+
func _run() -> void:
|
|
114
|
+
\t# The viewport holds no scene until two frames have been drawn.
|
|
115
|
+
\tvar guard: int = 0
|
|
116
|
+
\twhile Engine.get_frames_drawn() < 2 and guard < 600:
|
|
117
|
+
\t\tguard += 1
|
|
118
|
+
\t\tawait get_tree().process_frame
|
|
119
|
+
\tif DisplayServer.get_name() == "headless":
|
|
120
|
+
\t\t_warnings.append("No renderer (headless): no frame can be captured.")
|
|
121
|
+
\t\t_finish(true)
|
|
122
|
+
\t\treturn
|
|
123
|
+
\tawait get_tree().create_timer(WAIT_SECONDS, true).timeout
|
|
124
|
+
\tfor i: int in range(FRAMES):
|
|
125
|
+
\t\tif i > 0:
|
|
126
|
+
\t\t\tawait get_tree().create_timer(INTERVAL_SECONDS, true).timeout
|
|
127
|
+
\t\tawait RenderingServer.frame_post_draw
|
|
128
|
+
\t\tvar img: Image = get_viewport().get_texture().get_image()
|
|
129
|
+
\t\tvar file_name: String = "shot-%02d.png" % (i + 1)
|
|
130
|
+
\t\tif img == null:
|
|
131
|
+
\t\t\t_warnings.append("%s: the viewport returned no image." % file_name)
|
|
132
|
+
\t\t\tcontinue
|
|
133
|
+
\t\tif img.save_png(summer_out_dir.path_join(file_name)) == OK:
|
|
134
|
+
\t\t\t_shots.append({"file": file_name, "width": img.get_width(), "height": img.get_height()})
|
|
135
|
+
\t\telse:
|
|
136
|
+
\t\t\t_warnings.append("%s: could not write the PNG." % file_name)
|
|
137
|
+
\t_finish(true)
|
|
138
|
+
|
|
139
|
+
func _errors() -> Array:
|
|
140
|
+
\tvar errors: Array = []
|
|
141
|
+
\tvar path: String = summer_out_dir.path_join("errors.log")
|
|
142
|
+
\tif FileAccess.file_exists(path):
|
|
143
|
+
\t\tvar f: FileAccess = FileAccess.open(path, FileAccess.READ)
|
|
144
|
+
\t\tif f != null:
|
|
145
|
+
\t\t\twhile not f.eof_reached():
|
|
146
|
+
\t\t\t\tvar line: String = f.get_line()
|
|
147
|
+
\t\t\t\tif line.strip_edges() != "":
|
|
148
|
+
\t\t\t\t\terrors.append(line)
|
|
149
|
+
\treturn errors
|
|
150
|
+
|
|
151
|
+
func _finish(finished: bool) -> void:
|
|
152
|
+
\tif _done:
|
|
153
|
+
\t\treturn
|
|
154
|
+
\t_done = true
|
|
155
|
+
\tvar out: Dictionary = {"shots": _shots, "frame_warnings": _warnings, "errors_seen": _errors(), "duration_ms": Time.get_ticks_msec() - _start_ms, "finished": finished}
|
|
156
|
+
\tvar f: FileAccess = FileAccess.open(summer_out_dir.path_join("results.json"), FileAccess.WRITE)
|
|
157
|
+
\tif f != null:
|
|
158
|
+
\t\tf.store_string(JSON.stringify(out, " "))
|
|
159
|
+
\t\tf.close()
|
|
160
|
+
\tget_tree().quit()
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
163
|
+
function outputTail(output) {
|
|
164
|
+
return output.trim().split("\n").slice(-30).join("\n");
|
|
165
|
+
}
|
|
166
|
+
export async function captureGameplay(input, overrides = {}) {
|
|
167
|
+
const deps = { ...defaultDependencies, ...overrides };
|
|
168
|
+
const project = resolveExportProject(input.project);
|
|
169
|
+
const resolution = parseResolution(input.resolution);
|
|
170
|
+
const frames = Math.round(boundedNumber(input.frames, 1, 1, MAX_FRAMES, "frames"));
|
|
171
|
+
const waitSeconds = boundedNumber(input.waitSeconds, 3, 0, 120, "waitSeconds");
|
|
172
|
+
const intervalSeconds = boundedNumber(input.intervalSeconds, 1, 0.1, 60, "intervalSeconds");
|
|
173
|
+
const scene = checkScene(project, input.scene);
|
|
174
|
+
const started = deps.now();
|
|
175
|
+
const binary = deps.findBinary();
|
|
176
|
+
if (!binary) {
|
|
177
|
+
throw new BuildToolError("engine_not_installed", "Summer Engine is not installed on this machine.", 'Recovery: run "summer install", or set SUMMER_BIN to the engine executable, then capture again.');
|
|
178
|
+
}
|
|
179
|
+
let out;
|
|
180
|
+
if (input.out?.trim()) {
|
|
181
|
+
out = isAbsolute(input.out) ? input.out : resolve(project, input.out);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
out = join(await capturesDir(project), stamp(started));
|
|
185
|
+
}
|
|
186
|
+
await mkdir(out, { recursive: true });
|
|
187
|
+
await rm(join(out, "results.json"), { force: true });
|
|
188
|
+
// A never-opened checkout has no .godot/ caches; the game would boot into
|
|
189
|
+
// missing-import errors. Build them once, headless (same as tests/autopilot).
|
|
190
|
+
let imported = false;
|
|
191
|
+
if (!existsSync(join(project, ".godot"))) {
|
|
192
|
+
const run = await deps.run(binary, ["--headless", "--import", "--disable-crash-handler", "--path", project], IMPORT_TIMEOUT_MS);
|
|
193
|
+
if (run.timedOut || !existsSync(join(project, ".godot"))) {
|
|
194
|
+
throw new BuildToolError("import_failed", "Summer Engine could not import the project's assets before the capture.", "Recovery: open the project once in Summer Engine (or run the engine with --headless --import), then capture again.", undefined, { output: outputTail(run.output) });
|
|
195
|
+
}
|
|
196
|
+
imported = true;
|
|
197
|
+
}
|
|
198
|
+
const probe = join(out, PROBE_FILE);
|
|
199
|
+
await writeFile(probe, captureProbeSource(frames, waitSeconds, intervalSeconds));
|
|
200
|
+
const maxSeconds = Math.min(ENGINE_MAX_SECONDS, Math.ceil(waitSeconds + (frames - 1) * intervalSeconds + 30));
|
|
201
|
+
const args = [
|
|
202
|
+
"--disable-crash-handler",
|
|
203
|
+
// No local HTTP API or ~/.summer discovery files for this throwaway run.
|
|
204
|
+
"--summer-no-api",
|
|
205
|
+
"--path",
|
|
206
|
+
project,
|
|
207
|
+
"--resolution",
|
|
208
|
+
resolution.text,
|
|
209
|
+
...(scene ? ["--scene", scene] : []),
|
|
210
|
+
"--summer-verify",
|
|
211
|
+
probe,
|
|
212
|
+
"--summer-verify-out",
|
|
213
|
+
out,
|
|
214
|
+
"--summer-verify-max",
|
|
215
|
+
String(maxSeconds),
|
|
216
|
+
];
|
|
217
|
+
let run;
|
|
218
|
+
try {
|
|
219
|
+
run = await deps.run(binary, args, (maxSeconds + 60) * 1000);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
throw new BuildToolError("engine_launch_failed", `Summer Engine could not start: ${error instanceof Error ? error.message : String(error)}.`, 'Recovery: run "summer doctor" and check the engine install.');
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
await rm(probe, { force: true });
|
|
226
|
+
}
|
|
227
|
+
const resultsPath = join(out, "results.json");
|
|
228
|
+
const results = existsSync(resultsPath)
|
|
229
|
+
? (() => {
|
|
230
|
+
try {
|
|
231
|
+
return JSON.parse(readFileSync(resultsPath, "utf8"));
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
})()
|
|
237
|
+
: null;
|
|
238
|
+
const engineErrors = (results?.errors_seen ?? []).filter((line) => !line.startsWith("WARNING|"));
|
|
239
|
+
if (!results || results.failure_reason || !results.shots?.length) {
|
|
240
|
+
const reason = results?.failure_reason
|
|
241
|
+
? `${results.failure_reason}: ${results.error ?? ""}`.trim()
|
|
242
|
+
: results
|
|
243
|
+
? "the game ran but no frame was saved"
|
|
244
|
+
: "the verify instance wrote no results";
|
|
245
|
+
throw new BuildToolError("capture_failed", `No gameplay frame was captured (${reason}).`, results?.failure_reason === "no_main_scene"
|
|
246
|
+
? "Recovery: set a main scene in Project Settings, or pass scene."
|
|
247
|
+
: "Recovery: read the engine errors below, fix what they name, and capture again. An engine older than the verify instance (before 0.5.55) cannot capture: update Summer Engine.", undefined, {
|
|
248
|
+
...(engineErrors.length ? { engineErrors: engineErrors.slice(0, 10) } : {}),
|
|
249
|
+
...(results?.frame_warnings?.length ? { frameWarnings: results.frame_warnings } : {}),
|
|
250
|
+
output: outputTail(run.output),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
const captured = [];
|
|
254
|
+
for (const shot of results.shots) {
|
|
255
|
+
const path = join(out, shot.file);
|
|
256
|
+
if (!existsSync(path))
|
|
257
|
+
continue;
|
|
258
|
+
const size = pngSize(await readFile(path)) ?? { width: shot.width, height: shot.height };
|
|
259
|
+
captured.push({ path, ...size });
|
|
260
|
+
}
|
|
261
|
+
const warnings = [...(results.frame_warnings ?? [])];
|
|
262
|
+
if (results.finished === false)
|
|
263
|
+
warnings.push("The game hit its time limit before every frame was saved.");
|
|
264
|
+
const off = captured.filter((frame) => frame.width !== resolution.width || frame.height !== resolution.height);
|
|
265
|
+
if (off.length) {
|
|
266
|
+
warnings.push(`Frames are ${off[0].width}x${off[0].height}, not ${resolution.text}: the project's stretch settings (Project Settings > Display > Window) set the render size. Store screenshots need 1920x1080 or 1080x1920; crop or change the stretch mode.`);
|
|
267
|
+
}
|
|
268
|
+
if (engineErrors.length)
|
|
269
|
+
warnings.push(`The engine logged ${engineErrors.length} error(s) during the run; check the frames show the game as expected: ${engineErrors.slice(0, 3).join(" / ")}`);
|
|
270
|
+
return {
|
|
271
|
+
ok: true,
|
|
272
|
+
project,
|
|
273
|
+
scene: scene ?? "main scene",
|
|
274
|
+
resolution: resolution.text,
|
|
275
|
+
frames: captured,
|
|
276
|
+
out,
|
|
277
|
+
engine: binary,
|
|
278
|
+
imported,
|
|
279
|
+
durationMs: deps.now().getTime() - started.getTime(),
|
|
280
|
+
...(warnings.length ? { warnings } : {}),
|
|
281
|
+
next: "Look at each frame before using it. For the store: upload with summer_upload_image_begin/complete and set them as desktop.screenshots or mobile.screenshots (summer_store_set_art). The game's own HUD may show; add no captions, logos or overlays.",
|
|
282
|
+
};
|
|
283
|
+
}
|
|
@@ -148,7 +148,7 @@ function assertUploadUrl(value) {
|
|
|
148
148
|
}
|
|
149
149
|
const local = url.hostname === "localhost" ||
|
|
150
150
|
url.hostname === "127.0.0.1" ||
|
|
151
|
-
url.hostname === "::1";
|
|
151
|
+
url.hostname === "[::1]";
|
|
152
152
|
if (url.username ||
|
|
153
153
|
url.password ||
|
|
154
154
|
(url.protocol !== "https:" && !(local && url.protocol === "http:"))) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type StoreAccessResult } from "../hosted-mcp.js";
|
|
1
2
|
import { type ProjectMemorySummary } from "../../project-memory/project-memory.js";
|
|
2
3
|
export type DoctorStatus = "ok" | "warning" | "fail";
|
|
3
4
|
export interface DoctorCheck {
|
|
@@ -22,7 +23,14 @@ interface DoctorOptions {
|
|
|
22
23
|
}
|
|
23
24
|
export declare function runDoctor(options?: DoctorOptions): Promise<DoctorResult>;
|
|
24
25
|
export declare function checkLogin(): Promise<DoctorCheck>;
|
|
25
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Store access: with a store sign-in ("summer login --store"), ask the store
|
|
28
|
+
* for the creator's games through the hosted Summer Engine MCP, so a refusal
|
|
29
|
+
* shows here with its reason instead of on the first publishing call.
|
|
30
|
+
* A warning at worst: only publishing needs the store.
|
|
31
|
+
*/
|
|
32
|
+
export declare function checkStoreSignIn(probe?: () => Promise<StoreAccessResult>, signedIn?: () => Promise<boolean>): Promise<DoctorCheck>;
|
|
33
|
+
export declare function checkEngineInstall(readVersion?: (binary: string) => string | null): DoctorCheck;
|
|
26
34
|
/** The one-line Project Memory verdict: Markdown counts plus the template pin
|
|
27
35
|
* (`.summer/project.json` is memory too — the first fact a fresh agent
|
|
28
36
|
* needs). Exported for tests. */
|
|
@@ -4,8 +4,11 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
4
4
|
import { getAuthToken, getUserInfo } from "../auth.js";
|
|
5
5
|
import { ENGINE_BIN_ENV, ENGINE_BINARY_ENV, findEngineBinary } from "../engine-install.js";
|
|
6
6
|
import { checkEngineHealth, getApiPort, getApiToken } from "../engine.js";
|
|
7
|
+
import { isVersionAtLeast } from "../launch-posture.js";
|
|
8
|
+
import { EXPORT_MIN_ENGINE_VERSION, installedEngineVersion } from "./export-engine-version.js";
|
|
7
9
|
import { c, pad, sym, tildeify } from "../format.js";
|
|
8
10
|
import { getMcpLogPath } from "../mcp-log.js";
|
|
11
|
+
import { checkStoreAccess, hasStoreSignIn } from "../hosted-mcp.js";
|
|
9
12
|
import { formatProjectPin, getProjectMemorySummary, } from "../../project-memory/project-memory.js";
|
|
10
13
|
import { buildCliVersionCheck, buildSkillsVersionCheck, defaultSkillMarkerCandidates, detectRecordedInstall, fetchLatestRegistryVersion, } from "../../installer/version-check.js";
|
|
11
14
|
import { TOOLKIT_VERSION as version } from "../version.js";
|
|
@@ -27,6 +30,7 @@ export async function runDoctor(options = {}) {
|
|
|
27
30
|
checks.push(await checkCliVersionCurrent());
|
|
28
31
|
checks.push(await checkSkillsVersion());
|
|
29
32
|
checks.push(await checkLogin());
|
|
33
|
+
checks.push(await checkStoreSignIn());
|
|
30
34
|
checks.push(checkEngineInstall());
|
|
31
35
|
checks.push(await checkLocalApi());
|
|
32
36
|
checks.push(await checkProjectMemory());
|
|
@@ -120,17 +124,56 @@ export async function checkLogin() {
|
|
|
120
124
|
message: user ? user.email : "signed in",
|
|
121
125
|
};
|
|
122
126
|
}
|
|
123
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Store access: with a store sign-in ("summer login --store"), ask the store
|
|
129
|
+
* for the creator's games through the hosted Summer Engine MCP, so a refusal
|
|
130
|
+
* shows here with its reason instead of on the first publishing call.
|
|
131
|
+
* A warning at worst: only publishing needs the store.
|
|
132
|
+
*/
|
|
133
|
+
export async function checkStoreSignIn(probe = () => checkStoreAccess(), signedIn = hasStoreSignIn) {
|
|
134
|
+
const base = { id: "store-access", label: "Store" };
|
|
135
|
+
if (!(await signedIn())) {
|
|
136
|
+
return { ...base, status: "ok", message: "no store sign-in (only publishing needs it: summer login --store)" };
|
|
137
|
+
}
|
|
138
|
+
const result = await probe();
|
|
139
|
+
if (result.status === "ok") {
|
|
140
|
+
return { ...base, status: "ok", message: `the store accepts this sign-in${result.games === null ? "" : ` (${result.games} ${result.games === 1 ? "game" : "games"})`}` };
|
|
141
|
+
}
|
|
142
|
+
if (result.status === "not_signed_in") {
|
|
143
|
+
return { ...base, status: "warning", message: `${result.message} (run: summer login --store)` };
|
|
144
|
+
}
|
|
145
|
+
if (result.status === "unreachable") {
|
|
146
|
+
return { ...base, status: "warning", message: `could not reach the store: ${result.message}`, details: { ...result } };
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
...base,
|
|
150
|
+
status: "warning",
|
|
151
|
+
message: `the store refused this sign-in${result.code ? ` (${result.code})` : ""}: ${result.message}${result.requestId ? ` [requestId ${result.requestId}]` : ""}`,
|
|
152
|
+
details: { ...result },
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export function checkEngineInstall(readVersion = installedEngineVersion) {
|
|
124
156
|
const binary = findEngineBinary();
|
|
125
157
|
if (binary) {
|
|
126
158
|
// Shorten /Applications/Summer.app/Contents/MacOS/Summer -> /Applications/Summer.app
|
|
127
|
-
const display = binary.replace(/\/Contents\/MacOS\/Summer$/, "");
|
|
159
|
+
const display = tildeify(binary.replace(/\/Contents\/MacOS\/Summer$/, ""));
|
|
160
|
+
const version = readVersion(binary);
|
|
161
|
+
const details = { path: binary, version, minimumExportVersion: EXPORT_MIN_ENGINE_VERSION };
|
|
162
|
+
if (isVersionAtLeast(version, EXPORT_MIN_ENGINE_VERSION) === false) {
|
|
163
|
+
return {
|
|
164
|
+
id: "engine-install",
|
|
165
|
+
label: "Engine",
|
|
166
|
+
status: "warning",
|
|
167
|
+
message: `${display} is ${version}; summer.games exports need ${EXPORT_MIN_ENGINE_VERSION}+ (update: summer install --yes)`,
|
|
168
|
+
details,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
128
171
|
return {
|
|
129
172
|
id: "engine-install",
|
|
130
173
|
label: "Engine",
|
|
131
174
|
status: "ok",
|
|
132
|
-
message:
|
|
133
|
-
details
|
|
175
|
+
message: version ? `${display} (${version})` : display,
|
|
176
|
+
details,
|
|
134
177
|
};
|
|
135
178
|
}
|
|
136
179
|
return {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface EngineRun {
|
|
2
|
+
code: number | null;
|
|
3
|
+
signal: NodeJS.Signals | null;
|
|
4
|
+
timedOut: boolean;
|
|
5
|
+
output: string;
|
|
6
|
+
/** The engine's "ERROR:" / "SCRIPT ERROR:" lines from the whole run (the first 20), even when the tail lost them. */
|
|
7
|
+
errors?: string[];
|
|
8
|
+
}
|
|
9
|
+
/** "ERROR: ..." and "SCRIPT ERROR: ..." lines of engine output, without colour codes or the "at:" lines. */
|
|
10
|
+
export declare function engineErrorLines(output: string, limit?: number): string[];
|
|
11
|
+
/** Spawn the engine (argv only, no shell), keep the last output, kill on timeout. */
|
|
12
|
+
export declare function runEngine(binary: string, args: string[], timeoutMs: number): Promise<EngineRun>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
/** One headless engine process: argv only (no shell), the last output kept, killed on timeout. */
|
|
3
|
+
const OUTPUT_TAIL_BYTES = 8 * 1024;
|
|
4
|
+
const KILL_GRACE_MS = 5_000;
|
|
5
|
+
const MAX_ERROR_LINES = 20;
|
|
6
|
+
const MAX_ERROR_LINE_CHARS = 600;
|
|
7
|
+
const ANSI = /\u001b\[[0-9;]*m/g;
|
|
8
|
+
/** "ERROR: ..." and "SCRIPT ERROR: ..." lines of engine output, without colour codes or the "at:" lines. */
|
|
9
|
+
export function engineErrorLines(output, limit = MAX_ERROR_LINES) {
|
|
10
|
+
const lines = [];
|
|
11
|
+
for (const raw of output.split(/\r?\n/)) {
|
|
12
|
+
const line = raw.replace(ANSI, "").trim();
|
|
13
|
+
if (!/^(SCRIPT )?ERROR:/.test(line))
|
|
14
|
+
continue;
|
|
15
|
+
if (lines.length >= limit)
|
|
16
|
+
break;
|
|
17
|
+
lines.push(line.slice(0, MAX_ERROR_LINE_CHARS));
|
|
18
|
+
}
|
|
19
|
+
return lines;
|
|
20
|
+
}
|
|
21
|
+
/** Spawn the engine (argv only, no shell), keep the last output, kill on timeout. */
|
|
22
|
+
export function runEngine(binary, args, timeoutMs) {
|
|
23
|
+
return new Promise((resolvePromise, reject) => {
|
|
24
|
+
const child = nodeSpawn(binary, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
25
|
+
let output = "";
|
|
26
|
+
let timedOut = false;
|
|
27
|
+
// Error lines are collected per stream as they arrive, so a long run cannot push them out of the tail.
|
|
28
|
+
const errors = [];
|
|
29
|
+
const partial = { stdout: "", stderr: "" };
|
|
30
|
+
const collect = (text) => {
|
|
31
|
+
if (errors.length < MAX_ERROR_LINES)
|
|
32
|
+
errors.push(...engineErrorLines(text, MAX_ERROR_LINES - errors.length));
|
|
33
|
+
};
|
|
34
|
+
const keep = (stream) => (chunk) => {
|
|
35
|
+
const text = chunk.toString("utf8");
|
|
36
|
+
output = (output + text).slice(-OUTPUT_TAIL_BYTES);
|
|
37
|
+
const lines = (partial[stream] + text).split("\n");
|
|
38
|
+
partial[stream] = (lines.pop() ?? "").slice(-OUTPUT_TAIL_BYTES);
|
|
39
|
+
collect(lines.join("\n"));
|
|
40
|
+
};
|
|
41
|
+
child.stdout?.on("data", keep("stdout"));
|
|
42
|
+
child.stderr?.on("data", keep("stderr"));
|
|
43
|
+
let killTimer;
|
|
44
|
+
const timer = setTimeout(() => {
|
|
45
|
+
timedOut = true;
|
|
46
|
+
child.kill("SIGTERM");
|
|
47
|
+
killTimer = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
|
|
48
|
+
}, timeoutMs);
|
|
49
|
+
child.on("error", (error) => {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
clearTimeout(killTimer);
|
|
52
|
+
reject(error);
|
|
53
|
+
});
|
|
54
|
+
child.on("close", (code, signal) => {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
clearTimeout(killTimer);
|
|
57
|
+
collect(`${partial.stdout}\n${partial.stderr}`);
|
|
58
|
+
resolvePromise({ code, signal, timedOut, output, errors });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The oldest Summer Engine that exports what the Summer Games store takes.
|
|
3
|
+
* 0.6.0 has no "summer.games" export platform: it cannot read the presets
|
|
4
|
+
* summer_export_game writes and fails after a full engine start.
|
|
5
|
+
*/
|
|
6
|
+
export declare const EXPORT_MIN_ENGINE_VERSION = "0.7.0";
|
|
7
|
+
export declare const ENGINE_UPDATE_HOW = "run \"summer install --yes\" (or download it from summerengine.com/download)";
|
|
8
|
+
/** Installed Summer version of an engine binary, or null when this system cannot read it. */
|
|
9
|
+
export declare function installedEngineVersion(binary: string): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Refuse an engine older than EXPORT_MIN_ENGINE_VERSION, before anything is
|
|
12
|
+
* written to the project. An unreadable version passes: the engine's own
|
|
13
|
+
* export output then names what is wrong.
|
|
14
|
+
*/
|
|
15
|
+
export declare function assertExportEngine(binary: string, version: string | null): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { platform } from "node:os";
|
|
2
|
+
import { isVersionAtLeast, readInstalledEngineVersion } from "../launch-posture.js";
|
|
3
|
+
import { BuildToolError } from "./summer-bundle.js";
|
|
4
|
+
/**
|
|
5
|
+
* The oldest Summer Engine that exports what the Summer Games store takes.
|
|
6
|
+
* 0.6.0 has no "summer.games" export platform: it cannot read the presets
|
|
7
|
+
* summer_export_game writes and fails after a full engine start.
|
|
8
|
+
*/
|
|
9
|
+
export const EXPORT_MIN_ENGINE_VERSION = "0.7.0";
|
|
10
|
+
export const ENGINE_UPDATE_HOW = 'run "summer install --yes" (or download it from summerengine.com/download)';
|
|
11
|
+
/** Installed Summer version of an engine binary, or null when this system cannot read it. */
|
|
12
|
+
export function installedEngineVersion(binary) {
|
|
13
|
+
return readInstalledEngineVersion(binary, platform());
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Refuse an engine older than EXPORT_MIN_ENGINE_VERSION, before anything is
|
|
17
|
+
* written to the project. An unreadable version passes: the engine's own
|
|
18
|
+
* export output then names what is wrong.
|
|
19
|
+
*/
|
|
20
|
+
export function assertExportEngine(binary, version) {
|
|
21
|
+
if (isVersionAtLeast(version, EXPORT_MIN_ENGINE_VERSION) !== false)
|
|
22
|
+
return;
|
|
23
|
+
throw new BuildToolError("engine_too_old", `Summer Engine ${version} is installed; summer.games exports need Summer Engine ${EXPORT_MIN_ENGINE_VERSION} or newer.`, `Recovery: update Summer Engine to ${EXPORT_MIN_ENGINE_VERSION}+: ${ENGINE_UPDATE_HOW}, then export again. Nothing was written to the project.`, undefined, { engine: binary, engineVersion: version, minimumEngineVersion: EXPORT_MIN_ENGINE_VERSION });
|
|
24
|
+
}
|