summer-engine 4.3.0 → 4.5.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 +1 -1
- package/.claude-plugin/plugin.json +3 -1
- package/.codex-plugin/plugin.json +5 -3
- package/.cursor-plugin/plugin.json +4 -2
- package/.factory-plugin/plugin.json +4 -2
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +87 -0
- package/README.md +67 -27
- package/commands/start.md +19 -15
- package/commands/summer.md +1 -0
- package/dist/cli/command-usage.d.ts +3 -1
- package/dist/cli/command-usage.js +10 -4
- package/dist/cli/commands/create.d.ts +1 -25
- package/dist/cli/commands/create.js +10 -138
- package/dist/cli/commands/dev.d.ts +66 -0
- package/dist/cli/commands/dev.js +234 -0
- package/dist/cli/commands/install.d.ts +15 -58
- package/dist/cli/commands/install.js +31 -481
- package/dist/cli/commands/login.js +1 -1
- package/dist/cli/commands/ops.d.ts +17 -1
- package/dist/cli/commands/ops.js +97 -9
- package/dist/cli/commands/preview.d.ts +58 -0
- package/dist/cli/commands/preview.js +181 -0
- package/dist/cli/commands/run.d.ts +1 -17
- package/dist/cli/commands/run.js +17 -206
- package/dist/cli/index.js +5 -0
- package/dist/core/agent-host.d.ts +3 -0
- package/dist/core/agent-host.js +1 -1
- package/dist/core/api-client.d.ts +10 -2
- package/dist/core/api-client.js +13 -8
- package/dist/core/capabilities/agent-playbook.js +1 -0
- package/dist/core/capabilities/apps.d.ts +9 -9
- package/dist/core/capabilities/asset-import.d.ts +4 -1
- package/dist/core/capabilities/asset-import.js +15 -6
- package/dist/core/capabilities/capture-gameplay.js +1 -1
- package/dist/core/capabilities/connect-signal.d.ts +2 -2
- package/dist/core/capabilities/content-make/index.d.ts +81 -0
- package/dist/core/capabilities/content-make/index.js +706 -0
- package/dist/core/capabilities/content-make/media.d.ts +57 -0
- package/dist/core/capabilities/content-make/media.js +172 -0
- package/dist/core/capabilities/content-make/plan.d.ts +130 -0
- package/dist/core/capabilities/content-make/plan.js +433 -0
- package/dist/core/capabilities/content-make/render.d.ts +64 -0
- package/dist/core/capabilities/content-make/render.js +370 -0
- package/dist/core/capabilities/content-make/study.d.ts +13 -0
- package/dist/core/capabilities/content-make/study.js +165 -0
- package/dist/core/capabilities/content-make/types.d.ts +173 -0
- package/dist/core/capabilities/content-make/types.js +6 -0
- package/dist/core/capabilities/content-make/voice.d.ts +7 -0
- package/dist/core/capabilities/content-make/voice.js +27 -0
- package/dist/core/capabilities/events.d.ts +2 -2
- package/dist/core/capabilities/export-game.d.ts +4 -1
- package/dist/core/capabilities/export-game.js +4 -3
- package/dist/core/capabilities/export-presets.d.ts +2 -1
- package/dist/core/capabilities/export-presets.js +2 -1
- package/dist/core/capabilities/go-live.d.ts +14 -0
- package/dist/core/capabilities/go-live.js +408 -0
- package/dist/core/capabilities/image-generation.d.ts +6 -6
- package/dist/core/capabilities/item-manifest.d.ts +92 -0
- package/dist/core/capabilities/item-manifest.js +247 -0
- package/dist/core/capabilities/placement.d.ts +7 -7
- package/dist/core/capabilities/projects-local.d.ts +68 -0
- package/dist/core/capabilities/projects-local.js +172 -0
- package/dist/core/capabilities/publish-build.d.ts +13 -2
- package/dist/core/capabilities/publish-build.js +87 -21
- package/dist/core/capabilities/publish-download.js +1 -1
- package/dist/core/capabilities/runtime-control.d.ts +7 -2
- package/dist/core/capabilities/runtime-control.js +27 -1
- package/dist/core/capabilities/seeing/args.d.ts +2 -2
- package/dist/core/capabilities/store-checks.d.ts +45 -0
- package/dist/core/capabilities/store-checks.js +85 -0
- package/dist/core/capabilities/store-details.d.ts +37 -0
- package/dist/core/capabilities/store-details.js +94 -0
- package/dist/core/capabilities/summer-bundle.d.ts +21 -2
- package/dist/core/capabilities/summer-bundle.js +54 -7
- package/dist/core/capabilities/sync-items.d.ts +89 -0
- package/dist/core/capabilities/sync-items.js +352 -0
- package/dist/core/capabilities/tool-dispatch.js +66 -6
- package/dist/core/capabilities/video-ogv.d.ts +49 -0
- package/dist/core/capabilities/video-ogv.js +260 -0
- package/dist/core/capability-skew.js +2 -2
- package/dist/core/cli-usage-beacon.js +26 -2
- package/dist/core/cli-usage.d.ts +5 -2
- package/dist/core/cli-usage.js +8 -2
- package/dist/core/engine-install.d.ts +53 -0
- package/dist/core/engine-install.js +93 -0
- package/dist/core/engine-installer.d.ts +134 -0
- package/dist/core/engine-installer.js +760 -0
- package/dist/core/engine-launch.d.ts +126 -0
- package/dist/core/engine-launch.js +398 -0
- package/dist/core/engine-self-start.d.ts +63 -0
- package/dist/core/engine-self-start.js +282 -0
- package/dist/core/engine.d.ts +7 -1
- package/dist/core/engine.js +9 -2
- package/dist/core/error-codes.d.ts +4 -1
- package/dist/core/error-codes.js +7 -3
- package/dist/core/library-read.d.ts +2 -2
- package/dist/core/library-search.d.ts +2 -2
- package/dist/core/local-project.d.ts +50 -0
- package/dist/core/local-project.js +234 -0
- package/dist/core/ops/areas.js +9 -3
- package/dist/core/ops/catalog.d.ts +9 -0
- package/dist/core/ops/catalog.js +56 -4
- package/dist/core/ops/engine-answer.d.ts +4 -1
- package/dist/core/ops/engine-answer.js +16 -3
- package/dist/core/ops/engine.js +4 -4
- package/dist/core/ops/save-files.d.ts +6 -1
- package/dist/core/ops/save-files.js +67 -50
- package/dist/core/ops/search.js +9 -2
- package/dist/core/preview/input.d.ts +35 -0
- package/dist/core/preview/input.js +105 -0
- package/dist/core/preview/launch-config.d.ts +52 -0
- package/dist/core/preview/launch-config.js +137 -0
- package/dist/core/preview/page.d.ts +22 -0
- package/dist/core/preview/page.js +232 -0
- package/dist/core/preview/preview-source.d.ts +179 -0
- package/dist/core/preview/preview-source.js +350 -0
- package/dist/core/preview/server.d.ts +26 -0
- package/dist/core/preview/server.js +218 -0
- package/dist/core/telemetry.d.ts +20 -16
- package/dist/core/telemetry.js +92 -16
- package/dist/core/templates.d.ts +20 -0
- package/dist/core/templates.js +300 -3
- package/dist/core/tool-profiles.d.ts +2 -2
- package/dist/core/tool-profiles.js +1 -0
- package/dist/core/usage-fields.d.ts +41 -0
- package/dist/core/usage-fields.js +150 -0
- package/dist/core/util/sleep.d.ts +3 -0
- package/dist/core/util/sleep.js +12 -0
- package/dist/mcp/ops-surface.js +28 -2
- package/dist/mcp/server.d.ts +14 -4
- package/dist/mcp/server.js +42 -8
- package/dist/mcp/tool-surface.d.ts +8 -0
- package/dist/mcp/tool-surface.js +19 -2
- package/dist/mcp/tools/creator-tools.js +92 -14
- package/dist/mcp/tools/debug-tools.js +2 -0
- package/dist/mcp/tools/local-project-tools.d.ts +6 -0
- package/dist/mcp/tools/local-project-tools.js +33 -0
- package/dist/mcp/tools/navigation-tools.js +2 -1
- package/dist/mcp/tools/project-tools.js +5 -8
- package/dist/mcp/tools/seeing-tools.js +1 -1
- package/dist/mcp/tools/visual-tools.js +1 -1
- package/dist/mcp/tools/with-engine.js +1 -1
- package/gemini-extension.json +3 -1
- package/library/references/mcp-tools-reference/mcp-tools-reference.md +18 -5
- package/library/skills/README.md +12 -8
- package/library/skills/animated-loop/SKILL.md +10 -16
- package/library/skills/cinematic-cutscene/SKILL.md +7 -18
- package/library/skills/headless-scripting/SKILL.md +7 -4
- package/library/skills/in-game-purchases/SKILL.md +25 -8
- package/library/skills/in-game-purchases/resource.yaml +2 -1
- package/library/skills/launch-ready/SKILL.md +351 -0
- package/library/skills/launch-ready/resource.yaml +45 -0
- package/library/skills/make-content/SKILL.md +202 -0
- package/library/skills/make-content/resource.yaml +32 -0
- package/library/skills/make-game/SKILL.md +2 -0
- package/library/skills/make-game/resource.yaml +2 -1
- package/library/skills/navigate-summer/SKILL.md +4 -2
- package/library/skills/new-project/SKILL.md +15 -22
- package/library/skills/new-project/resource.yaml +2 -1
- package/library/skills/painted-25d-world/SKILL.md +366 -0
- package/library/skills/painted-25d-world/_building-blocks/blender-route.md +33 -0
- package/library/skills/painted-25d-world/_building-blocks/blockout_builder.gd +149 -0
- package/library/skills/painted-25d-world/_building-blocks/capture_paint_view.gd +32 -0
- package/library/skills/painted-25d-world/_building-blocks/paint_projection.gdshader +23 -0
- package/library/skills/painted-25d-world/_building-blocks/paintcam_check.gd +25 -0
- package/library/skills/painted-25d-world/_building-blocks/painted_door.gd +17 -0
- package/library/skills/painted-25d-world/_building-blocks/painted_location.gd +56 -0
- package/library/skills/painted-25d-world/_building-blocks/painted_player.gd +125 -0
- package/library/skills/painted-25d-world/_building-blocks/painted_world.gd +111 -0
- package/library/skills/painted-25d-world/_building-blocks/tools/cam.py +57 -0
- package/library/skills/painted-25d-world/_building-blocks/tools/detail4k.py +81 -0
- package/library/skills/painted-25d-world/_building-blocks/tools/overlay.py +17 -0
- package/library/skills/painted-25d-world/_building-blocks/tools/preview.py +60 -0
- package/library/skills/painted-25d-world/resource.yaml +39 -0
- package/library/skills/publish-your-game/SKILL.md +13 -14
- package/library/skills/publish-your-game/resource.yaml +4 -3
- package/library/skills/release-checklist/SKILL.md +188 -0
- package/library/skills/release-checklist/resource.yaml +40 -0
- package/library/skills/release-readiness/SKILL.md +1 -1
- package/library/skills/release-readiness/resource.yaml +1 -1
- package/library/skills/store-art/SKILL.md +1 -1
- package/library/skills/store-art/resource.yaml +1 -1
- package/library/skills/store-listing/SKILL.md +1 -1
- package/library/skills/store-listing/resource.yaml +1 -1
- package/library/skills/summer-cli/SKILL.md +2 -0
- package/library/skills/summer-store/SKILL.md +295 -39
- package/library/skills/summer-store/resource.yaml +9 -3
- package/library/skills/trailer-shot/SKILL.md +3 -3
- package/library/templates/README.md +5 -0
- package/library/tools/content-make/resource.yaml +132 -0
- package/library/tools/frame-shot/GUIDE.md +1 -1
- package/library/tools/go-live/resource.yaml +67 -0
- package/library/tools/import-from-url/GUIDE.md +1 -0
- package/library/tools/import-from-url-batch/GUIDE.md +1 -1
- package/library/tools/play/GUIDE.md +2 -0
- package/library/tools/projects-create-local/resource.yaml +55 -0
- package/library/tools/projects-open-local/resource.yaml +51 -0
- package/library/tools/publish-build/resource.yaml +3 -3
- package/library/tools/screenshot/GUIDE.md +1 -1
- package/library/tools/store-set-details/resource.yaml +114 -0
- package/library/tools/sync-items/resource.yaml +72 -0
- package/package.json +1 -1
- package/registry/generated/aliases.json +6 -0
- package/registry/generated/counts.json +3 -3
- package/registry/generated/gemini-extension.json +3 -1
- package/registry/generated/index.json +463 -37
- package/registry/generated/marketplace.claude.json +1 -1
- package/registry/generated/plugin.claude-directory.json +4 -2
- package/registry/generated/plugin.claude.json +3 -1
- 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/server.json +2 -2
- package/registry/generated/skills-index.md +12 -8
- package/registry/generated/skills-registry.json +62 -5
- package/registry/operations/legacy-names.json +40 -0
- package/registry/operations/public-catalog.json +10144 -387
|
@@ -5,6 +5,7 @@ import { copyFile, link, mkdir, open, readdir, readFile, stat, unlink } from "no
|
|
|
5
5
|
import { BlockList, isIP } from "node:net";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
|
+
import { cloudinaryOgvUrl, isConvertibleVideo } from "../capabilities/video-ogv.js";
|
|
8
9
|
/**
|
|
9
10
|
* Generated files on this machine (4.0). The generate operations, assets.get
|
|
10
11
|
* and jobs.wait take `out` here: a folder or a file path. This package keeps
|
|
@@ -42,7 +43,7 @@ const DOWNLOAD_TIMEOUT_MS = 120_000;
|
|
|
42
43
|
export const MAX_DOWNLOAD_BYTES = 500 * 1024 * 1024;
|
|
43
44
|
const MAX_REDIRECTS = 5;
|
|
44
45
|
/** The file types a save writes: media and models, never code or a library a game engine would load. */
|
|
45
|
-
const ALLOWED_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".mp3", ".wav", ".ogg", ".mp4", ".webm", ".glb", ".gltf", ".fbx", ".obj"]);
|
|
46
|
+
const ALLOWED_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".mp3", ".wav", ".ogg", ".mp4", ".webm", ".ogv", ".glb", ".gltf", ".fbx", ".obj"]);
|
|
46
47
|
const EXTENSIONS = {
|
|
47
48
|
"image/png": ".png",
|
|
48
49
|
"image/jpeg": ".jpg",
|
|
@@ -55,6 +56,7 @@ const EXTENSIONS = {
|
|
|
55
56
|
"audio/ogg": ".ogg",
|
|
56
57
|
"video/mp4": ".mp4",
|
|
57
58
|
"video/webm": ".webm",
|
|
59
|
+
"video/ogg": ".ogv",
|
|
58
60
|
"model/gltf-binary": ".glb",
|
|
59
61
|
"model/gltf+json": ".gltf",
|
|
60
62
|
};
|
|
@@ -120,6 +122,12 @@ function holdersOf(result) {
|
|
|
120
122
|
const list = [...(Array.isArray(record.assets) ? record.assets : []), record.asset, record];
|
|
121
123
|
return list.map(asRecord).filter((holder) => holder !== null && urlOf(holder) !== undefined);
|
|
122
124
|
}
|
|
125
|
+
/** A video file Summer Engine cannot load as is (mime type, file name or URL). */
|
|
126
|
+
function isVideoHolder(holder, url) {
|
|
127
|
+
if (text(holder.mimeType)?.toLowerCase().startsWith("video/"))
|
|
128
|
+
return true;
|
|
129
|
+
return [text(holder.fileName), url].some((name) => name !== undefined && isConvertibleVideo({ url: name }));
|
|
130
|
+
}
|
|
123
131
|
function withWarnings(answer, warnings) {
|
|
124
132
|
return answer.ok && warnings.length ? { ...answer, warnings: [...(answer.warnings ?? []), ...warnings] } : answer;
|
|
125
133
|
}
|
|
@@ -287,18 +295,24 @@ export class FileSaver {
|
|
|
287
295
|
return withWarnings(answer, [target.error]);
|
|
288
296
|
const copy = structuredClone(answer);
|
|
289
297
|
const warnings = [];
|
|
298
|
+
// Summer Engine plays video only as Ogg Theora: in a Godot project a stored video saves as its .ogv rendition.
|
|
299
|
+
const godot = (await findGameProject(target.dir))?.engine === "godot";
|
|
290
300
|
// With out the caller asked for a copy there; without it, a file this session already saved is reused.
|
|
291
301
|
const saved = out ? new Map() : this.defaultSaves;
|
|
292
302
|
let first = true;
|
|
293
303
|
for (const holder of holdersOf(resultOf(copy))) {
|
|
294
|
-
const
|
|
304
|
+
const original = urlOf(holder);
|
|
305
|
+
const ogv = godot && isVideoHolder(holder, original) ? cloudinaryOgvUrl(text(holder.fileUrl) ?? original) : null;
|
|
306
|
+
const url = ogv ?? original;
|
|
295
307
|
const known = saved.get(url);
|
|
296
308
|
if (known && (await isFile(known))) {
|
|
297
309
|
holder.localPath = known;
|
|
298
310
|
continue;
|
|
299
311
|
}
|
|
300
312
|
try {
|
|
301
|
-
const path = await this.download(url, holder, target, first, warnings);
|
|
313
|
+
const path = await this.download(url, holder, target, first, warnings, ogv ? ".ogv" : undefined);
|
|
314
|
+
if (ogv)
|
|
315
|
+
warnings.push(`Saved as ${path}: Summer Engine plays video only as Ogg Theora (.ogv).`);
|
|
302
316
|
first = false;
|
|
303
317
|
saved.set(url, path);
|
|
304
318
|
holder.localPath = path;
|
|
@@ -340,15 +354,15 @@ export class FileSaver {
|
|
|
340
354
|
return folder ? { dir: path } : { dir: dirname(path), file: basename(path) };
|
|
341
355
|
}
|
|
342
356
|
/** Download one file into the target under a free name; notes what changed from the caller's file name. */
|
|
343
|
-
async download(url, holder, target, first, warnings) {
|
|
357
|
+
async download(url, holder, target, first, warnings, forcedExt) {
|
|
344
358
|
const { temp, finalUrl, contentType } = await this.fetchToTemp(url, target.dir);
|
|
345
359
|
try {
|
|
346
|
-
const detected = allowed(extname(text(holder.fileName) ?? "")) ?? allowed(extname(finalUrl.pathname)) ?? EXTENSIONS[contentType ?? ""] ?? EXTENSIONS[text(holder.mimeType) ?? ""] ?? ".bin";
|
|
360
|
+
const detected = forcedExt ?? allowed(extname(text(holder.fileName) ?? "")) ?? allowed(extname(finalUrl.pathname)) ?? EXTENSIONS[contentType ?? ""] ?? EXTENSIONS[text(holder.mimeType) ?? ""] ?? ".bin";
|
|
347
361
|
if (target.file) {
|
|
348
362
|
const asked = extname(target.file);
|
|
349
|
-
const ext = allowed(asked) ?? detected;
|
|
363
|
+
const ext = forcedExt ?? allowed(asked) ?? detected;
|
|
350
364
|
const path = await claimName(temp, target.dir, basename(target.file, asked), ext);
|
|
351
|
-
const wanted = join(target.dir, target.file);
|
|
365
|
+
const wanted = join(target.dir, forcedExt ? `${basename(target.file, asked)}${forcedExt}` : target.file);
|
|
352
366
|
if (first && path !== wanted) {
|
|
353
367
|
warnings.push(allowed(asked) ? `${wanted} already exists, so the file was saved as ${path}.` : `out: ${asked || "no extension"} is not a media or model file type, so the file was saved as ${path}.`);
|
|
354
368
|
}
|
|
@@ -362,52 +376,55 @@ export class FileSaver {
|
|
|
362
376
|
await unlink(temp).catch(() => undefined);
|
|
363
377
|
}
|
|
364
378
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (
|
|
382
|
-
throw new Error(
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
379
|
+
fetchToTemp(url, dir) {
|
|
380
|
+
return fetchPublicToTemp(url, dir, this.options);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/** The file in a hidden temp file in `dir` (engines skip dot files), from a public https host, within the size limit. */
|
|
384
|
+
export async function fetchPublicToTemp(url, dir, options = {}) {
|
|
385
|
+
const lookupHost = options.lookup ?? lookupAll;
|
|
386
|
+
const fetchFile = options.fetch ?? fetch;
|
|
387
|
+
let current = await publicHttps(url, lookupHost);
|
|
388
|
+
let response;
|
|
389
|
+
for (let hops = 0;; hops++) {
|
|
390
|
+
response = await fetchFile(current.href, { redirect: "manual", signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
|
|
391
|
+
const location = response.headers.get("location");
|
|
392
|
+
if (response.status < 300 || response.status >= 400 || !location)
|
|
393
|
+
break;
|
|
394
|
+
await response.body?.cancel().catch(() => undefined);
|
|
395
|
+
if (hops >= MAX_REDIRECTS)
|
|
396
|
+
throw new Error("the download redirected too many times");
|
|
397
|
+
current = await publicHttps(new URL(location, current).href, lookupHost);
|
|
398
|
+
}
|
|
399
|
+
if (!response.ok)
|
|
400
|
+
throw new Error(`the download answered HTTP ${response.status}`);
|
|
401
|
+
if (response.url)
|
|
402
|
+
current = await publicHttps(response.url, lookupHost);
|
|
403
|
+
const maxBytes = options.maxBytes ?? MAX_DOWNLOAD_BYTES;
|
|
404
|
+
const limit = maxBytes >= 1024 * 1024 ? `${Math.floor(maxBytes / 1024 / 1024)} MB` : `${maxBytes} bytes`;
|
|
405
|
+
if (Number(response.headers.get("content-length") ?? 0) > maxBytes)
|
|
406
|
+
throw new Error(`the file is larger than ${limit}`);
|
|
407
|
+
await mkdir(dir, { recursive: true });
|
|
408
|
+
const temp = join(dir, `.summer-download-${randomBytes(6).toString("hex")}.part`);
|
|
409
|
+
const handle = await open(temp, "wx");
|
|
410
|
+
try {
|
|
411
|
+
let size = 0;
|
|
412
|
+
if (response.body) {
|
|
413
|
+
for await (const chunk of response.body) {
|
|
414
|
+
size += chunk.byteLength;
|
|
415
|
+
if (size > maxBytes)
|
|
416
|
+
throw new Error(`the file is larger than ${limit}`);
|
|
417
|
+
await handle.write(chunk);
|
|
401
418
|
}
|
|
402
|
-
await handle.close();
|
|
403
|
-
}
|
|
404
|
-
catch (error) {
|
|
405
|
-
await handle.close().catch(() => undefined);
|
|
406
|
-
await unlink(temp).catch(() => undefined);
|
|
407
|
-
throw error;
|
|
408
419
|
}
|
|
409
|
-
|
|
420
|
+
await handle.close();
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
await handle.close().catch(() => undefined);
|
|
424
|
+
await unlink(temp).catch(() => undefined);
|
|
425
|
+
throw error;
|
|
410
426
|
}
|
|
427
|
+
return { temp, finalUrl: current, contentType: response.headers.get("content-type")?.split(";")[0].trim() };
|
|
411
428
|
}
|
|
412
429
|
/** ops.describe of an operation that takes `out`: its input shows `out` too. */
|
|
413
430
|
function describedWithOut(answer) {
|
package/dist/core/ops/search.js
CHANGED
|
@@ -16,7 +16,9 @@ import { stem } from "../registry-search.js";
|
|
|
16
16
|
* action the query names gains a quarter: "publish" is store.publish
|
|
17
17
|
* before store.cancel_publish.
|
|
18
18
|
* - A title or tag the query says nearly word for word, articles aside
|
|
19
|
-
* ("how is my game doing" for "how did my game do"), adds 2 per word
|
|
19
|
+
* ("how is my game doing" for "how did my game do"), adds 2 per word, and
|
|
20
|
+
* 4 when the query is the title exactly: "list my games" is games.list
|
|
21
|
+
* ("List my games") before content.list_games ("List the team's games").
|
|
20
22
|
*/
|
|
21
23
|
const STOP = new Set([
|
|
22
24
|
"a", "an", "the", "my", "of", "for", "to", "in", "on", "and", "or", "with", "is", "it", "me", "i", "please", "how", "do", "what",
|
|
@@ -130,6 +132,11 @@ function phraseBonus(entry, all) {
|
|
|
130
132
|
}
|
|
131
133
|
return bonus;
|
|
132
134
|
}
|
|
135
|
+
/** The query is the row's title word for word, articles included: "list my games" for "List my games". */
|
|
136
|
+
function saysTitle(entry, all) {
|
|
137
|
+
const title = tokens(entry.title);
|
|
138
|
+
return title.length === all.length && title.every((word, index) => word.normed === all[index].normed);
|
|
139
|
+
}
|
|
133
140
|
/**
|
|
134
141
|
* The query names the row's whole action, and its area too when the action
|
|
135
142
|
* is only an everyday verb: "list my games" names games.list, "run my game"
|
|
@@ -167,7 +174,7 @@ function scoreAll(entries, query) {
|
|
|
167
174
|
});
|
|
168
175
|
if (total === 0)
|
|
169
176
|
return 0;
|
|
170
|
-
return total * (0.5 + (0.5 * strong) / terms.length) * (!missed && namesAction(entry, terms) ? 1.25 : 1) + phraseBonus(entry, all);
|
|
177
|
+
return total * (0.5 + (0.5 * strong) / terms.length) * (!missed && namesAction(entry, terms) ? 1.25 : 1) + phraseBonus(entry, all) * (saysTitle(entry, all) ? 2 : 1);
|
|
171
178
|
});
|
|
172
179
|
}
|
|
173
180
|
const STATUS_RANK = { stable: 0, preview: 1, deprecated: 2 };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser input -> the running game's input, for `summer preview`. The page
|
|
3
|
+
* sends what the person (or the agent's browser) did on the frame; this turns
|
|
4
|
+
* it into SimulateInputScript events (core/capabilities/runtime-control.ts),
|
|
5
|
+
* the same op summer_game_input sends.
|
|
6
|
+
*
|
|
7
|
+
* Keys: KeyboardEvent.code (the physical key) -> Godot's Key enum, sent as
|
|
8
|
+
* keycode AND physical_keycode so an InputMap action bound either way fires.
|
|
9
|
+
* Godot names letters by their upper-case code point and the special keys
|
|
10
|
+
* SPECIAL (1 << 22) | n (core/os/keyboard.h). Clicks: frame pixels scaled to
|
|
11
|
+
* the game window's own size (the engine downscales the frames it sends).
|
|
12
|
+
*/
|
|
13
|
+
export type PreviewInput = {
|
|
14
|
+
kind: "key";
|
|
15
|
+
code: string;
|
|
16
|
+
pressed: boolean;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "click";
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
button?: number;
|
|
22
|
+
};
|
|
23
|
+
/** Godot Key enum value for a KeyboardEvent.code, or null when unmapped. */
|
|
24
|
+
export declare function godotKeycode(code: string): number | null;
|
|
25
|
+
/** Frame size and the game window size it was scaled from. */
|
|
26
|
+
export interface FrameGeometry {
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
sourceWidth?: number;
|
|
30
|
+
sourceHeight?: number;
|
|
31
|
+
}
|
|
32
|
+
/** One SimulateInputScript event, or null for input the game cannot take. */
|
|
33
|
+
export declare function engineInputEvent(input: PreviewInput, frame: FrameGeometry | null): Record<string, unknown> | null;
|
|
34
|
+
/** Parse the page's POST body: {inputs: PreviewInput[]} (at most 64). */
|
|
35
|
+
export declare function parsePreviewInputs(body: unknown): PreviewInput[] | null;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser input -> the running game's input, for `summer preview`. The page
|
|
3
|
+
* sends what the person (or the agent's browser) did on the frame; this turns
|
|
4
|
+
* it into SimulateInputScript events (core/capabilities/runtime-control.ts),
|
|
5
|
+
* the same op summer_game_input sends.
|
|
6
|
+
*
|
|
7
|
+
* Keys: KeyboardEvent.code (the physical key) -> Godot's Key enum, sent as
|
|
8
|
+
* keycode AND physical_keycode so an InputMap action bound either way fires.
|
|
9
|
+
* Godot names letters by their upper-case code point and the special keys
|
|
10
|
+
* SPECIAL (1 << 22) | n (core/os/keyboard.h). Clicks: frame pixels scaled to
|
|
11
|
+
* the game window's own size (the engine downscales the frames it sends).
|
|
12
|
+
*/
|
|
13
|
+
const SPECIAL = 1 << 22;
|
|
14
|
+
const NAMED_KEYS = {
|
|
15
|
+
Escape: SPECIAL | 0x01,
|
|
16
|
+
Tab: SPECIAL | 0x02,
|
|
17
|
+
Backspace: SPECIAL | 0x04,
|
|
18
|
+
Enter: SPECIAL | 0x05,
|
|
19
|
+
NumpadEnter: SPECIAL | 0x06,
|
|
20
|
+
Insert: SPECIAL | 0x07,
|
|
21
|
+
Delete: SPECIAL | 0x08,
|
|
22
|
+
Home: SPECIAL | 0x0d,
|
|
23
|
+
End: SPECIAL | 0x0e,
|
|
24
|
+
ArrowLeft: SPECIAL | 0x0f,
|
|
25
|
+
ArrowUp: SPECIAL | 0x10,
|
|
26
|
+
ArrowRight: SPECIAL | 0x11,
|
|
27
|
+
ArrowDown: SPECIAL | 0x12,
|
|
28
|
+
PageUp: SPECIAL | 0x13,
|
|
29
|
+
PageDown: SPECIAL | 0x14,
|
|
30
|
+
ShiftLeft: SPECIAL | 0x15,
|
|
31
|
+
ShiftRight: SPECIAL | 0x15,
|
|
32
|
+
ControlLeft: SPECIAL | 0x16,
|
|
33
|
+
ControlRight: SPECIAL | 0x16,
|
|
34
|
+
MetaLeft: SPECIAL | 0x17,
|
|
35
|
+
MetaRight: SPECIAL | 0x17,
|
|
36
|
+
AltLeft: SPECIAL | 0x18,
|
|
37
|
+
AltRight: SPECIAL | 0x18,
|
|
38
|
+
CapsLock: SPECIAL | 0x19,
|
|
39
|
+
Space: 32,
|
|
40
|
+
Quote: 39,
|
|
41
|
+
Comma: 44,
|
|
42
|
+
Minus: 45,
|
|
43
|
+
Period: 46,
|
|
44
|
+
Slash: 47,
|
|
45
|
+
Semicolon: 59,
|
|
46
|
+
Equal: 61,
|
|
47
|
+
BracketLeft: 91,
|
|
48
|
+
Backslash: 92,
|
|
49
|
+
BracketRight: 93,
|
|
50
|
+
Backquote: 96,
|
|
51
|
+
};
|
|
52
|
+
/** Godot Key enum value for a KeyboardEvent.code, or null when unmapped. */
|
|
53
|
+
export function godotKeycode(code) {
|
|
54
|
+
const letter = /^Key([A-Z])$/.exec(code);
|
|
55
|
+
if (letter)
|
|
56
|
+
return letter[1].charCodeAt(0);
|
|
57
|
+
const digit = /^Digit([0-9])$/.exec(code);
|
|
58
|
+
if (digit)
|
|
59
|
+
return digit[1].charCodeAt(0);
|
|
60
|
+
const fn = /^F([1-9]|1[0-2])$/.exec(code);
|
|
61
|
+
if (fn)
|
|
62
|
+
return SPECIAL | (0x1c + Number(fn[1]) - 1);
|
|
63
|
+
return NAMED_KEYS[code] ?? null;
|
|
64
|
+
}
|
|
65
|
+
/** One SimulateInputScript event, or null for input the game cannot take. */
|
|
66
|
+
export function engineInputEvent(input, frame) {
|
|
67
|
+
if (input.kind === "key") {
|
|
68
|
+
const keycode = godotKeycode(input.code);
|
|
69
|
+
if (keycode === null)
|
|
70
|
+
return null;
|
|
71
|
+
return { type: "key", keycode, physical_keycode: keycode, pressed: input.pressed !== false };
|
|
72
|
+
}
|
|
73
|
+
if (!frame || !Number.isFinite(input.x) || !Number.isFinite(input.y))
|
|
74
|
+
return null;
|
|
75
|
+
if (input.x < 0 || input.y < 0 || input.x > frame.width || input.y > frame.height)
|
|
76
|
+
return null;
|
|
77
|
+
const scaleX = frame.sourceWidth && frame.width ? frame.sourceWidth / frame.width : 1;
|
|
78
|
+
const scaleY = frame.sourceHeight && frame.height ? frame.sourceHeight / frame.height : 1;
|
|
79
|
+
const button = input.button === 2 || input.button === 3 ? input.button : 1;
|
|
80
|
+
return {
|
|
81
|
+
type: "mouse_click",
|
|
82
|
+
position: [Math.round(input.x * scaleX), Math.round(input.y * scaleY)],
|
|
83
|
+
button,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Parse the page's POST body: {inputs: PreviewInput[]} (at most 64). */
|
|
87
|
+
export function parsePreviewInputs(body) {
|
|
88
|
+
const inputs = body?.inputs;
|
|
89
|
+
if (!Array.isArray(inputs) || inputs.length === 0 || inputs.length > 64)
|
|
90
|
+
return null;
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const item of inputs) {
|
|
93
|
+
const record = item;
|
|
94
|
+
if (record?.kind === "key" && typeof record.code === "string") {
|
|
95
|
+
out.push({ kind: "key", code: record.code, pressed: record.pressed !== false });
|
|
96
|
+
}
|
|
97
|
+
else if (record?.kind === "click" && typeof record.x === "number" && typeof record.y === "number") {
|
|
98
|
+
out.push({ kind: "click", x: record.x, y: record.y, button: typeof record.button === "number" ? record.button : 1 });
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export declare const PREVIEW_LAUNCH_NAME = "summer-preview";
|
|
2
|
+
export declare const DEFAULT_PREVIEW_PORT = 5199;
|
|
3
|
+
/** The schema version Desktop writes and documents. */
|
|
4
|
+
export declare const LAUNCH_JSON_VERSION = "0.0.1";
|
|
5
|
+
export interface LaunchEntry {
|
|
6
|
+
name: string;
|
|
7
|
+
runtimeExecutable: string;
|
|
8
|
+
runtimeArgs: string[];
|
|
9
|
+
port: number;
|
|
10
|
+
/** Working directory relative to the session folder (Desktop's `cwd`). */
|
|
11
|
+
cwd?: string;
|
|
12
|
+
}
|
|
13
|
+
/** The entry that starts `summer preview` on `port`, in `cwd` (relative to
|
|
14
|
+
* the session folder) when the project is below it. */
|
|
15
|
+
export declare function previewLaunchEntry(port?: number, cwd?: string): LaunchEntry;
|
|
16
|
+
/** The indentation of a JSON text's first indented line (2 when none). */
|
|
17
|
+
export declare function jsonIndent(text: string): string | number;
|
|
18
|
+
export type LaunchConfigResult = {
|
|
19
|
+
/** created: a new file; added: our entry joined an existing file;
|
|
20
|
+
* kept: an entry with our name was already there, nothing written. */
|
|
21
|
+
status: "created" | "added" | "kept";
|
|
22
|
+
path: string;
|
|
23
|
+
entry: LaunchEntry;
|
|
24
|
+
/** The file's autoVerify after the write (Desktop's default is on). */
|
|
25
|
+
autoVerify: boolean;
|
|
26
|
+
} | {
|
|
27
|
+
status: "skipped";
|
|
28
|
+
path: string;
|
|
29
|
+
entry: LaunchEntry;
|
|
30
|
+
reason: string;
|
|
31
|
+
};
|
|
32
|
+
export interface LaunchConfigOptions {
|
|
33
|
+
/** The entry to add (default previewLaunchEntry()). `summer dev` can pass its own. */
|
|
34
|
+
entry?: LaunchEntry;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Add the preview entry to <sessionRoot>/.claude/launch.json, creating the
|
|
38
|
+
* file when there is none. sessionRoot is the folder Claude Code Desktop's
|
|
39
|
+
* session starts in. Never overwrites another entry or setting, and never
|
|
40
|
+
* rewrites a file it cannot parse as plain JSON.
|
|
41
|
+
*/
|
|
42
|
+
export declare function writeClaudeLaunchConfig(sessionRoot: string, options?: LaunchConfigOptions): LaunchConfigResult;
|
|
43
|
+
/** Where `summer preview` serves the running game (Claude Code Desktop opens localhost:<port>). */
|
|
44
|
+
export declare const PREVIEW_URL = "http://localhost:5199";
|
|
45
|
+
/**
|
|
46
|
+
* Register `summer preview` in <session folder>/.claude/launch.json, so
|
|
47
|
+
* Claude Code Desktop's Browser pane shows the game as soon as the session
|
|
48
|
+
* opens (core/preview/launch-config.ts: it only adds). Null when the session
|
|
49
|
+
* folder is the home folder or a filesystem root (~/.claude is Claude Code's
|
|
50
|
+
* own config), or the project is neither at, above nor below it.
|
|
51
|
+
*/
|
|
52
|
+
export declare function registerPreview(sessionFolder: string, projectDir: string): LaunchConfigResult | null;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .claude/launch.json: how Claude Code Desktop's Browser pane finds a local
|
|
3
|
+
* server to open, screenshot and check (https://code.claude.com/docs/en/desktop,
|
|
4
|
+
* "Preview", read 2026-10-19):
|
|
5
|
+
*
|
|
6
|
+
* { "version": "0.0.1", "autoVerify": true,
|
|
7
|
+
* "configurations": [{ "name", "runtimeExecutable", "runtimeArgs", "port", ... }] }
|
|
8
|
+
*
|
|
9
|
+
* The file lives in the folder the session was started in (the caller passes
|
|
10
|
+
* that folder; an entry whose project sits below it carries `cwd`, relative
|
|
11
|
+
* to it). autoVerify is a file-level switch and on by default: after each edit Claude screenshots the
|
|
12
|
+
* page, checks it for errors and fixes what is wrong. The pane opens
|
|
13
|
+
* http://localhost:<port>. When that port is taken, Desktop picks a free one
|
|
14
|
+
* and rewrites the `--port` value in runtimeArgs, so the entry passes its port
|
|
15
|
+
* there and sets no `url` (a localhost url must match the port).
|
|
16
|
+
*
|
|
17
|
+
* The writer only adds. Other entries and settings stay as they are, an
|
|
18
|
+
* existing summer-preview entry is kept as it is (the person may have changed
|
|
19
|
+
* it), and a file it cannot rewrite without losing something (comments, not
|
|
20
|
+
* JSON, an unexpected shape) is left alone; the result then carries the entry
|
|
21
|
+
* to add by hand. A rewrite keeps the file's indentation and replaces the
|
|
22
|
+
* file in one step (temp file + rename). The Codex app has an in-app browser too, but its docs
|
|
23
|
+
* describe no file that names a server or a URL, so there is nothing to write
|
|
24
|
+
* for it.
|
|
25
|
+
*/
|
|
26
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
27
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
28
|
+
import { isHomeOrRoot } from "../local-project.js";
|
|
29
|
+
import { asRecord } from "../util/json.js";
|
|
30
|
+
export const PREVIEW_LAUNCH_NAME = "summer-preview";
|
|
31
|
+
export const DEFAULT_PREVIEW_PORT = 5199;
|
|
32
|
+
/** The schema version Desktop writes and documents. */
|
|
33
|
+
export const LAUNCH_JSON_VERSION = "0.0.1";
|
|
34
|
+
/** The entry that starts `summer preview` on `port`, in `cwd` (relative to
|
|
35
|
+
* the session folder) when the project is below it. */
|
|
36
|
+
export function previewLaunchEntry(port = DEFAULT_PREVIEW_PORT, cwd) {
|
|
37
|
+
return {
|
|
38
|
+
name: PREVIEW_LAUNCH_NAME,
|
|
39
|
+
runtimeExecutable: "npx",
|
|
40
|
+
runtimeArgs: ["-y", "summer-engine@latest", "preview", "--port", String(port)],
|
|
41
|
+
port,
|
|
42
|
+
...(cwd ? { cwd } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** The indentation of a JSON text's first indented line (2 when none). */
|
|
46
|
+
export function jsonIndent(text) {
|
|
47
|
+
const match = /^([ \t]+)\S/m.exec(text);
|
|
48
|
+
if (!match)
|
|
49
|
+
return 2;
|
|
50
|
+
return match[1].startsWith("\t") ? "\t" : match[1].length;
|
|
51
|
+
}
|
|
52
|
+
/** Replace `path` in one step, so a reader never sees half a file. */
|
|
53
|
+
function writeAtomic(path, text) {
|
|
54
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
55
|
+
try {
|
|
56
|
+
writeFileSync(temp, text);
|
|
57
|
+
renameSync(temp, path);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
rmSync(temp, { force: true });
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Add the preview entry to <sessionRoot>/.claude/launch.json, creating the
|
|
66
|
+
* file when there is none. sessionRoot is the folder Claude Code Desktop's
|
|
67
|
+
* session starts in. Never overwrites another entry or setting, and never
|
|
68
|
+
* rewrites a file it cannot parse as plain JSON.
|
|
69
|
+
*/
|
|
70
|
+
export function writeClaudeLaunchConfig(sessionRoot, options = {}) {
|
|
71
|
+
const entry = options.entry ?? previewLaunchEntry();
|
|
72
|
+
const dir = join(sessionRoot, ".claude");
|
|
73
|
+
const path = join(dir, "launch.json");
|
|
74
|
+
if (!existsSync(path)) {
|
|
75
|
+
mkdirSync(dir, { recursive: true });
|
|
76
|
+
const file = { version: LAUNCH_JSON_VERSION, autoVerify: true, configurations: [entry] };
|
|
77
|
+
writeAtomic(path, `${JSON.stringify(file, null, 2)}\n`);
|
|
78
|
+
return { status: "created", path, entry, autoVerify: true };
|
|
79
|
+
}
|
|
80
|
+
const text = readFileSync(path, "utf-8");
|
|
81
|
+
let parsed;
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(text);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Desktop accepts comments; rewriting would drop them.
|
|
87
|
+
const name = entry.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
88
|
+
if (new RegExp(`"name"\\s*:\\s*"${name}"`).test(text)) {
|
|
89
|
+
return { status: "kept", path, entry, autoVerify: !/"autoVerify"\s*:\s*false/.test(text) };
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
status: "skipped",
|
|
93
|
+
path,
|
|
94
|
+
entry,
|
|
95
|
+
reason: "it has comments or is not plain JSON, so it was not rewritten",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const file = asRecord(parsed);
|
|
99
|
+
if (!file || (file.configurations !== undefined && !Array.isArray(file.configurations))) {
|
|
100
|
+
return { status: "skipped", path, entry, reason: "it is not an object with a configurations list" };
|
|
101
|
+
}
|
|
102
|
+
const configurations = file.configurations ?? [];
|
|
103
|
+
const autoVerifySet = typeof file.autoVerify === "boolean";
|
|
104
|
+
if (configurations.some((item) => asRecord(item)?.name === entry.name)) {
|
|
105
|
+
return { status: "kept", path, entry, autoVerify: file.autoVerify !== false };
|
|
106
|
+
}
|
|
107
|
+
// A person's explicit autoVerify (on or off) is theirs; only fill it in.
|
|
108
|
+
const next = { ...file, configurations: [...configurations, entry] };
|
|
109
|
+
if (!autoVerifySet)
|
|
110
|
+
next.autoVerify = true;
|
|
111
|
+
writeAtomic(path, `${JSON.stringify(next, null, jsonIndent(text))}${text.endsWith("\n") ? "\n" : ""}`);
|
|
112
|
+
return { status: "added", path, entry, autoVerify: next.autoVerify !== false };
|
|
113
|
+
}
|
|
114
|
+
/** Where `summer preview` serves the running game (Claude Code Desktop opens localhost:<port>). */
|
|
115
|
+
export const PREVIEW_URL = `http://localhost:${DEFAULT_PREVIEW_PORT}`;
|
|
116
|
+
/**
|
|
117
|
+
* Register `summer preview` in <session folder>/.claude/launch.json, so
|
|
118
|
+
* Claude Code Desktop's Browser pane shows the game as soon as the session
|
|
119
|
+
* opens (core/preview/launch-config.ts: it only adds). Null when the session
|
|
120
|
+
* folder is the home folder or a filesystem root (~/.claude is Claude Code's
|
|
121
|
+
* own config), or the project is neither at, above nor below it.
|
|
122
|
+
*/
|
|
123
|
+
export function registerPreview(sessionFolder, projectDir) {
|
|
124
|
+
const real = (path) => (existsSync(path) ? realpathSync(path) : resolve(path));
|
|
125
|
+
const session = real(sessionFolder);
|
|
126
|
+
const project = real(projectDir);
|
|
127
|
+
if (isHomeOrRoot(session))
|
|
128
|
+
return null;
|
|
129
|
+
const below = relative(session, project);
|
|
130
|
+
const isBelow = !below.startsWith("..") && !isAbsolute(below);
|
|
131
|
+
const isAbove = !isBelow && !relative(project, session).startsWith("..") && !isAbsolute(relative(project, session));
|
|
132
|
+
if (!isBelow && !isAbove)
|
|
133
|
+
return null;
|
|
134
|
+
// Below: the preview starts in the project; above: it finds the project from the session folder.
|
|
135
|
+
const cwd = isBelow && below ? below.split(sep).join("/") : undefined;
|
|
136
|
+
return writeClaudeLaunchConfig(session, { entry: previewLaunchEntry(DEFAULT_PREVIEW_PORT, cwd) });
|
|
137
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one page `summer preview` serves. Self-contained: inline style and
|
|
3
|
+
* script, no fonts, no CDN, no request leaves 127.0.0.1 (the server's
|
|
4
|
+
* Content-Security-Policy enforces it), and no token anywhere (the server
|
|
5
|
+
* holds the engine connection).
|
|
6
|
+
*
|
|
7
|
+
* Loop: fetch /api/frame, show it, wait the interval the server derived from
|
|
8
|
+
* the measured capture time, repeat. Hidden tab = no requests. Each frame
|
|
9
|
+
* response carries the status (x-summer-status) so one request a frame is all
|
|
10
|
+
* the page makes. The first frame after the page loads or comes back into
|
|
11
|
+
* view, and the Refresh button, ask for a fresh one (?fresh=1): game frames
|
|
12
|
+
* are otherwise about 20 s apart, and the header says how old the frame is.
|
|
13
|
+
* While the game runs, keys and clicks on the frame go to it; a key release is
|
|
14
|
+
* always sent, so no key stays held in the game. A page framed inside another
|
|
15
|
+
* page sends no input.
|
|
16
|
+
*
|
|
17
|
+
* For agents checking the page: <body data-state> is connecting, no_editor,
|
|
18
|
+
* editor or game; #status holds the status line; #refresh fetches a fresh
|
|
19
|
+
* frame; the page logs nothing to the console in normal operation, so a
|
|
20
|
+
* console error is a real one.
|
|
21
|
+
*/
|
|
22
|
+
export declare const PREVIEW_PAGE_HTML = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Summer preview</title>\n<style>\n :root { --cobalt: #0047ab; --ink: #111; --muted: #6b6b6b; --line: #e6e6e6; }\n * { box-sizing: border-box; }\n html, body { height: 100%; margin: 0; }\n body { display: flex; flex-direction: column; background: #fff; color: var(--ink);\n font: 13px/1.4 -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif; }\n header, footer { display: flex; align-items: center; gap: 12px; padding: 8px 14px; min-height: 36px; }\n header { border-bottom: 1px solid var(--line); }\n footer { border-top: 1px solid var(--line); color: var(--muted); }\n .brand { color: var(--cobalt); font-weight: 700; }\n #project { font-weight: 600; }\n #scene { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .spacer { flex: 1; }\n #source { color: #fff; background: var(--cobalt); border-radius: 4px; padding: 1px 8px; font-weight: 600; }\n #source:empty { display: none; }\n #rate { color: var(--muted); font-variant-numeric: tabular-nums; }\n #refresh { font: inherit; color: var(--cobalt); background: #fff; border: 1px solid var(--line); border-radius: 4px; padding: 1px 8px; cursor: pointer; }\n body[data-state=\"no_editor\"] #refresh, body[data-state=\"connecting\"] #refresh { display: none; }\n main { flex: 1; min-height: 0; position: relative; display: flex; align-items: center; justify-content: center; outline: none; }\n #frame { width: 100%; height: 100%; object-fit: contain; display: block; user-select: none; -webkit-user-drag: none; }\n body[data-state=\"game\"] #frame { cursor: crosshair; }\n #empty { display: none; text-align: center; max-width: 520px; padding: 24px; }\n #empty h1 { font-size: 20px; margin: 0 0 8px; }\n #empty code { display: inline-block; margin-top: 8px; padding: 6px 10px; border: 1px solid var(--line); border-radius: 4px; color: var(--cobalt); }\n body[data-state=\"no_editor\"] #frame, body[data-state=\"connecting\"] #frame { display: none; }\n body[data-state=\"no_editor\"] #empty, body[data-state=\"connecting\"] #empty { display: block; }\n #status { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n</style>\n</head>\n<body data-state=\"connecting\">\n<header>\n <span class=\"brand\">Summer</span>\n <span id=\"project\"></span>\n <span id=\"scene\"></span>\n <span class=\"spacer\"></span>\n <span id=\"source\"></span>\n <span id=\"rate\"></span>\n <button id=\"refresh\" type=\"button\" title=\"Capture a fresh frame now\">Refresh</button>\n</header>\n<main id=\"stage\" tabindex=\"0\">\n <img id=\"frame\" alt=\"Live frame from Summer Engine\">\n <div id=\"empty\">\n <h1 id=\"empty-title\">Looking for Summer Engine</h1>\n <div id=\"empty-text\"></div>\n </div>\n</main>\n<footer><span id=\"status\">Looking for Summer Engine</span></footer>\n<script>\n(() => {\n const $ = (id) => document.getElementById(id);\n const frame = $(\"frame\");\n const stage = $(\"stage\");\n const framed = window.top !== window.self;\n let status = null;\n let timer = null;\n let loop = 0;\n let lastUrl = null;\n let note = null;\n const shown = [];\n\n function render() {\n const s = status;\n if (!s) return;\n const message = typeof s.message === \"string\" ? s.message : \"\";\n document.body.dataset.state = s.state;\n document.body.dataset.source = s.frame ? s.frame.source : \"\";\n $(\"project\").textContent = s.project || \"\";\n $(\"scene\").textContent = s.scene || \"\";\n $(\"source\").textContent = s.state === \"game\" ? \"Game\" : s.state === \"editor\" ? \"Editor\" : \"\";\n const now = Date.now();\n while (shown.length > 2 && now - shown[0] > 12000) shown.shift();\n const fps = shown.length > 1 ? (shown.length - 1) / ((shown[shown.length - 1] - shown[0]) / 1000) : 0;\n const rate = [];\n if (s.frame && s.frame.source === \"game\") rate.push(\"frame \" + Math.max(0, Math.round((now - s.frame.capturedAt) / 1000)) + \" s old\");\n else if (s.frame && fps) rate.push(fps.toFixed(1) + \" fps\");\n if (s.frame && s.latencyMs != null) rate.push(s.latencyMs + \" ms per frame\");\n $(\"rate\").textContent = rate.join(\" \u00B7 \");\n $(\"status\").textContent = note && now < note.until ? note.text : message;\n const parts = message.split(\": run \");\n $(\"empty-title\").textContent = s.state === \"connecting\" ? \"Looking for Summer Engine\" : parts[0];\n const empty = $(\"empty-text\");\n empty.textContent = \"\";\n if (parts[1]) {\n const code = document.createElement(\"code\");\n code.textContent = parts[1];\n empty.append(\"Run this in the project folder:\", document.createElement(\"br\"), code);\n }\n document.title = (s.project ? s.project + \" \u00B7 \" : \"\") + \"Summer preview\";\n }\n\n async function show(blob) {\n const url = URL.createObjectURL(blob);\n const next = new Image();\n next.src = url;\n try { await next.decode(); } catch (_) {}\n frame.src = url;\n if (lastUrl) URL.revokeObjectURL(lastUrl);\n lastUrl = url;\n shown.push(Date.now());\n }\n\n function readStatus(value) {\n return value && typeof value === \"object\" && typeof value.state === \"string\" ? value : null;\n }\n\n async function tick(id, fresh) {\n if (id !== loop || document.hidden) return;\n const started = performance.now();\n let wait = 2000;\n try {\n const res = await fetch(fresh ? \"/api/frame?fresh=1\" : \"/api/frame\", { cache: \"no-store\" });\n const header = res.headers.get(\"x-summer-status\");\n let next = null;\n if (res.status === 200) {\n if (header) next = readStatus(JSON.parse(decodeURIComponent(header)));\n await show(await res.blob());\n } else {\n next = readStatus(await res.json().catch(() => null));\n }\n if (next) {\n status = next;\n wait = status.intervalMs || 2000;\n }\n } catch (_) {\n status = { state: \"no_editor\", message: \"The preview server stopped: run npx -y summer-engine@latest preview\", frame: null };\n }\n try { render(); } catch (_) {}\n if (id !== loop || document.hidden) return;\n timer = setTimeout(() => tick(id, false), Math.max(0, wait - (performance.now() - started)));\n }\n\n function start() {\n clearTimeout(timer);\n loop += 1;\n if (!document.hidden) tick(loop, true);\n }\n document.addEventListener(\"visibilitychange\", start);\n $(\"refresh\").addEventListener(\"click\", start);\n setInterval(() => { if (!document.hidden) { try { render(); } catch (_) {} } }, 1000);\n\n function framePoint(event) {\n const rect = frame.getBoundingClientRect();\n const w = frame.naturalWidth, h = frame.naturalHeight;\n if (!w || !h) return null;\n const scale = Math.min(rect.width / w, rect.height / h);\n const x = (event.clientX - rect.left - (rect.width - w * scale) / 2) / scale;\n const y = (event.clientY - rect.top - (rect.height - h * scale) / 2) / scale;\n return x < 0 || y < 0 || x > w || y > h ? null : { x: x, y: y };\n }\n\n function inputOn() {\n return !framed && status && status.input;\n }\n\n // Releases go out even while input is off, so a key never stays held.\n async function send(inputs, release) {\n if (framed || (!release && !inputOn())) return;\n try {\n const res = await fetch(\"/api/input\", { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body: JSON.stringify({ inputs: inputs }) });\n const body = await res.json();\n note = body.ok || release ? null : { text: \"Input not sent: \" + body.error, until: Date.now() + 4000 };\n } catch (_) {\n note = { text: \"Input not sent: the preview server did not answer\", until: Date.now() + 4000 };\n }\n try { render(); } catch (_) {}\n }\n\n frame.addEventListener(\"click\", (event) => {\n const point = framePoint(event);\n stage.focus();\n if (point && status && status.state === \"game\") send([{ kind: \"click\", x: point.x, y: point.y, button: 1 }], false);\n });\n frame.addEventListener(\"contextmenu\", (event) => {\n if (!inputOn() || status.state !== \"game\") return;\n event.preventDefault();\n const point = framePoint(event);\n if (point) send([{ kind: \"click\", x: point.x, y: point.y, button: 2 }], false);\n });\n const held = new Set();\n function onKey(event, pressed) {\n if (pressed) {\n if (!inputOn() || event.metaKey) return;\n event.preventDefault();\n if (held.has(event.code)) return;\n held.add(event.code);\n send([{ kind: \"key\", code: event.code, pressed: true }], false);\n } else if (held.delete(event.code)) {\n event.preventDefault();\n send([{ kind: \"key\", code: event.code, pressed: false }], true);\n }\n }\n stage.addEventListener(\"keydown\", (event) => onKey(event, true));\n stage.addEventListener(\"keyup\", (event) => onKey(event, false));\n stage.addEventListener(\"blur\", () => {\n const codes = Array.from(held);\n held.clear();\n if (codes.length) send(codes.map((code) => ({ kind: \"key\", code: code, pressed: false })), true);\n });\n\n start();\n})();\n</script>\n</body>\n</html>\n";
|