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.
Files changed (97) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +4 -2
  3. package/.codex-plugin/plugin.json +5 -3
  4. package/.cursor-plugin/plugin.json +4 -2
  5. package/.factory-plugin/plugin.json +4 -2
  6. package/CHANGELOG.md +61 -0
  7. package/README.md +7 -4
  8. package/assets/api-docs.json.gz +0 -0
  9. package/dist/cli/commands/install.d.ts +10 -0
  10. package/dist/cli/commands/install.js +55 -10
  11. package/dist/cli/commands/login.js +12 -0
  12. package/dist/cli/commands/publish.js +2 -1
  13. package/dist/cli/commands/tool.d.ts +7 -0
  14. package/dist/cli/commands/tool.js +36 -1
  15. package/dist/core/auth.js +3 -0
  16. package/dist/core/capabilities/audit/args.d.ts +14 -14
  17. package/dist/core/capabilities/capture-gameplay.d.ts +69 -0
  18. package/dist/core/capabilities/capture-gameplay.js +283 -0
  19. package/dist/core/capabilities/creator.js +1 -1
  20. package/dist/core/capabilities/doctor.d.ts +9 -1
  21. package/dist/core/capabilities/doctor.js +47 -4
  22. package/dist/core/capabilities/engine-run.d.ts +12 -0
  23. package/dist/core/capabilities/engine-run.js +61 -0
  24. package/dist/core/capabilities/export-engine-version.d.ts +15 -0
  25. package/dist/core/capabilities/export-engine-version.js +24 -0
  26. package/dist/core/capabilities/export-game.d.ts +107 -0
  27. package/dist/core/capabilities/export-game.js +445 -0
  28. package/dist/core/capabilities/export-presets.d.ts +50 -0
  29. package/dist/core/capabilities/export-presets.js +177 -0
  30. package/dist/core/capabilities/export-templates.d.ts +118 -0
  31. package/dist/core/capabilities/export-templates.js +291 -0
  32. package/dist/core/capabilities/image-generation.d.ts +9 -0
  33. package/dist/core/capabilities/image-generation.js +19 -1
  34. package/dist/core/capabilities/publish-build.d.ts +77 -0
  35. package/dist/core/capabilities/publish-build.js +500 -0
  36. package/dist/core/capabilities/publish-download.d.ts +37 -0
  37. package/dist/core/capabilities/publish-download.js +168 -0
  38. package/dist/core/capabilities/runtime-control.d.ts +10 -10
  39. package/dist/core/capabilities/summer-bundle.d.ts +49 -0
  40. package/dist/core/capabilities/summer-bundle.js +156 -0
  41. package/dist/core/capabilities/tool-dispatch.js +43 -1
  42. package/dist/core/capabilities/ui-control.d.ts +2 -2
  43. package/dist/core/config.js +2 -2
  44. package/dist/core/engine-install.d.ts +16 -0
  45. package/dist/core/engine-install.js +32 -2
  46. package/dist/core/hosted-mcp.d.ts +115 -0
  47. package/dist/core/hosted-mcp.js +164 -0
  48. package/dist/core/launch-posture.d.ts +3 -0
  49. package/dist/core/launch-posture.js +15 -2
  50. package/dist/core/library-read.d.ts +2 -2
  51. package/dist/core/oauth.d.ts +78 -0
  52. package/dist/core/oauth.js +309 -0
  53. package/dist/core/util/zip-write.d.ts +18 -0
  54. package/dist/core/util/zip-write.js +112 -0
  55. package/dist/core/util/zip.d.ts +22 -0
  56. package/dist/core/util/zip.js +140 -0
  57. package/dist/mcp/hosted-mount.d.ts +50 -0
  58. package/dist/mcp/hosted-mount.js +200 -0
  59. package/dist/mcp/server.d.ts +2 -1
  60. package/dist/mcp/server.js +14 -6
  61. package/dist/mcp/tools/creator-tools.js +64 -1
  62. package/dist/mcp/tools/generate-tools.js +12 -2
  63. package/gemini-extension.json +3 -1
  64. package/library/references/mcp-tools-reference/mcp-tools-reference.md +7 -3
  65. package/library/skills/README.md +6 -4
  66. package/library/skills/bounded-engine-runs/SKILL.md +96 -0
  67. package/library/skills/bounded-engine-runs/resource.yaml +29 -0
  68. package/library/skills/compatibility-renderer-traps/SKILL.md +78 -0
  69. package/library/skills/compatibility-renderer-traps/resource.yaml +28 -0
  70. package/library/skills/concept-art/SKILL.md +2 -2
  71. package/library/skills/export-and-ship/SKILL.md +4 -0
  72. package/library/skills/headless-scripting/resource.yaml +1 -0
  73. package/library/skills/in-game-purchases/SKILL.md +123 -118
  74. package/library/skills/in-game-purchases/resource.yaml +2 -2
  75. package/library/skills/multiplayer-publish/SKILL.md +5 -1
  76. package/library/skills/navigate-summer/SKILL.md +1 -1
  77. package/library/skills/web-compatibility-look/resource.yaml +1 -0
  78. package/library/tools/capture-gameplay/resource.yaml +73 -0
  79. package/library/tools/creator-publish/resource.yaml +4 -3
  80. package/library/tools/creator-releases/resource.yaml +1 -1
  81. package/library/tools/export-game/resource.yaml +72 -0
  82. package/library/tools/export-templates/resource.yaml +63 -0
  83. package/library/tools/generate-image/resource.yaml +15 -1
  84. package/library/tools/open/resource.yaml +1 -1
  85. package/library/tools/publish-build/resource.yaml +78 -0
  86. package/package.json +1 -1
  87. package/registry/generated/aliases.json +4 -0
  88. package/registry/generated/counts.json +3 -3
  89. package/registry/generated/gemini-extension.json +3 -1
  90. package/registry/generated/index.json +241 -19
  91. package/registry/generated/marketplace.claude.json +2 -2
  92. package/registry/generated/plugin.claude.json +4 -2
  93. package/registry/generated/plugin.codex.json +5 -3
  94. package/registry/generated/plugin.cursor.json +4 -2
  95. package/registry/generated/plugin.factory.json +4 -2
  96. package/registry/generated/skills-index.md +6 -4
  97. package/registry/generated/skills-registry.json +28 -1
@@ -0,0 +1,168 @@
1
+ import { basename, extname } from "node:path";
2
+ import { stat } from "node:fs/promises";
3
+ import { resolveGatewayUrl } from "../config.js";
4
+ import { OAuthError } from "../oauth.js";
5
+ import { appendStoreJsonLine } from "../store.js";
6
+ import { readZipEntries } from "../util/zip.js";
7
+ import { BuildToolError } from "./summer-bundle.js";
8
+ import { createStore, readGrants, uploadParts } from "./publish-build.js";
9
+ /**
10
+ * The store's second build path (summer-platform store versions,
11
+ * internal/creatorstore): a web build (an HTML5 .zip played on summer.games)
12
+ * or a native download (macOS universal, Windows x64, Linux x64) that players
13
+ * download. summer_export_game format "download" makes these files;
14
+ * summer_publish_build uploads them here, through the same creator store
15
+ * routes as Studio (PSE /api/creator-store/* -> /v1/management/*):
16
+ *
17
+ * GET games/{g}/store/versions?platform= resume an unfinished upload of the same file
18
+ * POST games/{g}/store/versions {platform, fileName, sizeBytes, label} -> part plan
19
+ * GET .../{v}/parts parts storage already has
20
+ * POST .../{v}:signParts {partNumbers} -> presigned PUT per part
21
+ * POST .../{v}:complete seal; Summer checks the file
22
+ * GET .../{v} until ready or rejected
23
+ *
24
+ * Players get the newest ready version of a platform, so on a game that is
25
+ * already live the store refuses an agent's :complete (the owner finishes it).
26
+ */
27
+ export const DOWNLOAD_PLATFORMS = ["web", "macos-universal", "windows-x64", "linux-x64"];
28
+ /** summer-platform creatorstore limits (versions.go, webzip.go). */
29
+ const WEB_MAX_BYTES = 500 * 1024 * 1024;
30
+ const WEB_MAX_FILES = 2000;
31
+ const WEB_MAX_FILE_BYTES = 200 * 1024 * 1024;
32
+ const NATIVE_MAX_BYTES = 20 * 1024 ** 3;
33
+ const WEB_PART_BYTES = 16 * 1024 * 1024;
34
+ const NATIVE_PART_BYTES = 64 * 1024 * 1024;
35
+ const DEFAULT_WAIT_SECONDS = 300;
36
+ const POLL_MS = 3_000;
37
+ const seg = encodeURIComponent;
38
+ export function isDownloadPlatform(value) {
39
+ return DOWNLOAD_PLATFORMS.includes(value);
40
+ }
41
+ /** The store's file name rule: a letter or digit first, then letters, digits, ". _ -" and spaces, at most 128. */
42
+ export function storeFileName(file) {
43
+ const cleaned = basename(file).replace(/[^A-Za-z0-9._ -]/g, "-").replace(/^[^A-Za-z0-9]+/, "");
44
+ return (cleaned || "game.zip").slice(-128);
45
+ }
46
+ /** What the store will refuse, checked before anything is sent. */
47
+ async function checkFile(file, platform, sizeBytes) {
48
+ const lower = file.toLowerCase();
49
+ if (platform === "web") {
50
+ if (extname(lower) !== ".zip")
51
+ throw new BuildToolError("download_format_unsupported", "A web build is a .zip.", 'Recovery: export with summer_export_game targets ["web"].');
52
+ if (sizeBytes > WEB_MAX_BYTES)
53
+ throw new BuildToolError("download_too_large", `The web build is ${Math.ceil(sizeBytes / 1024 ** 2)} MiB; the store takes at most 500 MiB.`, "Recovery: make the build smaller (compress textures, remove unused assets) and export again.");
54
+ let entries;
55
+ try {
56
+ entries = await readZipEntries(file);
57
+ }
58
+ catch (error) {
59
+ throw new BuildToolError("download_unreadable", `${file} is not a readable .zip (${error instanceof Error ? error.message : String(error)}).`, "Recovery: export again.");
60
+ }
61
+ const files = entries.filter((entry) => !entry.name.endsWith("/"));
62
+ if (!files.some((entry) => entry.name === "index.html"))
63
+ throw new BuildToolError("web_entry_missing", "The web build has no index.html at the top of the .zip.", 'Recovery: export with summer_export_game targets ["web"]; do not wrap the files in a folder.');
64
+ if (files.length > WEB_MAX_FILES)
65
+ throw new BuildToolError("web_too_many_files", `The web build has ${files.length} files; the store takes at most ${WEB_MAX_FILES}.`, "Recovery: export again; a Summer web export has a handful of files.");
66
+ const big = files.find((entry) => entry.uncompressedSize > WEB_MAX_FILE_BYTES);
67
+ if (big)
68
+ throw new BuildToolError("web_file_too_large", `${big.name} is larger than 200 MiB.`, "Recovery: make the pack smaller and export again.");
69
+ return { fileCount: files.length };
70
+ }
71
+ if (!(lower.endsWith(".zip") || lower.endsWith(".tar.gz") || lower.endsWith(".tgz"))) {
72
+ throw new BuildToolError("download_format_unsupported", "A native download is a .zip or .tar.gz.", 'Recovery: export with summer_export_game format "download".');
73
+ }
74
+ if (sizeBytes > NATIVE_MAX_BYTES)
75
+ throw new BuildToolError("download_too_large", "The download is larger than 20 GiB.", "Recovery: make the build smaller and export again.");
76
+ return {};
77
+ }
78
+ export async function publishDownload(input, deps) {
79
+ const { gameId, file, clientVersion } = input;
80
+ if (!isDownloadPlatform(input.platform)) {
81
+ throw new BuildToolError("platform_invalid", `"${input.platform}" is not a store download platform.`, `Recovery: pass platform as one of ${DOWNLOAD_PLATFORMS.join(", ")}.`);
82
+ }
83
+ const platform = input.platform;
84
+ const sizeBytes = (await stat(file)).size;
85
+ const { fileCount } = await checkFile(file, platform, sizeBytes);
86
+ const fileName = storeFileName(file);
87
+ const target = { gameId, file, platform, fileName, sizeBytes, clientVersion, ...(fileCount ? { fileCount } : {}) };
88
+ if (input.confirm !== true) {
89
+ return {
90
+ ok: true,
91
+ status: "confirmation_required",
92
+ target,
93
+ next: "Show the user this exact game, file, platform, size and version. Only after they approve, call again with confirm=true. Nothing was uploaded.",
94
+ };
95
+ }
96
+ let token;
97
+ try {
98
+ token = await deps.token();
99
+ }
100
+ catch (error) {
101
+ if (error instanceof OAuthError)
102
+ throw new BuildToolError(error.code, error.message.replace(` ${error.recovery}`, ""), error.recovery);
103
+ throw error;
104
+ }
105
+ const store = createStore(await resolveGatewayUrl(), token, deps);
106
+ const audit = { at: new Date(deps.now()).toISOString(), operation: "publish_download", face: input.face, ...target };
107
+ await appendStoreJsonLine("creator-audit.jsonl", { ...audit, outcome: "started" });
108
+ const versions = `games/${seg(gameId)}/store/versions`;
109
+ try {
110
+ // The same file and version still waiting for its bytes: continue it.
111
+ const listed = await store.call("GET", `${versions}?platform=${seg(platform)}`);
112
+ let version = (listed.items ?? []).find((item) => item.status === "awaiting_upload" && item.platform === platform && item.fileName === fileName && item.sizeBytes === sizeBytes && item.label === clientVersion);
113
+ let plan = null;
114
+ const resumed = Boolean(version);
115
+ if (!version) {
116
+ const created = await store.call("POST", versions, {
117
+ body: { platform, fileName, sizeBytes, label: clientVersion },
118
+ });
119
+ version = created.version;
120
+ plan = { partSizeBytes: Number(created.upload?.partSizeBytes), partCount: Number(created.upload?.partCount) };
121
+ }
122
+ if (!version?.id)
123
+ throw new BuildToolError("store_invalid_response", "The store did not return a version id.", "Recovery: retry.");
124
+ const versionPath = `${versions}/${seg(version.id)}`;
125
+ if (version.status === "awaiting_upload") {
126
+ const partSizeBytes = plan?.partSizeBytes || (platform === "web" ? WEB_PART_BYTES : NATIVE_PART_BYTES);
127
+ const partCount = plan?.partCount || Math.max(1, Math.ceil(sizeBytes / partSizeBytes));
128
+ if (!(partSizeBytes > 0) || partCount !== Math.max(1, Math.ceil(sizeBytes / partSizeBytes))) {
129
+ throw new BuildToolError("store_invalid_response", "The store's part plan does not fit this file.", "Recovery: retry; if it repeats, report the store as unhealthy.");
130
+ }
131
+ const expected = (n) => (n < partCount ? partSizeBytes : sizeBytes - (partCount - 1) * partSizeBytes);
132
+ const stored = resumed ? (await store.call("GET", `${versionPath}/parts`)).parts ?? [] : [];
133
+ const done = new Set(stored.filter((part) => part.sizeBytes === expected(part.partNumber)).map((part) => part.partNumber));
134
+ const missing = Array.from({ length: partCount }, (_, index) => index + 1).filter((n) => !done.has(n));
135
+ if (missing.length) {
136
+ await uploadParts(async (numbers) => readGrants(await store.call("POST", `${versionPath}:signParts`, { body: { partNumbers: numbers } })), file, sizeBytes, { partSizeBytes, partCount }, deps, missing);
137
+ }
138
+ version = (await store.call("POST", `${versionPath}:complete`, { body: {} })).version ?? version;
139
+ }
140
+ const deadline = deps.now() + (input.waitSeconds ?? DEFAULT_WAIT_SECONDS) * 1000;
141
+ while (version.status !== "ready" && version.status !== "rejected" && deps.now() < deadline) {
142
+ await deps.sleep(POLL_MS);
143
+ version = await store.call("GET", versionPath);
144
+ }
145
+ const outcome = version.status === "ready" ? "ready" : version.status === "rejected" ? "rejected" : "processing";
146
+ await appendStoreJsonLine("creator-audit.jsonl", { ...audit, outcome, versionId: version.id });
147
+ if (version.status === "rejected") {
148
+ throw new BuildToolError("download_rejected", version.rejectionReason || "Summer Games did not accept this file.", "Recovery: fix what the reason says, export again and upload with a new clientVersion.", undefined, { versionId: version.id });
149
+ }
150
+ return {
151
+ ok: true,
152
+ status: outcome,
153
+ gameId,
154
+ versionId: version.id,
155
+ platform,
156
+ clientVersion,
157
+ resumed,
158
+ ...(version.playUrl ? { playUrl: version.playUrl } : {}),
159
+ next: outcome === "ready"
160
+ ? "The file is checked. Players get it once the store page is live; submit the page with summer_store_submit (the owner approves)."
161
+ : "Summer is still checking the file. Call summer_publish_build again with the same file and clientVersion to keep waiting; nothing is uploaded twice.",
162
+ };
163
+ }
164
+ catch (error) {
165
+ await appendStoreJsonLine("creator-audit.jsonl", { ...audit, outcome: "failed", code: error instanceof BuildToolError ? error.code : "publish_download_failed" });
166
+ throw error;
167
+ }
168
+ }
@@ -97,14 +97,14 @@ export declare const runtimeSetArgsSchema: z.ZodObject<{
97
97
  field: z.ZodOptional<z.ZodString>;
98
98
  instance: z.ZodOptional<z.ZodString>;
99
99
  }, "strip", z.ZodTypeAny, {
100
- path: string;
101
100
  value: string | number | boolean;
101
+ path: string;
102
102
  property: string;
103
103
  instance?: string | undefined;
104
104
  field?: string | undefined;
105
105
  }, {
106
- path: string;
107
106
  value: string | number | boolean;
107
+ path: string;
108
108
  property: string;
109
109
  instance?: string | undefined;
110
110
  field?: string | undefined;
@@ -117,13 +117,13 @@ export declare const runtimeCallArgsSchema: z.ZodObject<{
117
117
  args: z.ZodOptional<z.ZodArray<z.ZodUnknown, "many">>;
118
118
  instance: z.ZodOptional<z.ZodString>;
119
119
  }, "strip", z.ZodTypeAny, {
120
- path: string;
121
120
  method: string;
121
+ path: string;
122
122
  args?: unknown[] | undefined;
123
123
  instance?: string | undefined;
124
124
  }, {
125
- path: string;
126
125
  method: string;
126
+ path: string;
127
127
  args?: unknown[] | undefined;
128
128
  instance?: string | undefined;
129
129
  }>;
@@ -183,13 +183,13 @@ export declare const runtimeAnimateArgsSchema: z.ZodObject<{
183
183
  }, "strip", z.ZodTypeAny, {
184
184
  path: string;
185
185
  target: "tree" | "player" | "bones";
186
+ state?: string | undefined;
187
+ value?: string | number | boolean | undefined;
186
188
  instance?: string | undefined;
187
189
  name?: string | undefined;
188
- value?: string | number | boolean | undefined;
189
- cmd?: "pause" | "play" | "stop" | "state" | "seek" | "speed" | "travel" | "start" | "set_param" | "get_param" | undefined;
190
+ cmd?: "state" | "pause" | "play" | "stop" | "seek" | "speed" | "travel" | "start" | "set_param" | "get_param" | undefined;
190
191
  space?: "local" | "global" | "both" | undefined;
191
192
  blend?: number | undefined;
192
- state?: string | undefined;
193
193
  position?: number | undefined;
194
194
  speed?: number | undefined;
195
195
  bones?: string[] | undefined;
@@ -202,13 +202,13 @@ export declare const runtimeAnimateArgsSchema: z.ZodObject<{
202
202
  }, {
203
203
  path: string;
204
204
  target: "tree" | "player" | "bones";
205
+ state?: string | undefined;
206
+ value?: string | number | boolean | undefined;
205
207
  instance?: string | undefined;
206
208
  name?: string | undefined;
207
- value?: string | number | boolean | undefined;
208
- cmd?: "pause" | "play" | "stop" | "state" | "seek" | "speed" | "travel" | "start" | "set_param" | "get_param" | undefined;
209
+ cmd?: "state" | "pause" | "play" | "stop" | "seek" | "speed" | "travel" | "start" | "set_param" | "get_param" | undefined;
209
210
  space?: "local" | "global" | "both" | undefined;
210
211
  blend?: number | undefined;
211
- state?: string | undefined;
212
212
  position?: number | undefined;
213
213
  speed?: number | undefined;
214
214
  bones?: string[] | undefined;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The summer.games export (summer-platform ADR 0033): one .zip with
3
+ * summer-bundle.json, client.pck, and for a game with a host server.pck plus
4
+ * config/. This reads what the build upload declares; the Platform verifies
5
+ * the uploaded bytes again. Same rules as Studio's browser check
6
+ * (publicsummerengine src/lib/creator-store/pck-check.ts, checkBundle).
7
+ */
8
+ export declare const BUNDLE_SCHEMA = "summer.bundle.v1";
9
+ export declare const BUNDLE_MANIFEST = "summer-bundle.json";
10
+ /** A failure with a stable code, a plain message and the next step. */
11
+ export declare class BuildToolError extends Error {
12
+ readonly code: string;
13
+ readonly recovery: string;
14
+ readonly status?: number | undefined;
15
+ readonly detail?: Record<string, unknown> | undefined;
16
+ constructor(code: string, message: string, recovery: string, status?: number | undefined, detail?: Record<string, unknown> | undefined);
17
+ }
18
+ export interface BundleFile {
19
+ path: string;
20
+ sha256: string;
21
+ size: number;
22
+ }
23
+ export interface SummerBundle {
24
+ schema: typeof BUNDLE_SCHEMA;
25
+ summerVersion: string | null;
26
+ engineSha: string | null;
27
+ /** res:// scene the client pack opens. */
28
+ mainScene: string;
29
+ /** Client platforms the export was made for (ios, macos, windows, ...). */
30
+ targetPlatforms: string[];
31
+ /** True when the game has a host (server.pck + config/summer.build.json). */
32
+ hosted: boolean;
33
+ compositionPath: string | null;
34
+ /** client.pck as summer-bundle.json lists it ("sha256:<hex>"). */
35
+ clientPack: {
36
+ sha256: string;
37
+ size: number;
38
+ };
39
+ /** Hosted only: config/summer.build.json, sent as the Build declaration. */
40
+ hostedBuild?: Record<string, unknown>;
41
+ files: BundleFile[];
42
+ }
43
+ /** Read and check summer-bundle.json in an exported .zip. */
44
+ export declare function readSummerBundle(path: string): Promise<SummerBundle>;
45
+ /** "sha256:<hex>" and size of a whole file, streamed. */
46
+ export declare function hashFile(path: string): Promise<{
47
+ sha256: string;
48
+ sizeBytes: number;
49
+ }>;
@@ -0,0 +1,156 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { readZipEntries, readZipEntry, ZipReadError } from "../util/zip.js";
4
+ /**
5
+ * The summer.games export (summer-platform ADR 0033): one .zip with
6
+ * summer-bundle.json, client.pck, and for a game with a host server.pck plus
7
+ * config/. This reads what the build upload declares; the Platform verifies
8
+ * the uploaded bytes again. Same rules as Studio's browser check
9
+ * (publicsummerengine src/lib/creator-store/pck-check.ts, checkBundle).
10
+ */
11
+ export const BUNDLE_SCHEMA = "summer.bundle.v1";
12
+ export const BUNDLE_MANIFEST = "summer-bundle.json";
13
+ const BUNDLE_CLIENT = "client.pck";
14
+ const BUNDLE_SERVER = "server.pck";
15
+ const BUNDLE_BUILD = "config/summer.build.json";
16
+ const SHA256 = /^sha256:[0-9a-f]{64}$/;
17
+ const AS_EXPORTED = "Recovery: export the game again with the summer.games preset and use the .zip exactly as Summer Engine wrote it.";
18
+ /** A failure with a stable code, a plain message and the next step. */
19
+ export class BuildToolError extends Error {
20
+ code;
21
+ recovery;
22
+ status;
23
+ detail;
24
+ constructor(code, message, recovery, status, detail) {
25
+ super(`${message} ${recovery}`);
26
+ this.code = code;
27
+ this.recovery = recovery;
28
+ this.status = status;
29
+ this.detail = detail;
30
+ this.name = "BuildToolError";
31
+ }
32
+ }
33
+ function bundleError(message) {
34
+ return new BuildToolError("bundle_invalid", message, AS_EXPORTED);
35
+ }
36
+ /** Read and check summer-bundle.json in an exported .zip. */
37
+ export async function readSummerBundle(path) {
38
+ let entries;
39
+ try {
40
+ entries = await readZipEntries(path);
41
+ }
42
+ catch (error) {
43
+ throw bundleError(`${path} is not a readable summer.games export (${error instanceof Error ? error.message : String(error)}).`);
44
+ }
45
+ const byName = new Map(entries.map((entry) => [entry.name, entry]));
46
+ const manifestEntry = byName.get(BUNDLE_MANIFEST);
47
+ const clientEntry = byName.get(BUNDLE_CLIENT);
48
+ if (!manifestEntry || !clientEntry) {
49
+ throw bundleError(`${path} is not a summer.games export: it has no ${BUNDLE_MANIFEST} and ${BUNDLE_CLIENT}.`);
50
+ }
51
+ if (byName.size !== entries.length ||
52
+ entries.some((entry) => entry.encrypted ||
53
+ entry.name.endsWith("/") ||
54
+ entry.name.split("/").some((part) => !part || part === "." || part === "..") ||
55
+ /[\\\0\r\n]/.test(entry.name))) {
56
+ throw bundleError("The export contains a repeated or unsafe file path.");
57
+ }
58
+ let manifest;
59
+ try {
60
+ manifest = JSON.parse((await readZipEntry(path, manifestEntry)).toString("utf8"));
61
+ }
62
+ catch (error) {
63
+ if (error instanceof ZipReadError)
64
+ throw bundleError(`${BUNDLE_MANIFEST} could not be read: ${error.message}`);
65
+ throw bundleError(`${BUNDLE_MANIFEST} is not valid JSON.`);
66
+ }
67
+ const client = manifest?.client;
68
+ const mainScene = typeof client?.mainScene === "string" ? client.mainScene : null;
69
+ if (manifest?.schema !== BUNDLE_SCHEMA ||
70
+ client?.path !== BUNDLE_CLIENT ||
71
+ !mainScene?.startsWith("res://") ||
72
+ !Array.isArray(client?.targetPlatforms) ||
73
+ !client.targetPlatforms.every((value) => typeof value === "string") ||
74
+ !Array.isArray(manifest.files)) {
75
+ throw new BuildToolError("bundle_invalid", `${BUNDLE_MANIFEST} is not a ${BUNDLE_SCHEMA} manifest.`, "Recovery: update Summer Engine and export again.");
76
+ }
77
+ const hosted = Boolean(manifest.server);
78
+ const compositionPath = typeof manifest.compositionPath === "string" ? manifest.compositionPath : null;
79
+ if ((!hosted && manifest.compositionPath != null) ||
80
+ hosted !== byName.has(BUNDLE_SERVER) ||
81
+ (hosted &&
82
+ (manifest.server?.path !== BUNDLE_SERVER ||
83
+ typeof manifest.server.mainScene !== "string" ||
84
+ !/^res:\/\/.+\.tscn$/.test(manifest.server.mainScene) ||
85
+ !compositionPath ||
86
+ !/^res:\/\/.+\.tres$/.test(compositionPath)))) {
87
+ throw bundleError("The export must name its server pack, main scene and network composition together.");
88
+ }
89
+ const allowed = (name) => name === BUNDLE_MANIFEST ||
90
+ name === BUNDLE_CLIENT ||
91
+ name === BUNDLE_BUILD ||
92
+ (hosted && (name === BUNDLE_SERVER || name.startsWith("config/")));
93
+ const extra = entries.find((entry) => !allowed(entry.name));
94
+ if (extra)
95
+ throw bundleError(`The export contains ${extra.name}, which a summer.games export never has.`);
96
+ const files = [];
97
+ for (const item of manifest.files) {
98
+ const entry = typeof item?.path === "string" ? byName.get(item.path) : undefined;
99
+ if (!entry ||
100
+ item.path === BUNDLE_MANIFEST ||
101
+ typeof item.sha256 !== "string" ||
102
+ !SHA256.test(item.sha256) ||
103
+ item.size !== entry.uncompressedSize) {
104
+ throw bundleError(`The files do not match ${BUNDLE_MANIFEST}.`);
105
+ }
106
+ files.push({ path: entry.name, sha256: item.sha256, size: entry.uncompressedSize });
107
+ }
108
+ if (new Set(files.map((file) => file.path)).size !== files.length || files.length !== entries.length - 1) {
109
+ throw bundleError(`The files do not match ${BUNDLE_MANIFEST}.`);
110
+ }
111
+ const listedClient = files.find((file) => file.path === BUNDLE_CLIENT);
112
+ if (clientEntry.method !== 0) {
113
+ throw bundleError(`${BUNDLE_CLIENT} is compressed in this zip; Summer reads it in place, so it must be stored.`);
114
+ }
115
+ let hostedBuild;
116
+ if (hosted) {
117
+ const config = byName.get(BUNDLE_BUILD);
118
+ let build;
119
+ try {
120
+ build = config ? JSON.parse((await readZipEntry(path, config, 64 * 1024)).toString("utf8")) : undefined;
121
+ }
122
+ catch {
123
+ build = undefined;
124
+ }
125
+ if (!build || typeof build !== "object" || Array.isArray(build) || build.executionMode !== "hosted") {
126
+ throw bundleError(`${BUNDLE_BUILD} is missing or does not declare a hosted game.`);
127
+ }
128
+ hostedBuild = build;
129
+ }
130
+ const targetPlatforms = client.targetPlatforms;
131
+ if (new Set(targetPlatforms).size !== targetPlatforms.length || targetPlatforms.length === 0) {
132
+ throw bundleError("The export must name at least one platform, each once.");
133
+ }
134
+ return {
135
+ schema: BUNDLE_SCHEMA,
136
+ summerVersion: typeof manifest.export?.summerVersion === "string" ? manifest.export.summerVersion : null,
137
+ engineSha: typeof manifest.export?.engineSha === "string" ? manifest.export.engineSha : null,
138
+ mainScene: mainScene,
139
+ targetPlatforms,
140
+ hosted,
141
+ compositionPath,
142
+ clientPack: { sha256: listedClient.sha256, size: listedClient.size },
143
+ ...(hostedBuild ? { hostedBuild } : {}),
144
+ files,
145
+ };
146
+ }
147
+ /** "sha256:<hex>" and size of a whole file, streamed. */
148
+ export async function hashFile(path) {
149
+ const hash = createHash("sha256");
150
+ let sizeBytes = 0;
151
+ for await (const chunk of createReadStream(path)) {
152
+ hash.update(chunk);
153
+ sizeBytes += chunk.length;
154
+ }
155
+ return { sha256: `sha256:${hash.digest("hex")}`, sizeBytes };
156
+ }
@@ -47,6 +47,10 @@ import open from "open";
47
47
  import { openArgsSchema, runOpen } from "./navigation/open.js";
48
48
  import { shapeEngineLogResponse } from "../log-filters.js";
49
49
  import { listCreatorReleases, publishCreator, } from "./creator.js";
50
+ import { exportGame } from "./export-game.js";
51
+ import { exportTemplates } from "./export-templates.js";
52
+ import { captureGameplay } from "./capture-gameplay.js";
53
+ import { publishBuild } from "./publish-build.js";
50
54
  import { CONFIG_KEYS, getConfigValue, isConfigKey, readSummerConfig, resolveGatewayUrl, setConfigValue, unsetConfigValue, } from "../config.js";
51
55
  import { createDebugReportArtifact } from "./debug-report.js";
52
56
  import { buildGameTaskPlan, gameTaskPlanInputSchema } from "./game-task-plan.js";
@@ -608,7 +612,45 @@ export const TOOL_DISPATCH = [
608
612
  throw err;
609
613
  }
610
614
  }),
611
- entry("summer_creator_publish", "Publish an exported .pck through the creator API (confirm-gated)", false, (args) => publishCreator({
615
+ entry("summer_export_game", "Export the game as a summer.games .zip with the installed engine (headless)", false, (args) => exportGame({
616
+ project: optStr(args, "project"),
617
+ out: optStr(args, "out"),
618
+ targets: Array.isArray(args.targets) ? args.targets.map(String) : undefined,
619
+ format: optStr(args, "format"),
620
+ debug: args.debug === true,
621
+ ...(typeof args.timeoutSeconds === "number" ? { timeoutMs: args.timeoutSeconds * 1000 } : {}),
622
+ })),
623
+ entry("summer_export_templates", "List or install Summer export templates from the CDN (web, macos, windows)", false, (args) => {
624
+ const action = optStr(args, "action");
625
+ if (action !== "list" && action !== "install")
626
+ throw new ToolDispatchError('action must be "list" or "install".');
627
+ return exportTemplates({
628
+ action,
629
+ platforms: Array.isArray(args.platforms) ? args.platforms.map(String) : undefined,
630
+ includeDebug: args.includeDebug === true,
631
+ summerVersion: optStr(args, "summerVersion"),
632
+ });
633
+ }),
634
+ entry("summer_capture_gameplay", "Capture real gameplay frames offscreen, without a running editor", false, (args) => captureGameplay({
635
+ project: optStr(args, "project"),
636
+ scene: optStr(args, "scene"),
637
+ resolution: optStr(args, "resolution"),
638
+ frames: optNumberOrUndefined(args, "frames"),
639
+ waitSeconds: optNumberOrUndefined(args, "waitSeconds"),
640
+ intervalSeconds: optNumberOrUndefined(args, "intervalSeconds"),
641
+ out: optStr(args, "out"),
642
+ })),
643
+ entry("summer_publish_build", "Upload a summer.games export to your store game (confirm-gated)", false, (args) => publishBuild({
644
+ gameId: optStr(args, "gameId"),
645
+ file: optStr(args, "file"),
646
+ clientVersion: optStr(args, "clientVersion"),
647
+ publish: args.publish === true,
648
+ platform: optStr(args, "platform"),
649
+ confirm: args.confirm === true,
650
+ ...(typeof args.waitSeconds === "number" ? { waitSeconds: args.waitSeconds } : {}),
651
+ face: "cli",
652
+ })),
653
+ entry("summer_creator_publish", "Deprecated: publish a .pck to the legacy creator API; use publish-build", false, (args) => publishCreator({
612
654
  project: optStr(args, "project"),
613
655
  artifact: optStr(args, "artifact"),
614
656
  version: optStr(args, "version"),
@@ -88,17 +88,17 @@ export declare const uiActivateArgsSchema: z.ZodObject<{
88
88
  title: z.ZodOptional<z.ZodString>;
89
89
  button: z.ZodOptional<z.ZodString>;
90
90
  }, "strip", z.ZodTypeAny, {
91
+ value?: string | number | undefined;
91
92
  title?: string | undefined;
92
93
  path?: string | undefined;
93
- value?: string | number | undefined;
94
94
  action?: "focus" | "press" | "toggle" | "select_tab" | "set_text" | "set_value" | "dismiss_dialog" | undefined;
95
95
  button?: string | undefined;
96
96
  index?: number | undefined;
97
97
  submit?: boolean | undefined;
98
98
  }, {
99
+ value?: string | number | undefined;
99
100
  title?: string | undefined;
100
101
  path?: string | undefined;
101
- value?: string | number | undefined;
102
102
  action?: "focus" | "press" | "toggle" | "select_tab" | "set_text" | "set_value" | "dismiss_dialog" | undefined;
103
103
  button?: string | undefined;
104
104
  index?: number | undefined;
@@ -30,7 +30,7 @@ function validateGatewayUrl(value) {
30
30
  }
31
31
  const local = parsed.hostname === "localhost" ||
32
32
  parsed.hostname === "127.0.0.1" ||
33
- parsed.hostname === "::1";
33
+ parsed.hostname === "[::1]";
34
34
  if (parsed.protocol !== "https:" && !(local && parsed.protocol === "http:")) {
35
35
  throw new Error("gateway.url must use HTTPS (HTTP is allowed only for localhost). Recovery: set an HTTPS URL and retry.");
36
36
  }
@@ -49,7 +49,7 @@ function validateCreatorApiUrl(value) {
49
49
  }
50
50
  const local = parsed.hostname === "localhost" ||
51
51
  parsed.hostname === "127.0.0.1" ||
52
- parsed.hostname === "::1";
52
+ parsed.hostname === "[::1]";
53
53
  if (parsed.protocol !== "https:" && !(local && parsed.protocol === "http:")) {
54
54
  throw new Error("creator.apiUrl must use HTTPS (HTTP is allowed only for localhost). Recovery: set an HTTPS URL and retry.");
55
55
  }
@@ -58,6 +58,22 @@ export declare function linuxEngineInstallDir(): string;
58
58
  export declare function macEnginePaths(env?: NodeJS.ProcessEnv): string[];
59
59
  export declare function windowsEnginePaths(env?: NodeJS.ProcessEnv): string[];
60
60
  export declare function linuxEnginePaths(): string[];
61
+ /** Where `summer install` records the engine it installed last
62
+ * (~/.summer/engine-install.json), so an install with --path is found later
63
+ * without SUMMER_BIN. */
64
+ export declare const ENGINE_INSTALL_FILE = "engine-install.json";
65
+ export interface RecordedEngineInstall {
66
+ schemaVersion: 1;
67
+ /** The engine executable (on macOS the one inside Summer.app). */
68
+ binary: string;
69
+ version: string | null;
70
+ installedAt: string;
71
+ }
72
+ export declare function recordEngineInstall(binary: string, version: string | null): Promise<void>;
73
+ /** The engine `summer install` installed last, or null (sync: findEngineBinary is). */
74
+ export declare function recordedEngineBinary(): string | null;
75
+ /** The bundle summer install writes on macOS: <path>/Summer.app, or the path itself when it names a .app. */
76
+ export declare function macInstallDestination(customPath?: string): string;
61
77
  export declare function engineBinaryCandidates(os?: NodeJS.Platform, env?: NodeJS.ProcessEnv): string[];
62
78
  /** First existing engine binary for this machine, or null. SUMMER_BIN /
63
79
  * SUMMER_ENGINE_BINARY wins when set and present; a set-but-missing override
@@ -1,6 +1,7 @@
1
- import { existsSync, statSync } from "fs";
1
+ import { existsSync, readFileSync, statSync } from "fs";
2
2
  import { homedir, platform } from "os";
3
3
  import { basename, join } from "path";
4
+ import { getSummerDir, writeStoreJson } from "./store.js";
4
5
  /**
5
6
  * Where each platform's engine binary lives, shared by `summer install`,
6
7
  * `summer run`, and `summer doctor` so they can never disagree about what
@@ -95,6 +96,31 @@ export function windowsEnginePaths(env = process.env) {
95
96
  export function linuxEnginePaths() {
96
97
  return [join(linuxEngineInstallDir(), LINUX_ENGINE_BINARY_NAME)];
97
98
  }
99
+ /** Where `summer install` records the engine it installed last
100
+ * (~/.summer/engine-install.json), so an install with --path is found later
101
+ * without SUMMER_BIN. */
102
+ export const ENGINE_INSTALL_FILE = "engine-install.json";
103
+ export async function recordEngineInstall(binary, version) {
104
+ const record = { schemaVersion: 1, binary, version, installedAt: new Date().toISOString() };
105
+ await writeStoreJson(ENGINE_INSTALL_FILE, record);
106
+ }
107
+ /** The engine `summer install` installed last, or null (sync: findEngineBinary is). */
108
+ export function recordedEngineBinary() {
109
+ try {
110
+ const record = JSON.parse(readFileSync(join(getSummerDir(), ENGINE_INSTALL_FILE), "utf8"));
111
+ return record.schemaVersion === 1 && typeof record.binary === "string" && record.binary ? record.binary : null;
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ /** The bundle summer install writes on macOS: <path>/Summer.app, or the path itself when it names a .app. */
118
+ export function macInstallDestination(customPath) {
119
+ if (!customPath?.trim())
120
+ return "/Applications/Summer.app";
121
+ const trimmed = customPath.trim().replace(/\/+$/, "");
122
+ return /\.app$/i.test(trimmed) ? trimmed : join(trimmed, "Summer.app");
123
+ }
98
124
  export function engineBinaryCandidates(os = platform(), env = process.env) {
99
125
  const override = engineBinaryOverride(env)?.path;
100
126
  const platformPaths = os === "darwin"
@@ -102,7 +128,11 @@ export function engineBinaryCandidates(os = platform(), env = process.env) {
102
128
  : os === "linux"
103
129
  ? linuxEnginePaths()
104
130
  : windowsEnginePaths(env);
105
- return override ? [override, ...platformPaths] : platformPaths;
131
+ // The engine summer install put in place last wins over the default
132
+ // locations (an install with --path next to an older /Applications copy).
133
+ const recorded = recordedEngineBinary();
134
+ const paths = recorded ? [recorded, ...platformPaths.filter((path) => path !== recorded)] : platformPaths;
135
+ return override ? [override, ...paths] : paths;
106
136
  }
107
137
  /** First existing engine binary for this machine, or null. SUMMER_BIN /
108
138
  * SUMMER_ENGINE_BINARY wins when set and present; a set-but-missing override