summer-engine 2.6.6 → 2.8.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 +4 -1
- package/.codex-plugin/plugin.json +6 -2
- package/.cursor-plugin/plugin.json +5 -1
- package/.opencode/INSTALL.md +1 -1
- package/.opencode/plugins/summer.js +2 -2
- package/AGENTS.md +11 -7
- package/CHANGELOG.md +40 -0
- package/GEMINI.md +9 -7
- package/README.md +63 -21
- package/_persona/summer/SKILL.md +1 -0
- package/assets/autopilot/README.md +51 -0
- package/assets/autopilot/autopilot.gd +222 -0
- package/assets/autopilot/probe_base.gd +262 -0
- package/assets/autopilot/run.sh +78 -0
- package/commands/gameskill.md +11 -2
- package/commands/summer.md +13 -0
- package/dist/bin/summer.js +12 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +53 -0
- package/dist/commands/create.d.ts +1 -0
- package/dist/commands/create.js +38 -6
- package/dist/commands/debug.d.ts +2 -0
- package/dist/commands/debug.js +44 -0
- package/dist/commands/doctor.js +112 -8
- package/dist/commands/list.js +1 -1
- package/dist/commands/login.d.ts +19 -0
- package/dist/commands/login.js +123 -36
- package/dist/commands/logout.js +3 -16
- package/dist/commands/logs.d.ts +2 -0
- package/dist/commands/logs.js +19 -0
- package/dist/commands/mcp.js +7 -2
- package/dist/commands/open.js +1 -1
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +20 -0
- package/dist/commands/releases.d.ts +2 -0
- package/dist/commands/releases.js +20 -0
- package/dist/commands/run.js +1 -1
- package/dist/commands/skills.js +1 -1
- package/dist/lib/api-client.d.ts +15 -4
- package/dist/lib/api-client.js +138 -13
- package/dist/lib/async-op-lifecycle.d.ts +7 -2
- package/dist/lib/async-op-lifecycle.js +32 -9
- package/dist/lib/auth.d.ts +40 -12
- package/dist/lib/auth.js +173 -37
- package/dist/lib/cloud/engine-bridge.d.ts +17 -4
- package/dist/lib/cloud/engine-bridge.js +32 -6
- package/dist/lib/config.d.ts +20 -0
- package/dist/lib/config.js +142 -0
- package/dist/lib/creator.d.ts +93 -0
- package/dist/lib/creator.js +393 -0
- package/dist/lib/debug-report.d.ts +50 -0
- package/dist/lib/debug-report.js +308 -0
- package/dist/lib/engine.d.ts +35 -2
- package/dist/lib/engine.js +240 -6
- package/dist/lib/game-task-plan.js +4 -4
- package/dist/lib/mcp-boot-notice.d.ts +3 -0
- package/dist/lib/mcp-boot-notice.js +7 -0
- package/dist/lib/mcp-log.d.ts +3 -0
- package/dist/lib/mcp-log.js +72 -0
- package/dist/lib/public-surface-paths.d.ts +13 -0
- package/dist/lib/public-surface-paths.js +42 -0
- package/dist/lib/skills-registry.d.ts +33 -1
- package/dist/lib/skills-registry.js +55 -0
- package/dist/lib/store.d.ts +14 -0
- package/dist/lib/store.js +145 -0
- package/dist/lib/summer-compatibility.d.ts +6 -0
- package/dist/lib/summer-compatibility.js +22 -0
- package/dist/mcp/server.d.ts +10 -3
- package/dist/mcp/server.js +104 -11
- package/dist/mcp/tools/asset-tools.js +33 -12
- package/dist/mcp/tools/cloud-tools.js +15 -15
- package/dist/mcp/tools/creator-tools.d.ts +2 -0
- package/dist/mcp/tools/creator-tools.js +105 -0
- package/dist/mcp/tools/debug-tools.js +44 -1
- package/dist/mcp/tools/file-tools.d.ts +2 -0
- package/dist/mcp/tools/file-tools.js +154 -0
- package/dist/mcp/tools/generate-tools.js +159 -11
- package/dist/mcp/tools/project-tools.js +37 -27
- package/dist/mcp/tools/scene-tools.js +106 -29
- package/dist/mcp/tools/visual-tools.js +4 -3
- package/dist/mcp/tools/with-engine.js +4 -4
- package/hooks/session-start.sh +5 -5
- package/package.json +3 -1
- package/references/collaborative-protocol.md +50 -0
- package/references/gd-style.md +121 -0
- package/references/godot-version.md +51 -0
- package/references/mcp-tools-reference.md +199 -0
- package/references/public-surface-language.md +30 -0
- package/references/summer-folder.md +90 -0
- package/references/template-registry.md +73 -0
- package/skills/2d-assets/character-portrait/SKILL.md +17 -12
- package/skills/2d-assets/concept-art/SKILL.md +10 -9
- package/skills/2d-assets/create-asset-sheet/SKILL.md +17 -15
- package/skills/2d-assets/pixel-art/SKILL.md +36 -20
- package/skills/2d-assets/skybox-panorama/SKILL.md +32 -37
- package/skills/2d-assets/sprite-sheet/SKILL.md +40 -35
- package/skills/2d-assets/tileable-texture/SKILL.md +28 -22
- package/skills/2d-assets/ui-graphics/SKILL.md +46 -32
- package/skills/2d-assets/use-widget-asset/SKILL.md +8 -6
- package/skills/3d-assets/character-model/SKILL.md +34 -19
- package/skills/3d-assets/environment-kit/SKILL.md +11 -5
- package/skills/3d-assets/organic-model/SKILL.md +8 -5
- package/skills/3d-assets/prop-model/SKILL.md +9 -3
- package/skills/3d-assets/vehicle-model/SKILL.md +32 -19
- package/skills/README.md +1 -1
- package/skills/ai-and-npcs/design-npc/SKILL.md +31 -13
- package/skills/animation/animation-tree/SKILL.md +39 -15
- package/skills/animation/facial-and-lipsync/SKILL.md +25 -8
- package/skills/animation/generate-motion/SKILL.md +47 -15
- package/skills/animation/procedural-animation/SKILL.md +31 -13
- package/skills/animation/retarget/SKILL.md +127 -79
- package/skills/audio/adaptive-music/SKILL.md +24 -20
- package/skills/audio/ambient-bed/SKILL.md +22 -16
- package/skills/audio/audio-direction/SKILL.md +15 -10
- package/skills/audio/music-track/SKILL.md +18 -12
- package/skills/audio/sound-effect/SKILL.md +17 -12
- package/skills/audio/voice-line/SKILL.md +42 -42
- package/skills/character-controllers/fps-controller/SKILL.md +26 -16
- package/skills/debugging/debug/SKILL.md +74 -20
- package/skills/deployment/export-and-ship/SKILL.md +105 -38
- package/skills/deployment/remote-deploy/SKILL.md +140 -0
- package/skills/gameplay-mechanics/auto-fire-targeting/SKILL.md +26 -1
- package/skills/gameplay-mechanics/design-mechanic/SKILL.md +23 -15
- package/skills/level-design/design-level/SKILL.md +22 -8
- package/skills/level-design/scene-to-level/SKILL.md +13 -7
- package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +8 -8
- package/skills/multiplayer-and-networking/peer-to-peer-multiplayer/SKILL.md +2 -7
- package/skills/multiplayer-and-networking/setup-multiplayer/SKILL.md +26 -18
- package/skills/performance/tune-performance/SKILL.md +51 -32
- package/skills/rendering-and-lighting/3d-lighting/SKILL.md +34 -5
- package/skills/rendering-and-lighting/art-direction/SKILL.md +6 -8
- package/skills/scene-and-project/brainstorm-game/SKILL.md +7 -9
- package/skills/scene-and-project/make-game/SKILL.md +2 -1
- package/skills/scene-and-project/play/SKILL.md +10 -4
- package/skills/scene-and-project/scene-composition/SKILL.md +21 -8
- package/skills/scene-and-project/summer-cloud/SKILL.md +92 -0
- package/skills/scripting-patterns/gdscript-patterns/SKILL.md +1 -1
- package/skills/ui-and-ux/ui-basics/SKILL.md +16 -7
- package/skills/video/animated-loop/SKILL.md +63 -39
- package/skills/video/cinematic-cutscene/SKILL.md +66 -30
- package/skills/video/trailer-shot/SKILL.md +30 -21
- package/skills/visual-effects/_building-blocks/additive-billboard-particles.md +25 -16
- package/skills/visual-effects/_building-blocks/trauma-shake-snippet.md +5 -3
- package/skills/visual-effects/game-feel/SKILL.md +51 -33
- package/skills/visual-effects/recipes/dissolve/SKILL.md +51 -9
- package/skills/visual-effects/recipes/fire/SKILL.md +136 -49
- package/skills/visual-effects/recipes/hit-spark/SKILL.md +45 -8
- package/skills/visual-effects/recipes/lightning/SKILL.md +59 -27
- package/skills/visual-effects/recipes/magic-glow/SKILL.md +59 -26
- package/skills/visual-effects/recipes/muzzle-flash/SKILL.md +68 -25
- package/skills/visual-effects/recipes/smoke/SKILL.md +89 -28
- package/skills/visual-effects/recipes/water-ripple/SKILL.md +83 -23
- package/skills/workflow/brainstorming/SKILL.md +1 -1
- package/skills/workflow/debugging-game-feel/SKILL.md +29 -9
- package/skills/workflow/diagnosing-perf-regressions/SKILL.md +46 -21
- package/skills/workflow/gameskill/SKILL.md +11 -2
- package/skills/workflow/headless-scripting/SKILL.md +407 -0
- package/skills/workflow/investigating-bugs/SKILL.md +4 -2
- package/skills/workflow/mcpupdate/SKILL.md +9 -9
- package/skills/workflow/playtesting-a-feature/SKILL.md +94 -21
- package/skills/workflow/skill-create/SKILL.md +7 -7
- package/skills/workflow/skill-test/SKILL.md +3 -3
- package/skills/workflow/using-summer/SKILL.md +32 -3
- package/skills/workflow/verification-before-completion/SKILL.md +2 -0
- package/skills/workflow/writing-plans/SKILL.md +7 -7
- package/skills/workflow/writing-skills/SKILL.md +7 -5
- package/skills/workflow/writing-skills/testing-skills-with-subagents.md +1 -1
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { getCredentialMetadata, getCreatorToken, } from "./auth.js";
|
|
6
|
+
import { getCreatorApiUrl, readSummerConfig, } from "./config.js";
|
|
7
|
+
import { appendStoreJsonLine } from "./store.js";
|
|
8
|
+
export const CREATOR_API_CONTRACT = "summer.creator.v1";
|
|
9
|
+
const CREATOR_PUBLISH_PATH = "/api/creator/v1/publish";
|
|
10
|
+
const CREATOR_RELEASES_PATH = "/api/creator/v1/releases";
|
|
11
|
+
const REQUIRED_SCOPE = "publish";
|
|
12
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13
|
+
const VERSION = /^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$/;
|
|
14
|
+
export const CREATOR_API_AUDIT = {
|
|
15
|
+
auditedAt: "2026-07-30",
|
|
16
|
+
repository: "SummerEngine/summercraft",
|
|
17
|
+
contract: CREATOR_API_CONTRACT,
|
|
18
|
+
credentialAudience: "summercraft-creator",
|
|
19
|
+
requiredServerScope: REQUIRED_SCOPE,
|
|
20
|
+
operations: {
|
|
21
|
+
publish: { supported: true, route: CREATOR_PUBLISH_PATH },
|
|
22
|
+
releases: { supported: true, route: CREATOR_RELEASES_PATH },
|
|
23
|
+
logs: { supported: false, route: null },
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
const LOGS_RECOVERY = "Recovery: Summercraft must first own a durable runtime-log source with retention, redaction, project/release authorization, and a versioned query route. No placeholder logs were returned.";
|
|
27
|
+
export class CreatorOperationError extends Error {
|
|
28
|
+
code;
|
|
29
|
+
operation;
|
|
30
|
+
recovery;
|
|
31
|
+
status;
|
|
32
|
+
constructor(code, operation, message, recovery, status) {
|
|
33
|
+
super(`${message} ${recovery}`);
|
|
34
|
+
this.code = code;
|
|
35
|
+
this.operation = operation;
|
|
36
|
+
this.recovery = recovery;
|
|
37
|
+
this.status = status;
|
|
38
|
+
this.name = "CreatorOperationError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const defaultDependencies = {
|
|
42
|
+
fetch: (input, init) => globalThis.fetch(input, init),
|
|
43
|
+
now: () => new Date(),
|
|
44
|
+
};
|
|
45
|
+
async function resolveProjectId(explicit) {
|
|
46
|
+
const projectId = explicit?.trim() || (await readSummerConfig()).creator?.projectId;
|
|
47
|
+
if (!projectId) {
|
|
48
|
+
throw new Error('No creator project is selected. Recovery: pass --project-id <uuid>, or run "summer config set creator.projectId <uuid>".');
|
|
49
|
+
}
|
|
50
|
+
if (!UUID.test(projectId)) {
|
|
51
|
+
throw new Error(`Creator project ID "${projectId}" is not a UUID. Recovery: copy the Summer game ID from the creator dashboard and configure it again.`);
|
|
52
|
+
}
|
|
53
|
+
return projectId;
|
|
54
|
+
}
|
|
55
|
+
async function requireCreatorCredential(operation) {
|
|
56
|
+
const token = await getCreatorToken();
|
|
57
|
+
if (!token) {
|
|
58
|
+
throw new CreatorOperationError("creator_token_required", operation, "Creator publishing needs a separate Summercraft API token.", 'Recovery: run "summer login --creator", mint a token with the exact "publish" scope in the opened browser page, and paste its one-time sc_ value. Your core Summer login will not be changed.');
|
|
59
|
+
}
|
|
60
|
+
const metadata = await getCredentialMetadata();
|
|
61
|
+
if (!metadata.creator?.scopes.includes(REQUIRED_SCOPE)) {
|
|
62
|
+
throw new CreatorOperationError("creator_scope_missing", operation, `The locally recorded creator credential does not declare the exact "${REQUIRED_SCOPE}" scope.`, 'Recovery: run "summer login --creator --force", mint a new publish-scoped token, and retry. The server will still independently verify the scope.');
|
|
63
|
+
}
|
|
64
|
+
return token;
|
|
65
|
+
}
|
|
66
|
+
function recoveryForStatus(operation, status) {
|
|
67
|
+
if (status === 401 || status === 403) {
|
|
68
|
+
return 'Recovery: run "summer login --creator --force", mint a non-revoked token with the exact "publish" scope, and retry.';
|
|
69
|
+
}
|
|
70
|
+
if (status === 404) {
|
|
71
|
+
return 'Recovery: check creator.projectId against the Summer creator dashboard and make sure that game belongs to the token owner.';
|
|
72
|
+
}
|
|
73
|
+
if (status === 409 && operation === "publish") {
|
|
74
|
+
return "Recovery: read the server detail, bump an immutable version when required, then repeat the unconfirmed command to review the new exact target.";
|
|
75
|
+
}
|
|
76
|
+
return `Recovery: retry once; if it repeats, check creator.apiUrl and the Summercraft creator API health.`;
|
|
77
|
+
}
|
|
78
|
+
async function readJsonResponse(response, operation) {
|
|
79
|
+
const text = await response.text();
|
|
80
|
+
let payload;
|
|
81
|
+
try {
|
|
82
|
+
payload = JSON.parse(text);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
throw new CreatorOperationError("creator_invalid_response", operation, `The creator API returned a non-JSON response (${response.status}).`, recoveryForStatus(operation, response.status), response.status);
|
|
86
|
+
}
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
const code = typeof payload.error === "string" ? payload.error : "creator_request_failed";
|
|
89
|
+
const detail = typeof payload.detail === "string"
|
|
90
|
+
? payload.detail
|
|
91
|
+
: `The creator API refused the request (${response.status}).`;
|
|
92
|
+
const recovery = typeof payload.recovery === "string"
|
|
93
|
+
? payload.recovery
|
|
94
|
+
: recoveryForStatus(operation, response.status);
|
|
95
|
+
throw new CreatorOperationError(code, operation, detail, recovery, response.status);
|
|
96
|
+
}
|
|
97
|
+
if (payload.contract !== CREATOR_API_CONTRACT) {
|
|
98
|
+
throw new CreatorOperationError("creator_contract_mismatch", operation, `The creator API did not return contract ${CREATOR_API_CONTRACT}.`, "Recovery: upgrade the Summer CLI and retry; if it repeats, the configured creator API is incompatible.", response.status);
|
|
99
|
+
}
|
|
100
|
+
return payload;
|
|
101
|
+
}
|
|
102
|
+
async function creatorRequest(operation, path, token, deps, init = {}) {
|
|
103
|
+
let response;
|
|
104
|
+
try {
|
|
105
|
+
response = await deps.fetch(`${await getCreatorApiUrl()}${path}`, {
|
|
106
|
+
...init,
|
|
107
|
+
headers: {
|
|
108
|
+
accept: "application/json",
|
|
109
|
+
authorization: `Bearer ${token}`,
|
|
110
|
+
...(init.body ? { "content-type": "application/json" } : {}),
|
|
111
|
+
...init.headers,
|
|
112
|
+
},
|
|
113
|
+
signal: AbortSignal.timeout(operation === "publish" ? 300_000 : 30_000),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
throw new CreatorOperationError("creator_network_failed", operation, `The creator API request failed: ${error instanceof Error ? error.message : String(error)}.`, "Recovery: check your network and creator.apiUrl, then retry. No successful release is being claimed.");
|
|
118
|
+
}
|
|
119
|
+
return readJsonResponse(response, operation);
|
|
120
|
+
}
|
|
121
|
+
async function inspectArtifact(path) {
|
|
122
|
+
const artifactPath = resolve(path);
|
|
123
|
+
const info = await lstat(artifactPath).catch(() => null);
|
|
124
|
+
if (!info || !info.isFile() || info.isSymbolicLink()) {
|
|
125
|
+
throw new Error(`Release artifact ${artifactPath} must be a real regular file, not a missing path, directory, or symlink.`);
|
|
126
|
+
}
|
|
127
|
+
if (!artifactPath.toLowerCase().endsWith(".pck")) {
|
|
128
|
+
throw new Error(`Release artifact ${artifactPath} is not a .pck. Recovery: export a Summer PCK and pass its exact path with --artifact.`);
|
|
129
|
+
}
|
|
130
|
+
if (info.size <= 0) {
|
|
131
|
+
throw new Error(`Release artifact ${artifactPath} is empty. Recovery: export the Summer PCK again and verify it locally before publishing.`);
|
|
132
|
+
}
|
|
133
|
+
const hash = createHash("sha256");
|
|
134
|
+
for await (const chunk of createReadStream(artifactPath))
|
|
135
|
+
hash.update(chunk);
|
|
136
|
+
return {
|
|
137
|
+
path: artifactPath,
|
|
138
|
+
sizeBytes: info.size,
|
|
139
|
+
sha256: hash.digest("hex"),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
async function readManifest(path) {
|
|
143
|
+
if (!path)
|
|
144
|
+
return undefined;
|
|
145
|
+
const manifestPath = resolve(path);
|
|
146
|
+
try {
|
|
147
|
+
return JSON.parse(await readFile(manifestPath, "utf8"));
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
throw new Error(`Cannot read release manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}. Recovery: pass a valid JSON manifest or omit --manifest.`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function assertUploadUrl(value) {
|
|
154
|
+
if (typeof value !== "string") {
|
|
155
|
+
throw new CreatorOperationError("creator_invalid_response", "publish", "The prepare response did not contain an uploadUrl.", "Recovery: retry once; if it repeats, report the Summercraft creator API as unhealthy.");
|
|
156
|
+
}
|
|
157
|
+
let url;
|
|
158
|
+
try {
|
|
159
|
+
url = new URL(value);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
throw new CreatorOperationError("creator_unsafe_upload_url", "publish", "The creator API returned a malformed upload URL.", "Recovery: do not upload the artifact; report the creator API response as a security issue.");
|
|
163
|
+
}
|
|
164
|
+
const local = url.hostname === "localhost" ||
|
|
165
|
+
url.hostname === "127.0.0.1" ||
|
|
166
|
+
url.hostname === "::1";
|
|
167
|
+
if (url.username ||
|
|
168
|
+
url.password ||
|
|
169
|
+
(url.protocol !== "https:" && !(local && url.protocol === "http:"))) {
|
|
170
|
+
throw new CreatorOperationError("creator_unsafe_upload_url", "publish", "The creator API returned an unsafe upload URL.", "Recovery: do not upload the artifact; report the creator API response as a security issue.");
|
|
171
|
+
}
|
|
172
|
+
return url.toString();
|
|
173
|
+
}
|
|
174
|
+
function assertUploadHeaders(value) {
|
|
175
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
176
|
+
throw new CreatorOperationError("creator_invalid_response", "publish", "The prepare response did not contain signed upload headers.", "Recovery: retry once; if it repeats, report the Summercraft creator API as unhealthy.");
|
|
177
|
+
}
|
|
178
|
+
const headers = Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string"));
|
|
179
|
+
if (headers["content-type"] !== "application/octet-stream" ||
|
|
180
|
+
headers["if-none-match"] !== "*") {
|
|
181
|
+
throw new CreatorOperationError("creator_unsafe_upload_headers", "publish", "The prepare response did not bind the expected content type and write-once header.", "Recovery: do not upload the artifact; report the creator API response as a security issue.");
|
|
182
|
+
}
|
|
183
|
+
return headers;
|
|
184
|
+
}
|
|
185
|
+
async function uploadArtifact(uploadUrl, headers, artifactPath, deps) {
|
|
186
|
+
let response;
|
|
187
|
+
try {
|
|
188
|
+
response = await deps.fetch(uploadUrl, {
|
|
189
|
+
method: "PUT",
|
|
190
|
+
headers,
|
|
191
|
+
body: createReadStream(artifactPath),
|
|
192
|
+
// Node fetch requires this for a streaming request body. It is a Node
|
|
193
|
+
// extension and therefore absent from the DOM RequestInit declaration.
|
|
194
|
+
duplex: "half",
|
|
195
|
+
signal: AbortSignal.timeout(600_000),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
throw new CreatorOperationError("creator_upload_failed", "publish", `The artifact upload failed: ${error instanceof Error ? error.message : String(error)}.`, "Recovery: rerun the unconfirmed publish command to recompute and review the exact artifact, then retry.");
|
|
200
|
+
}
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
throw new CreatorOperationError("creator_upload_refused", "publish", `The immutable artifact upload was refused (${response.status}).`, response.status === 412
|
|
203
|
+
? "Recovery: the content-addressed key is already occupied. Do not overwrite it; verify the project/version and retry with a new immutable version."
|
|
204
|
+
: "Recovery: repeat prepare and reproduce every returned signed header exactly, then retry.", response.status);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
export async function publishCreator(input, overrides = {}) {
|
|
208
|
+
const deps = { ...defaultDependencies, ...overrides };
|
|
209
|
+
const project = resolve(input.project ?? process.cwd());
|
|
210
|
+
const projectId = await resolveProjectId(input.projectId);
|
|
211
|
+
const channel = input.channel?.trim() ||
|
|
212
|
+
(await readSummerConfig()).creator?.channel ||
|
|
213
|
+
"production";
|
|
214
|
+
if (channel !== "production") {
|
|
215
|
+
throw new CreatorOperationError("creator_channel_unsupported", "publish", `The Summercraft creator API has no "${channel}" release channel.`, 'Recovery: use --channel production or run "summer config set creator.channel production". No release was created.');
|
|
216
|
+
}
|
|
217
|
+
const version = input.version?.trim();
|
|
218
|
+
if (!version || !VERSION.test(version)) {
|
|
219
|
+
throw new Error("A release version is required and must be 1–32 characters of letters, numbers, dots, underscores, or hyphens. Recovery: pass --version <value>.");
|
|
220
|
+
}
|
|
221
|
+
if (!input.artifact) {
|
|
222
|
+
throw new Error("A release artifact is required. Recovery: export a Summer .pck and pass --artifact <path>; the CLI never guesses an export layout.");
|
|
223
|
+
}
|
|
224
|
+
const artifact = await inspectArtifact(input.artifact);
|
|
225
|
+
const manifest = await readManifest(input.manifest);
|
|
226
|
+
const auditBase = {
|
|
227
|
+
at: deps.now().toISOString(),
|
|
228
|
+
operation: "publish",
|
|
229
|
+
face: input.face,
|
|
230
|
+
project,
|
|
231
|
+
projectId,
|
|
232
|
+
channel,
|
|
233
|
+
artifact: artifact.path,
|
|
234
|
+
version,
|
|
235
|
+
sha256: artifact.sha256,
|
|
236
|
+
sizeBytes: artifact.sizeBytes,
|
|
237
|
+
notes: input.notes?.trim() || null,
|
|
238
|
+
};
|
|
239
|
+
if (!input.confirm) {
|
|
240
|
+
await appendStoreJsonLine("creator-audit.jsonl", {
|
|
241
|
+
...auditBase,
|
|
242
|
+
outcome: "confirmation_required",
|
|
243
|
+
});
|
|
244
|
+
throw new CreatorOperationError("publish_confirmation_required", "publish", `Publishing changes project ${projectId}: version ${version}, sha256 ${artifact.sha256}, ${artifact.sizeBytes} bytes from ${artifact.path}.`, "Recovery: show this exact project, version, digest, size, and artifact path to the user. Only after approval, rerun with --confirm (or confirm=true). No release was created.");
|
|
245
|
+
}
|
|
246
|
+
let token;
|
|
247
|
+
try {
|
|
248
|
+
token = await requireCreatorCredential("publish");
|
|
249
|
+
await appendStoreJsonLine("creator-audit.jsonl", {
|
|
250
|
+
...auditBase,
|
|
251
|
+
outcome: "started",
|
|
252
|
+
tokenAudience: "summercraft-creator",
|
|
253
|
+
advisoryTokenScopes: [REQUIRED_SCOPE],
|
|
254
|
+
});
|
|
255
|
+
const confirmation = {
|
|
256
|
+
gameId: projectId,
|
|
257
|
+
version,
|
|
258
|
+
sha256: artifact.sha256,
|
|
259
|
+
};
|
|
260
|
+
const prepareBody = {
|
|
261
|
+
contract: CREATOR_API_CONTRACT,
|
|
262
|
+
operation: "prepare",
|
|
263
|
+
...confirmation,
|
|
264
|
+
sizeBytes: artifact.sizeBytes,
|
|
265
|
+
contentType: "application/octet-stream",
|
|
266
|
+
...(input.notes?.trim() ? { changelog: input.notes.trim() } : {}),
|
|
267
|
+
...(manifest !== undefined ? { manifest } : {}),
|
|
268
|
+
confirmation,
|
|
269
|
+
};
|
|
270
|
+
const prepared = await creatorRequest("publish", CREATOR_PUBLISH_PATH, token, deps, {
|
|
271
|
+
method: "POST",
|
|
272
|
+
body: JSON.stringify(prepareBody),
|
|
273
|
+
});
|
|
274
|
+
if (prepared.operation !== "prepare") {
|
|
275
|
+
throw new CreatorOperationError("creator_invalid_response", "publish", "The prepare response named the wrong operation.", "Recovery: do not upload the artifact; upgrade the Summer CLI or report the creator API as incompatible.");
|
|
276
|
+
}
|
|
277
|
+
if (prepared.method !== "PUT" ||
|
|
278
|
+
prepared.finalizeUrl !== CREATOR_PUBLISH_PATH) {
|
|
279
|
+
throw new CreatorOperationError("creator_invalid_response", "publish", "The prepare response did not bind the expected PUT and versioned finalize route.", "Recovery: do not upload the artifact; upgrade the Summer CLI or report the creator API as incompatible.");
|
|
280
|
+
}
|
|
281
|
+
await uploadArtifact(assertUploadUrl(prepared.uploadUrl), assertUploadHeaders(prepared.headers), artifact.path, deps);
|
|
282
|
+
const finalized = await creatorRequest("publish", CREATOR_PUBLISH_PATH, token, deps, {
|
|
283
|
+
method: "POST",
|
|
284
|
+
body: JSON.stringify({
|
|
285
|
+
...prepareBody,
|
|
286
|
+
operation: "finalize",
|
|
287
|
+
}),
|
|
288
|
+
});
|
|
289
|
+
if (finalized.operation !== "finalize" ||
|
|
290
|
+
typeof finalized.releaseId !== "string" ||
|
|
291
|
+
!UUID.test(finalized.releaseId) ||
|
|
292
|
+
finalized.status !== "pending_review" ||
|
|
293
|
+
finalized.gameId !== projectId ||
|
|
294
|
+
finalized.version !== version ||
|
|
295
|
+
finalized.sha256 !== artifact.sha256 ||
|
|
296
|
+
finalized.sizeBytes !== artifact.sizeBytes) {
|
|
297
|
+
throw new CreatorOperationError("creator_invalid_response", "publish", "The finalize response did not echo the exact project, version, digest, size, release ID, and status.", "Recovery: query summer releases before retrying; the server may have recorded the release even though its response was incomplete.");
|
|
298
|
+
}
|
|
299
|
+
const result = {
|
|
300
|
+
ok: true,
|
|
301
|
+
contract: CREATOR_API_CONTRACT,
|
|
302
|
+
operation: "publish",
|
|
303
|
+
projectId,
|
|
304
|
+
releaseId: finalized.releaseId,
|
|
305
|
+
version,
|
|
306
|
+
sha256: artifact.sha256,
|
|
307
|
+
sizeBytes: artifact.sizeBytes,
|
|
308
|
+
status: finalized.status,
|
|
309
|
+
detail: typeof finalized.detail === "string" ? finalized.detail : null,
|
|
310
|
+
};
|
|
311
|
+
await appendStoreJsonLine("creator-audit.jsonl", {
|
|
312
|
+
...auditBase,
|
|
313
|
+
outcome: "succeeded",
|
|
314
|
+
releaseId: result.releaseId,
|
|
315
|
+
releaseStatus: result.status,
|
|
316
|
+
});
|
|
317
|
+
return result;
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
await appendStoreJsonLine("creator-audit.jsonl", {
|
|
321
|
+
...auditBase,
|
|
322
|
+
outcome: "failed",
|
|
323
|
+
code: error instanceof CreatorOperationError
|
|
324
|
+
? error.code
|
|
325
|
+
: "creator_request_invalid",
|
|
326
|
+
status: error instanceof CreatorOperationError ? error.status ?? null : null,
|
|
327
|
+
});
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function readReleases(value, projectId) {
|
|
332
|
+
if (!Array.isArray(value)) {
|
|
333
|
+
throw new CreatorOperationError("creator_invalid_response", "releases", "The creator API response did not contain a releases array.", "Recovery: upgrade the Summer CLI and retry; if it repeats, report the creator API as incompatible.");
|
|
334
|
+
}
|
|
335
|
+
const releases = [];
|
|
336
|
+
for (const item of value) {
|
|
337
|
+
const row = item;
|
|
338
|
+
if (!row ||
|
|
339
|
+
typeof row !== "object" ||
|
|
340
|
+
typeof row.releaseId !== "string" ||
|
|
341
|
+
!UUID.test(row.releaseId) ||
|
|
342
|
+
row.gameId !== projectId ||
|
|
343
|
+
typeof row.version !== "string" ||
|
|
344
|
+
!["published", "pending_review", "retired"].includes(String(row.state)) ||
|
|
345
|
+
typeof row.gameStatus !== "string" ||
|
|
346
|
+
(row.sha256 !== null &&
|
|
347
|
+
(typeof row.sha256 !== "string" ||
|
|
348
|
+
!/^[0-9a-f]{64}$/.test(row.sha256))) ||
|
|
349
|
+
(row.sizeBytes !== null &&
|
|
350
|
+
(typeof row.sizeBytes !== "number" ||
|
|
351
|
+
!Number.isSafeInteger(row.sizeBytes) ||
|
|
352
|
+
row.sizeBytes < 0)) ||
|
|
353
|
+
(row.changelog !== null && typeof row.changelog !== "string") ||
|
|
354
|
+
typeof row.submittedAt !== "string" ||
|
|
355
|
+
!Number.isFinite(Date.parse(row.submittedAt)) ||
|
|
356
|
+
(row.artifactPlane !== "r2" && row.artifactPlane !== "legacy")) {
|
|
357
|
+
throw new CreatorOperationError("creator_invalid_response", "releases", "The creator API returned a malformed or cross-project release item.", "Recovery: do not trust this release list; upgrade the Summer CLI or report the creator API as incompatible.");
|
|
358
|
+
}
|
|
359
|
+
releases.push(row);
|
|
360
|
+
}
|
|
361
|
+
return releases;
|
|
362
|
+
}
|
|
363
|
+
export async function listCreatorReleases(input, overrides = {}) {
|
|
364
|
+
const deps = { ...defaultDependencies, ...overrides };
|
|
365
|
+
const projectId = await resolveProjectId(input.projectId);
|
|
366
|
+
const token = await requireCreatorCredential("releases");
|
|
367
|
+
const params = new URLSearchParams({
|
|
368
|
+
gameId: projectId,
|
|
369
|
+
limit: String(input.limit ?? 20),
|
|
370
|
+
});
|
|
371
|
+
if (input.cursor)
|
|
372
|
+
params.set("cursor", input.cursor);
|
|
373
|
+
const payload = await creatorRequest("releases", `${CREATOR_RELEASES_PATH}?${params}`, token, deps);
|
|
374
|
+
if (payload.gameId !== projectId) {
|
|
375
|
+
throw new CreatorOperationError("creator_invalid_response", "releases", "The creator API returned release history for a different project.", "Recovery: do not trust this release list; check creator.apiUrl and report the response as a security issue.");
|
|
376
|
+
}
|
|
377
|
+
if (payload.nextCursor !== null &&
|
|
378
|
+
typeof payload.nextCursor !== "string") {
|
|
379
|
+
throw new CreatorOperationError("creator_invalid_response", "releases", "The creator API returned a malformed release cursor.", "Recovery: omit the cursor and retry from the newest release; report the response if it repeats.");
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
ok: true,
|
|
383
|
+
contract: CREATOR_API_CONTRACT,
|
|
384
|
+
operation: "releases",
|
|
385
|
+
projectId,
|
|
386
|
+
releases: readReleases(payload.releases, projectId),
|
|
387
|
+
nextCursor: payload.nextCursor,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
export async function readCreatorLogs(input) {
|
|
391
|
+
await resolveProjectId(input.projectId);
|
|
392
|
+
throw new CreatorOperationError("creator_backend_unavailable", "logs", "Summercraft does not have a durable creator runtime-log API.", LOGS_RECOVERY);
|
|
393
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type DoctorResult } from "../commands/doctor.js";
|
|
2
|
+
export interface DebugReportOptions {
|
|
3
|
+
issue?: string;
|
|
4
|
+
outputPath?: string;
|
|
5
|
+
includePlaySession?: boolean;
|
|
6
|
+
playWaitMs?: number;
|
|
7
|
+
maxConsoleLines?: number;
|
|
8
|
+
maxDebuggerEntries?: number;
|
|
9
|
+
includeDoctor?: boolean;
|
|
10
|
+
baseDir?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DebugReportArtifact {
|
|
13
|
+
path: string;
|
|
14
|
+
markdown: string;
|
|
15
|
+
report: DebugReport;
|
|
16
|
+
}
|
|
17
|
+
export interface DebugReport {
|
|
18
|
+
generatedAt: string;
|
|
19
|
+
issue: string | null;
|
|
20
|
+
environment: {
|
|
21
|
+
cliVersion: string;
|
|
22
|
+
nodeVersion: string;
|
|
23
|
+
platform: string;
|
|
24
|
+
release: string;
|
|
25
|
+
cwd: string;
|
|
26
|
+
hostname: string;
|
|
27
|
+
};
|
|
28
|
+
doctor: DoctorResult | null;
|
|
29
|
+
mcp: {
|
|
30
|
+
logPath: string;
|
|
31
|
+
recentLog: string[];
|
|
32
|
+
};
|
|
33
|
+
engine: {
|
|
34
|
+
connected: boolean;
|
|
35
|
+
error?: string;
|
|
36
|
+
health?: unknown;
|
|
37
|
+
project?: unknown;
|
|
38
|
+
scene?: unknown;
|
|
39
|
+
diagnostics?: unknown;
|
|
40
|
+
console?: unknown;
|
|
41
|
+
debuggerErrors?: unknown;
|
|
42
|
+
debuggerWarnings?: unknown;
|
|
43
|
+
playSession?: unknown;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export declare function defaultDebugReportFilename(date?: Date): string;
|
|
47
|
+
export declare function redactDebugReport(report: DebugReport): DebugReport;
|
|
48
|
+
export declare function renderDebugReportMarkdown(report: DebugReport): string;
|
|
49
|
+
export declare function collectDebugReport(options?: DebugReportOptions): Promise<DebugReport>;
|
|
50
|
+
export declare function createDebugReportArtifact(options?: DebugReportOptions): Promise<DebugReportArtifact>;
|