summer-engine 4.3.0 → 4.6.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 +100 -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/commands/setup.js +175 -32
- 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/installer/agent-config.d.ts +29 -0
- package/dist/installer/agent-config.js +75 -11
- package/dist/installer/agent-detect.d.ts +44 -0
- package/dist/installer/agent-detect.js +166 -0
- package/dist/installer/agent-table.d.ts +19 -0
- package/dist/installer/agent-table.js +130 -0
- package/dist/installer/setup-error.d.ts +19 -0
- package/dist/installer/setup-error.js +34 -0
- package/dist/installer/version-check.d.ts +5 -0
- package/dist/installer/version-check.js +21 -0
- package/dist/mcp/ops-surface.d.ts +7 -0
- package/dist/mcp/ops-surface.js +40 -3
- 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/dist/project-memory/agent-instructions.js +11 -3
- 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 +6 -4
- 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
|
@@ -20,11 +20,13 @@ import { tmpdir } from "node:os";
|
|
|
20
20
|
import { join } from "node:path";
|
|
21
21
|
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
22
22
|
import { EngineApiClient } from "../api-client.js";
|
|
23
|
-
import { CLI_RECOVERY, codedFailure, codeForStatus, describeNetworkError, engineNotRunningFromConnectError, networkDetails, NOT_SIGNED_IN, } from "../error-codes.js";
|
|
23
|
+
import { CLI_RECOVERY, codedFailure, codeForStatus, describeNetworkError, EngineNotRunningError, engineNotRunningFromConnectError, networkDetails, NOT_SIGNED_IN, } from "../error-codes.js";
|
|
24
|
+
import { engineSelectionFromEnv } from "../engine.js";
|
|
24
25
|
import { missingEngineEventsResult, missingEngineOpResult, } from "../capability-skew.js";
|
|
25
26
|
import { buildAgentPlaybook } from "./agent-playbook.js";
|
|
26
27
|
import { recentEvents, recentEventsArgsSchema, waitForEvent, waitForEventArgsSchema, } from "./events.js";
|
|
27
28
|
import { importResolvedAsset } from "./asset-import.js";
|
|
29
|
+
import { importUrls } from "./video-ogv.js";
|
|
28
30
|
import { executeSceneMutation, occurrenceCount, readTextPayload, safeProjectPath, validSha256, } from "./engine-ops.js";
|
|
29
31
|
import { extractOpError, withOldEngineHint } from "./engine-receipt.js";
|
|
30
32
|
import { lookupApiDocs } from "./api-docs.js";
|
|
@@ -52,7 +54,13 @@ import { shapeEngineLogResponse } from "../log-filters.js";
|
|
|
52
54
|
import { exportGame } from "./export-game.js";
|
|
53
55
|
import { exportTemplates } from "./export-templates.js";
|
|
54
56
|
import { captureGameplay } from "./capture-gameplay.js";
|
|
57
|
+
import { goLive } from "./go-live.js";
|
|
55
58
|
import { publishBuild } from "./publish-build.js";
|
|
59
|
+
import { DETAIL_SECTIONS, setStoreDetails } from "./store-details.js";
|
|
60
|
+
import { syncItems } from "./sync-items.js";
|
|
61
|
+
import { contentMake, contentMakeInput } from "./content-make/index.js";
|
|
62
|
+
import { createLocalProject, LocalProjectError, localProjectFailure, openLocalProject } from "./projects-local.js";
|
|
63
|
+
import { BuildToolError, buildToolFailure } from "./summer-bundle.js";
|
|
56
64
|
import { resolveGatewayUrl } from "../config.js";
|
|
57
65
|
import { createDebugReportArtifact } from "./debug-report.js";
|
|
58
66
|
import { buildGameTaskPlan, gameTaskPlanInputSchema } from "./game-task-plan.js";
|
|
@@ -105,6 +113,32 @@ export class CodedToolError extends ToolResultError {
|
|
|
105
113
|
super(codedFailure(code, message, extra, recovery), message);
|
|
106
114
|
}
|
|
107
115
|
}
|
|
116
|
+
/** A creator tool's BuildToolError as a ToolResultError, so `summer tool` and
|
|
117
|
+
* `summer <area> <action>` print its whole structured failure (code,
|
|
118
|
+
* recovery and detail such as a manifest's problem list), as the MCP face does. */
|
|
119
|
+
async function structuredBuildFailure(run) {
|
|
120
|
+
try {
|
|
121
|
+
return await run();
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error instanceof BuildToolError)
|
|
125
|
+
throw new ToolResultError(buildToolFailure(error), error.message);
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** projects.create_local / open_local: their coded failures, printed whole. */
|
|
130
|
+
async function localProjectStep(run) {
|
|
131
|
+
try {
|
|
132
|
+
return await run();
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (error instanceof LocalProjectError)
|
|
136
|
+
throw new ToolResultError(localProjectFailure(error), error.message);
|
|
137
|
+
if (error instanceof EngineNotRunningError)
|
|
138
|
+
throw new CodedToolError("engine_not_running", error.message, {}, error.recovery);
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
108
142
|
/** The local engine is not reachable — a clean state, not a crash. Prints
|
|
109
143
|
* `{ok:false, code:"engine_not_running", error, recovery, hint}`. */
|
|
110
144
|
export class EngineUnavailableError extends CodedToolError {
|
|
@@ -715,6 +749,34 @@ export const TOOL_DISPATCH = [
|
|
|
715
749
|
...(typeof args.waitSeconds === "number" ? { waitSeconds: args.waitSeconds } : {}),
|
|
716
750
|
face: "cli",
|
|
717
751
|
})),
|
|
752
|
+
entry("summer_go_live", "Take your store game live: release its Build and publish its page, then watch until players get it (confirm-gated)", false, (args) => structuredBuildFailure(() => goLive({
|
|
753
|
+
gameId: optStr(args, "gameId"),
|
|
754
|
+
publicationId: optStr(args, "publicationId"),
|
|
755
|
+
confirm: args.confirm === true,
|
|
756
|
+
...(typeof args.waitSeconds === "number" ? { waitSeconds: args.waitSeconds } : {}),
|
|
757
|
+
face: "cli",
|
|
758
|
+
}))),
|
|
759
|
+
entry("summer_store_set_details", "Fill store page sections: genres, features, credits, languages, requirements, content survey, AI disclosure", false, (args) => structuredBuildFailure(() => setStoreDetails({
|
|
760
|
+
gameId: optStr(args, "gameId"),
|
|
761
|
+
...Object.fromEntries(DETAIL_SECTIONS.filter((section) => args[section] !== undefined).map((section) => [section, args[section]])),
|
|
762
|
+
face: "cli",
|
|
763
|
+
}))),
|
|
764
|
+
entry("summer_sync_items", "Sync summer.items.json to your store game by slug; the owner approves (confirm-gated)", false, (args) =>
|
|
765
|
+
// The same structured failure the MCP face gives (a manifest's whole problem list), printed whole.
|
|
766
|
+
structuredBuildFailure(() => syncItems({
|
|
767
|
+
gameId: optStr(args, "gameId"),
|
|
768
|
+
project: optStr(args, "project"),
|
|
769
|
+
file: optStr(args, "file"),
|
|
770
|
+
...(Array.isArray(args.slugs) ? { slugs: args.slugs.filter((slug) => typeof slug === "string") } : {}),
|
|
771
|
+
confirm: args.confirm === true,
|
|
772
|
+
face: "cli",
|
|
773
|
+
}))),
|
|
774
|
+
entry("summer_projects_create_local", "Create a Summer project on this computer from a template (3d-basic by default)", false, (args) => localProjectStep(() => createLocalProject({ template: optStr(args, "template"), name: optStr(args, "name"), dir: optStr(args, "dir") }))),
|
|
775
|
+
entry("summer_projects_open_local", "Open a project in Summer Engine and wait until it connects; install:true installs a missing engine (only after the person's yes)", false, (args) => localProjectStep(() => openLocalProject({ path: optStr(args, "path"), install: args.install === true }, engineSelectionFromEnv() ?? { cwd: process.cwd() },
|
|
776
|
+
// A shell command waits for the whole install and first open, like `summer dev`
|
|
777
|
+
// (the MCP answers "call again" instead, to stay within a tool call).
|
|
778
|
+
{ waitMs: args.install === true ? 60 * 60_000 : 180_000 }))),
|
|
779
|
+
entry("summer_content_make", "Make one finished piece of content for a game, beat for beat on a reference, from local footage, into the team's Review", false, (args) => structuredBuildFailure(() => contentMake({ ...contentMakeInput(args), face: "cli" }))),
|
|
718
780
|
// --- debug ---
|
|
719
781
|
entry("summer_get_diagnostics", "Overview of editor console and runtime debugger errors/warnings", true, async (args, ctx) => requireEngineSuccess(await (await ctx.engine()).getDiagnostics())),
|
|
720
782
|
entry("summer_get_console", "Read recent editor Output panel messages (deduplicated)", true, async (args, ctx) => {
|
|
@@ -1130,16 +1192,14 @@ export const TOOL_DISPATCH = [
|
|
|
1130
1192
|
return requireEngineSuccess(await client.getSceneState(target, { depth, limit }));
|
|
1131
1193
|
}),
|
|
1132
1194
|
entry("summer_import_from_url", "Download one file by URL through Godot's import pipeline", true, async (args, ctx) => {
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
op.path = args.path;
|
|
1136
|
-
return requireEngineSuccess(await (await ctx.engine()).executeOps([op]));
|
|
1195
|
+
const path = optStr(args, "path");
|
|
1196
|
+
return requireEngineSuccess(await importUrls(await ctx.engine(), [{ url: str(args, "url"), ...(path ? { path } : {}) }], { batch: false }));
|
|
1137
1197
|
}),
|
|
1138
1198
|
entry("summer_import_from_url_batch", "Download multiple files by URL in one operation", true, async (args, ctx) => {
|
|
1139
1199
|
if (!Array.isArray(args.imports)) {
|
|
1140
1200
|
throw new ToolDispatchError("imports must be an array of {url, path} objects");
|
|
1141
1201
|
}
|
|
1142
|
-
return requireEngineSuccess(await (await ctx.engine()
|
|
1202
|
+
return requireEngineSuccess(await importUrls(await ctx.engine(), args.imports, { batch: true }));
|
|
1143
1203
|
}),
|
|
1144
1204
|
// --- scene ---
|
|
1145
1205
|
entry("summer_create_scene", "Create a new minimal .tscn with a create-only guard", true, async (args, ctx) => {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Cloudinary's rendition: at most 1920x1080, Theora + Vorbis, same frame rate. */
|
|
2
|
+
export declare const OGV_TRANSFORM = "c_limit,h_1080,w_1920";
|
|
3
|
+
export type ImportEntry = {
|
|
4
|
+
url: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
};
|
|
7
|
+
export type VideoConversion = {
|
|
8
|
+
from: string;
|
|
9
|
+
to: string;
|
|
10
|
+
via: "cloudinary" | "ffmpeg" | "summerengine.com";
|
|
11
|
+
};
|
|
12
|
+
/** The part of the engine client this needs. */
|
|
13
|
+
export interface VideoImportClient {
|
|
14
|
+
executeOps(ops: Record<string, unknown>[]): Promise<unknown>;
|
|
15
|
+
getProjectRoot?(): string | undefined;
|
|
16
|
+
}
|
|
17
|
+
/** What the conversion runs; tests replace it. */
|
|
18
|
+
export interface VideoOgvDeps {
|
|
19
|
+
/** The ffmpeg audio encoder for Vorbis when this ffmpeg has Theora, else null. */
|
|
20
|
+
ffmpegTheora(): Promise<{
|
|
21
|
+
audio: "libvorbis" | "vorbis" | null;
|
|
22
|
+
} | null>;
|
|
23
|
+
/** Run ffmpeg with these arguments. */
|
|
24
|
+
runFfmpeg(args: string[]): Promise<void>;
|
|
25
|
+
/** Download a public https file into a hidden file in `dir`; returns its path. */
|
|
26
|
+
download(url: string, dir: string): Promise<string>;
|
|
27
|
+
/** Ask summerengine.com for an .ogv of `url`; returns its URL. */
|
|
28
|
+
convertRemote(url: string): Promise<string>;
|
|
29
|
+
}
|
|
30
|
+
/** True when the entry is a video the engine cannot load (by the URL's file name or the requested path). */
|
|
31
|
+
export declare function isConvertibleVideo(entry: ImportEntry): boolean;
|
|
32
|
+
/** The res:// path the .ogv lands at: the requested path with .ogv, else res://assets/videos/<name>.ogv (the engine's own inference). */
|
|
33
|
+
export declare function ogvResPath(entry: ImportEntry): string;
|
|
34
|
+
/** The .ogv rendition of a Cloudinary video URL, or null when the URL is not one (or is signed). */
|
|
35
|
+
export declare function cloudinaryOgvUrl(raw: string): string | null;
|
|
36
|
+
/** ffmpeg arguments: Theora q8 video, Vorbis q5 audio when there is audio, same frame rate, at most 1920x1080. */
|
|
37
|
+
export declare function ffmpegOgvArgs(input: string, output: string, audio: "libvorbis" | "vorbis" | null): string[];
|
|
38
|
+
export declare class VideoConvertError extends Error {
|
|
39
|
+
readonly reason: "not_signed_in" | "remote_failed" | "local_failed";
|
|
40
|
+
constructor(reason: "not_signed_in" | "remote_failed" | "local_failed", detail?: string);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Import URLs into the project the way ImportFromUrl / ImportFromUrlBatch do,
|
|
44
|
+
* converting every video to .ogv first. Without a video the engine gets
|
|
45
|
+
* exactly the op it got before. `batch` picks ImportFromUrlBatch.
|
|
46
|
+
*/
|
|
47
|
+
export declare function importUrls(client: VideoImportClient, entries: ImportEntry[], options: {
|
|
48
|
+
batch: boolean;
|
|
49
|
+
}, deps?: VideoOgvDeps): Promise<unknown>;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video imports for Summer Engine. The engine plays video only as Ogg Theora
|
|
3
|
+
* (.ogv): an .mp4, .mov or .webm import never becomes a loadable resource and
|
|
4
|
+
* the engine rolls it back. Every import path (summer_import_from_url, its
|
|
5
|
+
* batch, summer_import_asset_by_id, summer_import_asset) runs its entries
|
|
6
|
+
* through importUrls, which converts a video first, so a video just works:
|
|
7
|
+
*
|
|
8
|
+
* 1. A Cloudinary video URL (every video Summer generates or stores) becomes
|
|
9
|
+
* its .ogv rendition URL: Cloudinary converts on delivery and the engine
|
|
10
|
+
* downloads that. Nothing runs on this computer.
|
|
11
|
+
* 2. Else, when this computer has an ffmpeg with Theora and the project folder
|
|
12
|
+
* is known: download, convert (Theora video q8, Vorbis audio q5, same frame
|
|
13
|
+
* rate, at most 1920x1080), write the .ogv into the project and register it
|
|
14
|
+
* with the editor.
|
|
15
|
+
* 3. Else summerengine.com converts it (needs the Summer sign-in) and the
|
|
16
|
+
* engine downloads the .ogv it answers.
|
|
17
|
+
*
|
|
18
|
+
* The file lands at the requested path with its extension swapped to .ogv,
|
|
19
|
+
* and the answer says so (videoConversions, note).
|
|
20
|
+
*/
|
|
21
|
+
import { execFile } from "node:child_process";
|
|
22
|
+
import { mkdir, rename, stat, unlink } from "node:fs/promises";
|
|
23
|
+
import { dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
24
|
+
import { randomBytes } from "node:crypto";
|
|
25
|
+
import { getAuthToken } from "../auth.js";
|
|
26
|
+
import { resolveGatewayUrl } from "../config.js";
|
|
27
|
+
import { summerRequestHeaders } from "../agent-host.js";
|
|
28
|
+
/** Containers Summer Engine cannot load; .ogv is the one it can. */
|
|
29
|
+
const VIDEO_EXTENSION = /\.(?:mp4|mov|webm|m4v|mkv|avi)$/i;
|
|
30
|
+
/** Cloudinary's rendition: at most 1920x1080, Theora + Vorbis, same frame rate. */
|
|
31
|
+
export const OGV_TRANSFORM = "c_limit,h_1080,w_1920";
|
|
32
|
+
const VIDEO_UPLOAD = "/video/upload/";
|
|
33
|
+
function pathOf(raw) {
|
|
34
|
+
try {
|
|
35
|
+
return new URL(raw).pathname;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return raw.split(/[?#]/)[0] ?? raw;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** True when the entry is a video the engine cannot load (by the URL's file name or the requested path). */
|
|
42
|
+
export function isConvertibleVideo(entry) {
|
|
43
|
+
return VIDEO_EXTENSION.test(pathOf(entry.url)) || (entry.path !== undefined && VIDEO_EXTENSION.test(entry.path));
|
|
44
|
+
}
|
|
45
|
+
/** The res:// path the .ogv lands at: the requested path with .ogv, else res://assets/videos/<name>.ogv (the engine's own inference). */
|
|
46
|
+
export function ogvResPath(entry) {
|
|
47
|
+
if (entry.path) {
|
|
48
|
+
const swapped = entry.path.replace(/\.[A-Za-z0-9]+$/, ".ogv");
|
|
49
|
+
return swapped.toLowerCase().endsWith(".ogv") ? swapped : `${entry.path}.ogv`;
|
|
50
|
+
}
|
|
51
|
+
const file = decodeURIComponent(pathOf(entry.url).split("/").pop() || "video");
|
|
52
|
+
const stem = file.replace(/\.[A-Za-z0-9]+$/, "").replace(/[^\p{L}\p{N}_.-]+/gu, "_") || "video";
|
|
53
|
+
return `res://assets/videos/${stem}.ogv`;
|
|
54
|
+
}
|
|
55
|
+
/** The .ogv rendition of a Cloudinary video URL, or null when the URL is not one (or is signed). */
|
|
56
|
+
export function cloudinaryOgvUrl(raw) {
|
|
57
|
+
let url;
|
|
58
|
+
try {
|
|
59
|
+
url = new URL(raw);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (url.protocol !== "https:" || url.hostname !== "res.cloudinary.com" || url.username || url.password || url.port)
|
|
65
|
+
return null;
|
|
66
|
+
const at = url.pathname.indexOf(VIDEO_UPLOAD);
|
|
67
|
+
if (at < 0)
|
|
68
|
+
return null;
|
|
69
|
+
const rest = url.pathname.slice(at + VIDEO_UPLOAD.length);
|
|
70
|
+
// A signed delivery URL stops working when its transformation changes.
|
|
71
|
+
if (/^s--[^/]+--\//.test(rest))
|
|
72
|
+
return null;
|
|
73
|
+
if (!VIDEO_EXTENSION.test(rest) && !rest.toLowerCase().endsWith(".ogv"))
|
|
74
|
+
return null;
|
|
75
|
+
const stem = rest.replace(/\.[A-Za-z0-9]+$/, "");
|
|
76
|
+
if (!stem)
|
|
77
|
+
return null;
|
|
78
|
+
const transformed = stem.startsWith(`${OGV_TRANSFORM}/`) ? stem : `${OGV_TRANSFORM}/${stem}`;
|
|
79
|
+
return `${url.origin}${url.pathname.slice(0, at)}${VIDEO_UPLOAD}${transformed}.ogv`;
|
|
80
|
+
}
|
|
81
|
+
/** ffmpeg arguments: Theora q8 video, Vorbis q5 audio when there is audio, same frame rate, at most 1920x1080. */
|
|
82
|
+
export function ffmpegOgvArgs(input, output, audio) {
|
|
83
|
+
const audioArgs = audio === "libvorbis"
|
|
84
|
+
? ["-map", "0:a:0?", "-c:a", "libvorbis", "-q:a", "5"]
|
|
85
|
+
: audio === "vorbis"
|
|
86
|
+
? ["-map", "0:a:0?", "-c:a", "vorbis", "-strict", "-2", "-ac", "2", "-q:a", "5"]
|
|
87
|
+
: ["-an"];
|
|
88
|
+
return [
|
|
89
|
+
"-v", "error", "-y", "-i", input,
|
|
90
|
+
"-map", "0:v:0",
|
|
91
|
+
"-vf", "scale='min(iw,1920)':'min(ih,1080)':force_original_aspect_ratio=decrease:force_divisible_by=2",
|
|
92
|
+
"-c:v", "libtheora", "-q:v", "8",
|
|
93
|
+
...audioArgs,
|
|
94
|
+
"-f", "ogg", output,
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
function exec(file, args, timeoutMs) {
|
|
98
|
+
return new Promise((resolvePromise, reject) => {
|
|
99
|
+
execFile(file, args, { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
100
|
+
if (error)
|
|
101
|
+
reject(new Error(String(stderr || error.message).trim().split("\n").slice(-3).join(" ")));
|
|
102
|
+
else
|
|
103
|
+
resolvePromise(stdout);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
let theoraProbe;
|
|
108
|
+
const defaultDeps = {
|
|
109
|
+
ffmpegTheora() {
|
|
110
|
+
theoraProbe ??= exec("ffmpeg", ["-hide_banner", "-encoders"], 15_000).then((out) => (/\blibtheora\b/.test(out) ? { audio: /\blibvorbis\b/.test(out) ? "libvorbis" : /\svorbis\s/.test(out) ? "vorbis" : null } : null), () => null);
|
|
111
|
+
return theoraProbe;
|
|
112
|
+
},
|
|
113
|
+
async runFfmpeg(args) {
|
|
114
|
+
await exec("ffmpeg", args, 15 * 60_000);
|
|
115
|
+
},
|
|
116
|
+
async download(url, dir) {
|
|
117
|
+
// Loaded here: save-files.ts imports this module too.
|
|
118
|
+
const { fetchPublicToTemp } = await import("../ops/save-files.js");
|
|
119
|
+
return (await fetchPublicToTemp(url, dir)).temp;
|
|
120
|
+
},
|
|
121
|
+
async convertRemote(url) {
|
|
122
|
+
const token = await getAuthToken();
|
|
123
|
+
if (!token)
|
|
124
|
+
throw new VideoConvertError("not_signed_in");
|
|
125
|
+
const gateway = await resolveGatewayUrl();
|
|
126
|
+
const res = await fetch(`${gateway}/api/mcp/video/ogv`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: { ...summerRequestHeaders({ surface: "mcp" }), Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
129
|
+
body: JSON.stringify({ url }),
|
|
130
|
+
signal: AbortSignal.timeout(150_000),
|
|
131
|
+
});
|
|
132
|
+
const body = (await res.json().catch(() => ({})));
|
|
133
|
+
if (!res.ok || typeof body.ogvUrl !== "string") {
|
|
134
|
+
throw new VideoConvertError(res.status === 401 ? "not_signed_in" : "remote_failed", body.message ?? body.error ?? `HTTP ${res.status}`);
|
|
135
|
+
}
|
|
136
|
+
return body.ogvUrl;
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
export class VideoConvertError extends Error {
|
|
140
|
+
reason;
|
|
141
|
+
constructor(reason, detail) {
|
|
142
|
+
super(reason === "not_signed_in"
|
|
143
|
+
? "Summer Engine plays video only as Ogg Theora (.ogv), and this video needs converting. This computer has no ffmpeg with Theora, so summerengine.com converts it, which needs the Summer sign-in: run npx -y summer-engine@latest login, then import again."
|
|
144
|
+
: `Summer Engine plays video only as Ogg Theora (.ogv), and converting this video failed: ${detail ?? "unknown error"}.`);
|
|
145
|
+
this.reason = reason;
|
|
146
|
+
this.name = "VideoConvertError";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function exists(path) {
|
|
150
|
+
try {
|
|
151
|
+
await stat(path);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Convert on this computer and put the .ogv at `resPath` in the project; returns the RenameFile receipt, or null when the file was moved into place directly. */
|
|
159
|
+
async function convertLocally(client, root, url, resPath, audio, deps) {
|
|
160
|
+
const dest = join(root, resPath.slice("res://".length));
|
|
161
|
+
const inside = relative(root, dest);
|
|
162
|
+
if (inside === "" || inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside)) {
|
|
163
|
+
throw new VideoConvertError("local_failed", `${resPath} is outside the project`);
|
|
164
|
+
}
|
|
165
|
+
const dir = dirname(dest);
|
|
166
|
+
await mkdir(dir, { recursive: true });
|
|
167
|
+
// Hidden names: the editor skips dot files, so it never sees a half-written video.
|
|
168
|
+
const staging = join(dir, `.summer-video-${randomBytes(6).toString("hex")}.ogv`);
|
|
169
|
+
const source = await deps.download(url, dir);
|
|
170
|
+
try {
|
|
171
|
+
await deps.runFfmpeg(ffmpegOgvArgs(source, staging, audio));
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
await unlink(staging).catch(() => undefined);
|
|
175
|
+
throw new VideoConvertError("local_failed", error instanceof Error ? error.message : String(error));
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
await unlink(source).catch(() => undefined);
|
|
179
|
+
}
|
|
180
|
+
if (await exists(dest)) {
|
|
181
|
+
// Already known to the editor: replace its bytes in place.
|
|
182
|
+
await rename(staging, dest);
|
|
183
|
+
return { receipt: { ok: true, op: "ImportFromUrl", meta: { path: resPath, replaced: true } } };
|
|
184
|
+
}
|
|
185
|
+
// RenameFile registers the new file with the editor's file system (the one op that does so for a file on disk).
|
|
186
|
+
const stagingRes = `res://${relative(root, staging).split(sep).join("/")}`;
|
|
187
|
+
try {
|
|
188
|
+
const receipt = await client.executeOps([{ op: "RenameFile", from: stagingRes, to: resPath }]);
|
|
189
|
+
const failed = receipt?.status === "error" ||
|
|
190
|
+
(receipt?.results ?? []).some((result) => result?.ok === false);
|
|
191
|
+
if (!failed)
|
|
192
|
+
return { receipt: { ok: true, op: "ImportFromUrl", meta: { path: resPath } } };
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Fall through: the file still goes into place.
|
|
196
|
+
}
|
|
197
|
+
if (await exists(staging))
|
|
198
|
+
await rename(staging, dest);
|
|
199
|
+
return {
|
|
200
|
+
receipt: { ok: true, op: "ImportFromUrl", meta: { path: resPath } },
|
|
201
|
+
note: `${resPath} is on disk; the editor lists it after its next file scan.`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function receiptsOf(result) {
|
|
205
|
+
const results = result?.results;
|
|
206
|
+
return Array.isArray(results) ? results : [];
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Import URLs into the project the way ImportFromUrl / ImportFromUrlBatch do,
|
|
210
|
+
* converting every video to .ogv first. Without a video the engine gets
|
|
211
|
+
* exactly the op it got before. `batch` picks ImportFromUrlBatch.
|
|
212
|
+
*/
|
|
213
|
+
export async function importUrls(client, entries, options, deps = defaultDeps) {
|
|
214
|
+
const engineOp = (list) => options.batch
|
|
215
|
+
? { op: "ImportFromUrlBatch", imports: list }
|
|
216
|
+
: { op: "ImportFromUrl", url: list[0].url, ...(list[0].path ? { path: list[0].path } : {}) };
|
|
217
|
+
if (!entries.some(isConvertibleVideo))
|
|
218
|
+
return client.executeOps([engineOp(entries)]);
|
|
219
|
+
const forEngine = [];
|
|
220
|
+
const localReceipts = [];
|
|
221
|
+
const conversions = [];
|
|
222
|
+
const notes = [];
|
|
223
|
+
for (const entry of entries) {
|
|
224
|
+
if (!isConvertibleVideo(entry)) {
|
|
225
|
+
forEngine.push(entry);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const to = ogvResPath(entry);
|
|
229
|
+
const rendition = cloudinaryOgvUrl(entry.url);
|
|
230
|
+
if (rendition) {
|
|
231
|
+
forEngine.push({ url: rendition, path: to });
|
|
232
|
+
conversions.push({ from: entry.url, to, via: "cloudinary" });
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const root = client.getProjectRoot?.();
|
|
236
|
+
const theora = root ? await deps.ffmpegTheora() : null;
|
|
237
|
+
if (root && theora) {
|
|
238
|
+
const placed = await convertLocally(client, root, entry.url, to, theora.audio, deps);
|
|
239
|
+
localReceipts.push(placed.receipt);
|
|
240
|
+
if (placed.note)
|
|
241
|
+
notes.push(placed.note);
|
|
242
|
+
conversions.push({ from: entry.url, to, via: "ffmpeg" });
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
forEngine.push({ url: await deps.convertRemote(entry.url), path: to });
|
|
246
|
+
conversions.push({ from: entry.url, to, via: "summerengine.com" });
|
|
247
|
+
}
|
|
248
|
+
const engineResult = forEngine.length ? await client.executeOps([engineOp(forEngine)]) : { status: "ok", results: [] };
|
|
249
|
+
const base = engineResult && typeof engineResult === "object" && !Array.isArray(engineResult) ? engineResult : { result: engineResult };
|
|
250
|
+
const paths = conversions.map((conversion) => conversion.to).join(", ");
|
|
251
|
+
return {
|
|
252
|
+
...base,
|
|
253
|
+
results: [...receiptsOf(engineResult), ...localReceipts],
|
|
254
|
+
videoConversions: conversions,
|
|
255
|
+
note: [
|
|
256
|
+
`Summer Engine plays video only as Ogg Theora (.ogv), so the video was converted and imported as ${paths}. Load it from that path.`,
|
|
257
|
+
...notes,
|
|
258
|
+
].join(" "),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
@@ -57,8 +57,8 @@ export const CLI_KNOWN_OP_NEEDS = [
|
|
|
57
57
|
"ProjectSetting", "InputMapAddAction", "InputMapBind",
|
|
58
58
|
// Files (summer_write_file / summer_replace_text / summer_create_scene)
|
|
59
59
|
"WriteFile",
|
|
60
|
-
// Import
|
|
61
|
-
"ImportFromUrl", "ImportFromUrlBatch",
|
|
60
|
+
// Import (RenameFile registers a video converted on this computer: core/capabilities/video-ogv.ts)
|
|
61
|
+
"ImportFromUrl", "ImportFromUrlBatch", "RenameFile",
|
|
62
62
|
// Diagnostics + runtime control
|
|
63
63
|
"GetConsoleOutput", "ClearConsoleOutput", "GetDebuggerErrors", "IsGameRunning",
|
|
64
64
|
// summer_play / summer_stop send these as ops for the instance-aware and
|
|
@@ -2,15 +2,39 @@
|
|
|
2
2
|
* The detached child cli-usage.ts starts when a CLI command exits: it sends
|
|
3
3
|
* that command's one usage line (argv[2], JSON) and exits. It prints nothing.
|
|
4
4
|
*/
|
|
5
|
+
import { getInstallId } from "./feedback/client.js";
|
|
5
6
|
import { postUsage } from "./telemetry.js";
|
|
7
|
+
import { usageLineFields } from "./usage-fields.js";
|
|
8
|
+
/** The fields the parent noted travel in the line; the child adds the template and engine state. */
|
|
9
|
+
function processFieldsFrom(line) {
|
|
10
|
+
const fields = {};
|
|
11
|
+
for (const key of ["projectIdHash", "templateId", "templateTags", "instanceCount"]) {
|
|
12
|
+
if (line[key] !== undefined)
|
|
13
|
+
fields[key] = line[key];
|
|
14
|
+
delete line[key];
|
|
15
|
+
}
|
|
16
|
+
return fields;
|
|
17
|
+
}
|
|
6
18
|
// A hard cap: an abort does not cancel a TLS connect that hangs, so the
|
|
7
19
|
// child never lives past 3 s, whatever the network does.
|
|
8
20
|
setTimeout(() => process.exit(0), 3000).unref();
|
|
9
21
|
void (async () => {
|
|
10
22
|
try {
|
|
11
23
|
const body = JSON.parse(process.argv[2] ?? "");
|
|
12
|
-
if (body && typeof body === "object" && !Array.isArray(body))
|
|
13
|
-
|
|
24
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
25
|
+
const line = body;
|
|
26
|
+
// Only the first-run line goes without an account token.
|
|
27
|
+
if (process.argv[3] === "--anonymous") {
|
|
28
|
+
if (line.event === "cli_first_run")
|
|
29
|
+
await postUsage(line, 1500, { anonymous: true });
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const extra = await usageLineFields(processFieldsFrom(line), typeof line.error === "string" ? line.error : null);
|
|
33
|
+
// A sign-in that worked joins this machine's anonymous first-run line to the account.
|
|
34
|
+
const join = line.tool === "login" && line.ok === true ? { installId: await getInstallId() } : {};
|
|
35
|
+
await postUsage({ ...line, ...extra, ...join }, 1500);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
14
38
|
}
|
|
15
39
|
catch {
|
|
16
40
|
// best-effort: a bad body or an unreachable server sends nothing
|
package/dist/core/cli-usage.d.ts
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* one per command run by a signed-in person, with the tool (`summer_call`
|
|
6
6
|
* plus the operation for `summer <area> <action>`, the summer_* tool for
|
|
7
7
|
* `summer tool`, else the command name), success or the error code, duration
|
|
8
|
-
* and the agent app named in the environment
|
|
9
|
-
*
|
|
8
|
+
* and the agent app named in the environment, plus the closed-shape fields
|
|
9
|
+
* of usage-fields.ts (project id hash, template, game clients started,
|
|
10
|
+
* engine state) and, on a `summer login` that worked, this machine's install
|
|
11
|
+
* id so the anonymous first-run line joins the account. Never arguments,
|
|
12
|
+
* paths or project content. Off with SUMMER_NO_TELEMETRY=1 or DO_NOT_TRACK=1; nothing
|
|
10
13
|
* at all when no account token is on disk or in SUMMER_TOKEN. A command that
|
|
11
14
|
* ran on Summer's server (a cloud operation, a hosted `summer tool`) sends no
|
|
12
15
|
* line: the server records it from the request's `X-Summer-Client-Surface: cli`.
|
package/dist/core/cli-usage.js
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* one per command run by a signed-in person, with the tool (`summer_call`
|
|
6
6
|
* plus the operation for `summer <area> <action>`, the summer_* tool for
|
|
7
7
|
* `summer tool`, else the command name), success or the error code, duration
|
|
8
|
-
* and the agent app named in the environment
|
|
9
|
-
*
|
|
8
|
+
* and the agent app named in the environment, plus the closed-shape fields
|
|
9
|
+
* of usage-fields.ts (project id hash, template, game clients started,
|
|
10
|
+
* engine state) and, on a `summer login` that worked, this machine's install
|
|
11
|
+
* id so the anonymous first-run line joins the account. Never arguments,
|
|
12
|
+
* paths or project content. Off with SUMMER_NO_TELEMETRY=1 or DO_NOT_TRACK=1; nothing
|
|
10
13
|
* at all when no account token is on disk or in SUMMER_TOKEN. A command that
|
|
11
14
|
* ran on Summer's server (a cloud operation, a hosted `summer tool`) sends no
|
|
12
15
|
* line: the server records it from the request's `X-Summer-Client-Surface: cli`.
|
|
@@ -23,6 +26,7 @@ import { isTelemetryDisabled, resolveAgentHost } from "./agent-host.js";
|
|
|
23
26
|
import { hasEnvAuthToken } from "./auth.js";
|
|
24
27
|
import { OP_NAME } from "./ops/contract.js";
|
|
25
28
|
import { getSummerDir } from "./store.js";
|
|
29
|
+
import { processUsageFields } from "./usage-fields.js";
|
|
26
30
|
/** What the server accepts as a tool: `summer_call`, `summer_play`, `create`, `skills_install`. */
|
|
27
31
|
const TOOL = /^[a-z][a-z0-9_]{0,79}$/;
|
|
28
32
|
const ERROR_CODE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
@@ -78,6 +82,8 @@ export function cliUsageBody(started, exitCode, now = Date.now(), env = process.
|
|
|
78
82
|
durationMs: Math.max(0, Math.round(now - started.startedAt)),
|
|
79
83
|
mcpClient: resolveAgentHost(env, undefined).id,
|
|
80
84
|
surfaceVersion: "v4",
|
|
85
|
+
// The child (cli-usage-beacon.ts) checks these again and adds the template and engine state.
|
|
86
|
+
...processUsageFields(),
|
|
81
87
|
};
|
|
82
88
|
}
|
|
83
89
|
/** A token on disk or in the environment; the child reads it, the parent only checks it is there. */
|
|
@@ -119,3 +119,56 @@ export declare function linuxArchiveKind(nameOrUrl: string): "tar.gz" | "zip" |
|
|
|
119
119
|
* name first, then the only file, then anything that looks like an engine
|
|
120
120
|
* build (covers artifacts packed straight from bin/). */
|
|
121
121
|
export declare function findExtractedEngineBinary(files: string[]): string | null;
|
|
122
|
+
/** Why `summer install` failed: the CLI usage line's error code, one per kind
|
|
123
|
+
* of failure, so telemetry says more than `exit_1`. */
|
|
124
|
+
export type InstallFailureReason = "release_info" | "download" | "checksum" | "disk_space" | "mount" | "copy" | "permission" | "app_running" | "needs_confirmation" | "installer" | "unsupported_platform";
|
|
125
|
+
/** An install failure with its reason; the message ends in one next step. */
|
|
126
|
+
export declare class EngineInstallError extends Error {
|
|
127
|
+
readonly reason: InstallFailureReason;
|
|
128
|
+
constructor(reason: InstallFailureReason, message: string);
|
|
129
|
+
}
|
|
130
|
+
/** The command every "try again" next step names. */
|
|
131
|
+
export declare const INSTALL_RETRY = "npx -y summer-engine@latest install";
|
|
132
|
+
/** The useful line of an error: for a failed shell command the last line of
|
|
133
|
+
* its stderr ("cp: ...: Permission denied"), not "Command failed: ...". */
|
|
134
|
+
export declare function errorDetail(err: unknown): string;
|
|
135
|
+
/** A full disk and a refused write have their own reasons wherever they
|
|
136
|
+
* happen; anything else is `fallback`. */
|
|
137
|
+
export declare function installFailureReason(err: unknown, fallback: InstallFailureReason): InstallFailureReason;
|
|
138
|
+
/** Turn an unexpected error into an EngineInstallError: disk_space and
|
|
139
|
+
* permission get their own message, anything else `message(detail)`. */
|
|
140
|
+
export declare function asInstallError(err: unknown, fallback: InstallFailureReason, message: (detail: string) => string): EngineInstallError;
|
|
141
|
+
/** The engine download when the server sends no content-length (the macOS DMG is about 660 MB). */
|
|
142
|
+
export declare const DEFAULT_DOWNLOAD_BYTES: number;
|
|
143
|
+
/** The install folder needs about this many times the download: the unpacked
|
|
144
|
+
* app plus the staged copy that replaces an existing one. */
|
|
145
|
+
export declare const INSTALL_SPACE_FACTOR = 2.5;
|
|
146
|
+
/** Free bytes on the volume holding `path`, and an id for that volume. */
|
|
147
|
+
export type FreeSpace = (path: string) => {
|
|
148
|
+
bytes: number;
|
|
149
|
+
volume: string;
|
|
150
|
+
};
|
|
151
|
+
export declare function formatSize(bytes: number): string;
|
|
152
|
+
/**
|
|
153
|
+
* Refuse before the download fills the disk. Each need is a folder and the
|
|
154
|
+
* bytes it takes; needs on one volume add up (the default macOS temp folder
|
|
155
|
+
* and /Applications share the system volume). A volume whose free space
|
|
156
|
+
* cannot be read is skipped: the install tries, and ENOSPC still maps to
|
|
157
|
+
* disk_space.
|
|
158
|
+
*/
|
|
159
|
+
export declare function checkDiskSpace(needs: Array<{
|
|
160
|
+
path: string;
|
|
161
|
+
bytes: number;
|
|
162
|
+
}>, freeSpace: FreeSpace): void;
|
|
163
|
+
/** Where the macOS engine goes: --path wins; otherwise /Applications/Summer.app,
|
|
164
|
+
* or ~/Applications/Summer.app when this user cannot replace it there (a
|
|
165
|
+
* standard, non-admin account). macEnginePaths looks there too, and the
|
|
166
|
+
* install record puts it first. */
|
|
167
|
+
export declare function chooseMacDestination(input: {
|
|
168
|
+
customPath?: string;
|
|
169
|
+
home: string;
|
|
170
|
+
canReplace: (app: string) => boolean;
|
|
171
|
+
}): {
|
|
172
|
+
destApp: string;
|
|
173
|
+
fallback: boolean;
|
|
174
|
+
};
|