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
package/dist/lib/auth.js
CHANGED
|
@@ -1,52 +1,188 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { readStoreJson, readStoreText, removeStoreFile, writeStoreJson, writeStoreText, } from "./store.js";
|
|
2
|
+
export { getSummerDir } from "./store.js";
|
|
3
|
+
const AUTH_TOKEN_FILE = "auth-token";
|
|
4
|
+
const CLOUD_TOKEN_FILE = "cloud-token";
|
|
5
|
+
const CREATOR_TOKEN_FILE = "creator-token";
|
|
6
|
+
const USER_FILE = "user.json";
|
|
7
|
+
const METADATA_FILE = "credential-metadata.json";
|
|
8
|
+
function decodeJwtMetadata(token, explicitScopes = []) {
|
|
9
|
+
const payload = decodeJwtPayload(token);
|
|
10
|
+
const aud = payload.aud;
|
|
11
|
+
const audience = typeof aud === "string"
|
|
12
|
+
? [aud]
|
|
13
|
+
: Array.isArray(aud)
|
|
14
|
+
? aud.filter((value) => typeof value === "string")
|
|
15
|
+
: [];
|
|
16
|
+
const claimScopes = typeof payload.scope === "string"
|
|
17
|
+
? payload.scope.split(/\s+/)
|
|
18
|
+
: Array.isArray(payload.scp)
|
|
19
|
+
? payload.scp.filter((value) => typeof value === "string")
|
|
20
|
+
: [];
|
|
21
|
+
const scopes = [...new Set([...explicitScopes, ...claimScopes].filter(Boolean))].sort();
|
|
22
|
+
const issuedAt = typeof payload.iat === "number"
|
|
23
|
+
? new Date(payload.iat * 1000).toISOString()
|
|
24
|
+
: undefined;
|
|
25
|
+
const expiresAt = typeof payload.exp === "number"
|
|
26
|
+
? new Date(payload.exp * 1000).toISOString()
|
|
27
|
+
: undefined;
|
|
28
|
+
return {
|
|
29
|
+
audience,
|
|
30
|
+
scopes,
|
|
31
|
+
...(typeof payload.type === "string" ? { tokenType: payload.type } : {}),
|
|
32
|
+
...(issuedAt ? { issuedAt } : {}),
|
|
33
|
+
...(expiresAt ? { expiresAt } : {}),
|
|
34
|
+
};
|
|
8
35
|
}
|
|
9
|
-
|
|
10
|
-
if (!existsSync(SUMMER_DIR)) {
|
|
11
|
-
await mkdir(SUMMER_DIR, { recursive: true, mode: 0o700 });
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
export async function getAuthToken() {
|
|
36
|
+
function decodeJwtPayload(token) {
|
|
15
37
|
try {
|
|
16
|
-
const
|
|
17
|
-
|
|
38
|
+
const encoded = token.split(".")[1];
|
|
39
|
+
if (encoded) {
|
|
40
|
+
return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
41
|
+
}
|
|
18
42
|
}
|
|
19
43
|
catch {
|
|
20
|
-
|
|
44
|
+
// Metadata is advisory only. The gateway verifies the signature.
|
|
21
45
|
}
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
async function readMetadata() {
|
|
49
|
+
return ((await readStoreJson(METADATA_FILE)) ?? {
|
|
50
|
+
schemaVersion: 1,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async function updateMetadata(kind, value) {
|
|
54
|
+
const current = await readMetadata();
|
|
55
|
+
const next = {
|
|
56
|
+
...current,
|
|
57
|
+
schemaVersion: 1,
|
|
58
|
+
[kind]: value,
|
|
59
|
+
};
|
|
60
|
+
if (!next.auth)
|
|
61
|
+
delete next.auth;
|
|
62
|
+
if (!next.cloud)
|
|
63
|
+
delete next.cloud;
|
|
64
|
+
if (!next.creator)
|
|
65
|
+
delete next.creator;
|
|
66
|
+
await writeStoreJson(METADATA_FILE, next);
|
|
67
|
+
}
|
|
68
|
+
export async function getAuthToken() {
|
|
69
|
+
const token = await readStoreText(AUTH_TOKEN_FILE);
|
|
70
|
+
return token?.trim() || null;
|
|
22
71
|
}
|
|
23
|
-
export async function saveAuthToken(token) {
|
|
24
|
-
|
|
25
|
-
|
|
72
|
+
export async function saveAuthToken(token, scopes = []) {
|
|
73
|
+
const clean = token.trim();
|
|
74
|
+
if (!clean)
|
|
75
|
+
throw new Error("Cannot save an empty auth token.");
|
|
76
|
+
await writeStoreText(AUTH_TOKEN_FILE, `${clean}\n`);
|
|
77
|
+
await updateMetadata("auth", decodeJwtMetadata(clean, scopes));
|
|
26
78
|
}
|
|
27
79
|
export async function getCloudToken() {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
80
|
+
const token = await readStoreText(CLOUD_TOKEN_FILE);
|
|
81
|
+
return token?.trim() || null;
|
|
82
|
+
}
|
|
83
|
+
export async function saveCloudToken(token, scopes = []) {
|
|
84
|
+
const clean = token.trim();
|
|
85
|
+
if (!clean)
|
|
86
|
+
throw new Error("Cannot save an empty cloud token.");
|
|
87
|
+
await writeStoreText(CLOUD_TOKEN_FILE, `${clean}\n`);
|
|
88
|
+
await updateMetadata("cloud", decodeJwtMetadata(clean, scopes));
|
|
35
89
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Summercraft creator tokens are a separate credential audience from the
|
|
92
|
+
* core-compatible Summer CLI JWT. Never store an sc_ token in auth-token:
|
|
93
|
+
* the engine reads that file and expects the summer-cli JWT contract.
|
|
94
|
+
*/
|
|
95
|
+
export async function getCreatorToken() {
|
|
96
|
+
const token = await readStoreText(CREATOR_TOKEN_FILE);
|
|
97
|
+
return token?.trim() || null;
|
|
98
|
+
}
|
|
99
|
+
export async function saveCreatorToken(token) {
|
|
100
|
+
const clean = token.trim();
|
|
101
|
+
if (!/^sc_[A-Za-z0-9_-]{32,}$/.test(clean)) {
|
|
102
|
+
throw new Error('That is not a Summercraft creator API token. Recovery: open /creator/settings/tokens, mint a token with the "publish" scope, and paste the complete one-time sc_ value.');
|
|
103
|
+
}
|
|
104
|
+
await writeStoreText(CREATOR_TOKEN_FILE, `${clean}\n`);
|
|
105
|
+
await updateMetadata("creator", {
|
|
106
|
+
audience: ["summercraft-creator"],
|
|
107
|
+
scopes: ["publish"],
|
|
108
|
+
tokenType: "api",
|
|
109
|
+
});
|
|
39
110
|
}
|
|
40
111
|
export async function getUserInfo() {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
112
|
+
return readStoreJson(USER_FILE);
|
|
113
|
+
}
|
|
114
|
+
export async function saveUserInfo(info) {
|
|
115
|
+
await writeStoreJson(USER_FILE, info);
|
|
116
|
+
}
|
|
117
|
+
export async function saveLoginSession(session) {
|
|
118
|
+
const payload = decodeJwtPayload(session.token);
|
|
119
|
+
if (!session.user ||
|
|
120
|
+
typeof payload.sub !== "string" ||
|
|
121
|
+
payload.sub !== session.user.id) {
|
|
122
|
+
throw new Error('The gateway returned an incomplete or mismatched identity. Recovery: run "summer login --force" again; if it repeats, report the CLI login endpoint as unhealthy.');
|
|
44
123
|
}
|
|
45
|
-
|
|
46
|
-
|
|
124
|
+
const metadata = decodeJwtMetadata(session.token, session.scopes);
|
|
125
|
+
if (metadata.tokenType !== "cli" ||
|
|
126
|
+
!metadata.audience.includes("summer-cli")) {
|
|
127
|
+
throw new Error('The gateway returned a token that is not valid for the Summer CLI. Recovery: run "summer login --force" again after the gateway is updated.');
|
|
47
128
|
}
|
|
129
|
+
if (metadata.expiresAt && Date.parse(metadata.expiresAt) <= Date.now()) {
|
|
130
|
+
throw new Error('The gateway returned an expired CLI token. Recovery: run "summer login --force" again; if it repeats, check the gateway clock and token issuer.');
|
|
131
|
+
}
|
|
132
|
+
// Keep auth-token and user.json canonical: the desktop engine already reads
|
|
133
|
+
// those exact files. credential-metadata.json adds audience/scope information
|
|
134
|
+
// without changing or duplicating the secret.
|
|
135
|
+
// Activate the new auth token last. The desktop engine cross-checks token.sub
|
|
136
|
+
// against user.json, so an interrupted multi-file write fails closed instead
|
|
137
|
+
// of adopting a mismatched identity.
|
|
138
|
+
if (session.user)
|
|
139
|
+
await saveUserInfo(session.user);
|
|
140
|
+
if (session.cloudToken) {
|
|
141
|
+
await saveCloudToken(session.cloudToken, session.cloudScopes);
|
|
142
|
+
}
|
|
143
|
+
await saveAuthToken(session.token, session.scopes);
|
|
48
144
|
}
|
|
49
|
-
export async function
|
|
50
|
-
await
|
|
51
|
-
|
|
145
|
+
export async function getCredentialMetadata() {
|
|
146
|
+
const metadata = await readMetadata();
|
|
147
|
+
const token = await getAuthToken();
|
|
148
|
+
const cloudToken = await getCloudToken();
|
|
149
|
+
const creatorToken = await getCreatorToken();
|
|
150
|
+
return {
|
|
151
|
+
schemaVersion: 1,
|
|
152
|
+
...(token
|
|
153
|
+
? { auth: metadata.auth ?? decodeJwtMetadata(token) }
|
|
154
|
+
: {}),
|
|
155
|
+
...(cloudToken
|
|
156
|
+
? { cloud: metadata.cloud ?? decodeJwtMetadata(cloudToken) }
|
|
157
|
+
: {}),
|
|
158
|
+
...(creatorToken
|
|
159
|
+
? {
|
|
160
|
+
creator: metadata.creator ?? {
|
|
161
|
+
audience: ["summercraft-creator"],
|
|
162
|
+
scopes: [],
|
|
163
|
+
tokenType: "api",
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
: {}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
export async function clearAuthCredentials() {
|
|
170
|
+
let removed = 0;
|
|
171
|
+
for (const file of [
|
|
172
|
+
AUTH_TOKEN_FILE,
|
|
173
|
+
CLOUD_TOKEN_FILE,
|
|
174
|
+
CREATOR_TOKEN_FILE,
|
|
175
|
+
USER_FILE,
|
|
176
|
+
METADATA_FILE,
|
|
177
|
+
]) {
|
|
178
|
+
if (await removeStoreFile(file))
|
|
179
|
+
removed += 1;
|
|
180
|
+
}
|
|
181
|
+
return removed;
|
|
182
|
+
}
|
|
183
|
+
export function assertCredentialScopes(metadata, requiredScopes) {
|
|
184
|
+
const missing = requiredScopes.filter((scope) => !metadata?.scopes.includes(scope));
|
|
185
|
+
if (missing.length === 0)
|
|
186
|
+
return;
|
|
187
|
+
throw new Error(`This sign-in does not grant ${missing.join(", ")}. Recovery: run "summer login --force" after the platform enables scoped creator tokens, then retry.`);
|
|
52
188
|
}
|
|
@@ -13,9 +13,22 @@ export declare function writeProjectPathMarker(projectRoot: string): Promise<voi
|
|
|
13
13
|
*/
|
|
14
14
|
export declare function saveDirtyScenesIfRunning(projectRoot: string): Promise<boolean>;
|
|
15
15
|
/**
|
|
16
|
-
* After apply (spec 8.7):
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* After apply (spec 8.7): tell the open editor that files changed underneath it.
|
|
17
|
+
*
|
|
18
|
+
* BOTH HALVES OF THIS ARE CURRENTLY DEAD, and the user-visible symptom is that
|
|
19
|
+
* after a cloud pull the editor keeps showing the pre-pull bytes:
|
|
20
|
+
*
|
|
21
|
+
* - `ScanChanges` is not an engine op. The dispatch ladder
|
|
22
|
+
* (modules/1summer_engine/editor/ops_executor.cpp:1060-1290) has 84 op kinds and
|
|
23
|
+
* no ScanChanges branch, so this fails on EVERY build, not just older ones. The
|
|
24
|
+
* engine-side capability exists as EditorFileSystem::scan_changes() but is
|
|
25
|
+
* exposed neither as an op nor to GDScript, so there is no caller-reachable
|
|
26
|
+
* rescan verb at all. O2 owns adding one.
|
|
27
|
+
* - The scene reload depends on `session.scene`, which /api/health never returns
|
|
28
|
+
* (see the note in lib/engine.ts), so it is always undefined and never fires.
|
|
29
|
+
*
|
|
30
|
+
* Until the engine exposes a rescan, say so out loud rather than swallowing it —
|
|
31
|
+
* a stale editor that looks correct is exactly the failure this codebase is trying
|
|
32
|
+
* to stop shipping.
|
|
20
33
|
*/
|
|
21
34
|
export declare function notifyEngineAfterApply(projectRoot: string, appliedPaths: readonly string[]): Promise<void>;
|
|
@@ -81,10 +81,23 @@ export async function saveDirtyScenesIfRunning(projectRoot) {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
* After apply (spec 8.7):
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
84
|
+
* After apply (spec 8.7): tell the open editor that files changed underneath it.
|
|
85
|
+
*
|
|
86
|
+
* BOTH HALVES OF THIS ARE CURRENTLY DEAD, and the user-visible symptom is that
|
|
87
|
+
* after a cloud pull the editor keeps showing the pre-pull bytes:
|
|
88
|
+
*
|
|
89
|
+
* - `ScanChanges` is not an engine op. The dispatch ladder
|
|
90
|
+
* (modules/1summer_engine/editor/ops_executor.cpp:1060-1290) has 84 op kinds and
|
|
91
|
+
* no ScanChanges branch, so this fails on EVERY build, not just older ones. The
|
|
92
|
+
* engine-side capability exists as EditorFileSystem::scan_changes() but is
|
|
93
|
+
* exposed neither as an op nor to GDScript, so there is no caller-reachable
|
|
94
|
+
* rescan verb at all. O2 owns adding one.
|
|
95
|
+
* - The scene reload depends on `session.scene`, which /api/health never returns
|
|
96
|
+
* (see the note in lib/engine.ts), so it is always undefined and never fires.
|
|
97
|
+
*
|
|
98
|
+
* Until the engine exposes a rescan, say so out loud rather than swallowing it —
|
|
99
|
+
* a stale editor that looks correct is exactly the failure this codebase is trying
|
|
100
|
+
* to stop shipping.
|
|
88
101
|
*/
|
|
89
102
|
export async function notifyEngineAfterApply(projectRoot, appliedPaths) {
|
|
90
103
|
if (!appliedPaths.length)
|
|
@@ -92,22 +105,35 @@ export async function notifyEngineAfterApply(projectRoot, appliedPaths) {
|
|
|
92
105
|
const session = await connectForProject(projectRoot);
|
|
93
106
|
if (!session)
|
|
94
107
|
return;
|
|
108
|
+
let rescanned = false;
|
|
95
109
|
try {
|
|
96
110
|
await session.client.executeOps([{ op: "ScanChanges" }], { projectIdHash: session.projectIdHash });
|
|
111
|
+
rescanned = true;
|
|
97
112
|
}
|
|
98
113
|
catch {
|
|
99
|
-
|
|
114
|
+
rescanned = false;
|
|
100
115
|
}
|
|
116
|
+
let reloaded = false;
|
|
101
117
|
try {
|
|
102
118
|
const scene = session.scene;
|
|
103
119
|
if (scene && scene.startsWith("res://")) {
|
|
104
120
|
const sceneRel = scene.slice("res://".length);
|
|
105
121
|
if (appliedPaths.includes(sceneRel)) {
|
|
106
122
|
await session.client.executeOps([{ op: "OpenScene", path: scene }], { projectIdHash: session.projectIdHash });
|
|
123
|
+
reloaded = true;
|
|
107
124
|
}
|
|
108
125
|
}
|
|
109
126
|
}
|
|
110
127
|
catch {
|
|
111
|
-
|
|
128
|
+
reloaded = false;
|
|
129
|
+
}
|
|
130
|
+
if (!rescanned) {
|
|
131
|
+
console.warn(`Pulled ${appliedPaths.length} file(s), but could not tell the running editor to rescan ` +
|
|
132
|
+
"(this engine build exposes no rescan op). The editor may still be showing the old " +
|
|
133
|
+
"contents — click into its window, or reopen the affected scene, before trusting what you see.");
|
|
134
|
+
}
|
|
135
|
+
else if (!reloaded) {
|
|
136
|
+
console.warn("Pulled files and rescanned, but could not confirm which scene is open, so an open " +
|
|
137
|
+
"scene replaced by the pull was not reloaded. Reopen it before trusting what you see.");
|
|
112
138
|
}
|
|
113
139
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface SummerConfig {
|
|
2
|
+
schemaVersion: 1;
|
|
3
|
+
gateway?: {
|
|
4
|
+
url?: string;
|
|
5
|
+
};
|
|
6
|
+
creator?: {
|
|
7
|
+
apiUrl?: string;
|
|
8
|
+
projectId?: string;
|
|
9
|
+
channel?: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export declare const CONFIG_KEYS: readonly ["gateway.url", "creator.apiUrl", "creator.projectId", "creator.channel"];
|
|
13
|
+
export type ConfigKey = (typeof CONFIG_KEYS)[number];
|
|
14
|
+
export declare function readSummerConfig(): Promise<SummerConfig>;
|
|
15
|
+
export declare function getConfigValue(config: SummerConfig, key: ConfigKey): string | undefined;
|
|
16
|
+
export declare function setConfigValue(key: ConfigKey, rawValue: string): Promise<SummerConfig>;
|
|
17
|
+
export declare function unsetConfigValue(key: ConfigKey): Promise<SummerConfig>;
|
|
18
|
+
export declare function isConfigKey(value: string): value is ConfigKey;
|
|
19
|
+
export declare function getGatewayUrl(): Promise<string>;
|
|
20
|
+
export declare function getCreatorApiUrl(): Promise<string>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { readStoreJson, writeStoreJson } from "./store.js";
|
|
2
|
+
const CONFIG_FILE = "config.json";
|
|
3
|
+
const DEFAULT_GATEWAY_URL = "https://www.summerengine.com";
|
|
4
|
+
const DEFAULT_CREATOR_API_URL = "https://summercraft.ai";
|
|
5
|
+
export const CONFIG_KEYS = [
|
|
6
|
+
"gateway.url",
|
|
7
|
+
"creator.apiUrl",
|
|
8
|
+
"creator.projectId",
|
|
9
|
+
"creator.channel",
|
|
10
|
+
];
|
|
11
|
+
function emptyConfig() {
|
|
12
|
+
return { schemaVersion: 1 };
|
|
13
|
+
}
|
|
14
|
+
export async function readSummerConfig() {
|
|
15
|
+
const value = await readStoreJson(CONFIG_FILE);
|
|
16
|
+
if (!value)
|
|
17
|
+
return emptyConfig();
|
|
18
|
+
if (value.schemaVersion !== 1) {
|
|
19
|
+
throw new Error(`Unsupported ~/.summer/config.json schema ${String(value.schemaVersion)}. Recovery: upgrade the Summer CLI, or move config.json aside and configure it again.`);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function validateGatewayUrl(value) {
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = new URL(value);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new Error(`gateway.url must be a complete URL. Recovery: use "summer config set gateway.url https://www.summerengine.com".`);
|
|
30
|
+
}
|
|
31
|
+
const local = parsed.hostname === "localhost" ||
|
|
32
|
+
parsed.hostname === "127.0.0.1" ||
|
|
33
|
+
parsed.hostname === "::1";
|
|
34
|
+
if (parsed.protocol !== "https:" && !(local && parsed.protocol === "http:")) {
|
|
35
|
+
throw new Error("gateway.url must use HTTPS (HTTP is allowed only for localhost). Recovery: set an HTTPS URL and retry.");
|
|
36
|
+
}
|
|
37
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
38
|
+
throw new Error("gateway.url cannot contain credentials, query parameters, or fragments. Recovery: set only the origin URL and retry.");
|
|
39
|
+
}
|
|
40
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
41
|
+
}
|
|
42
|
+
function validateCreatorApiUrl(value) {
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = new URL(value);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error('creator.apiUrl must be a complete URL. Recovery: use "summer config set creator.apiUrl https://summercraft.ai".');
|
|
49
|
+
}
|
|
50
|
+
const local = parsed.hostname === "localhost" ||
|
|
51
|
+
parsed.hostname === "127.0.0.1" ||
|
|
52
|
+
parsed.hostname === "::1";
|
|
53
|
+
if (parsed.protocol !== "https:" && !(local && parsed.protocol === "http:")) {
|
|
54
|
+
throw new Error("creator.apiUrl must use HTTPS (HTTP is allowed only for localhost). Recovery: set an HTTPS URL and retry.");
|
|
55
|
+
}
|
|
56
|
+
if (parsed.username ||
|
|
57
|
+
parsed.password ||
|
|
58
|
+
parsed.pathname !== "/" ||
|
|
59
|
+
parsed.search ||
|
|
60
|
+
parsed.hash) {
|
|
61
|
+
throw new Error("creator.apiUrl must be an origin without credentials, a path, query parameters, or fragments.");
|
|
62
|
+
}
|
|
63
|
+
return parsed.origin;
|
|
64
|
+
}
|
|
65
|
+
function validateValue(key, value) {
|
|
66
|
+
const clean = value.trim();
|
|
67
|
+
if (!clean) {
|
|
68
|
+
throw new Error(`Cannot set ${key} to an empty value. Recovery: provide a value, or run "summer config unset ${key}".`);
|
|
69
|
+
}
|
|
70
|
+
if (key === "gateway.url")
|
|
71
|
+
return validateGatewayUrl(clean);
|
|
72
|
+
if (key === "creator.apiUrl")
|
|
73
|
+
return validateCreatorApiUrl(clean);
|
|
74
|
+
if (key === "creator.channel" && !/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(clean)) {
|
|
75
|
+
throw new Error("creator.channel may contain letters, numbers, dots, underscores, and hyphens. Recovery: choose a channel such as production or preview.");
|
|
76
|
+
}
|
|
77
|
+
if (key === "creator.projectId" && clean.length > 128) {
|
|
78
|
+
throw new Error("creator.projectId is too long. Recovery: copy the project ID from the Summer creator dashboard and retry.");
|
|
79
|
+
}
|
|
80
|
+
return clean;
|
|
81
|
+
}
|
|
82
|
+
export function getConfigValue(config, key) {
|
|
83
|
+
if (key === "gateway.url")
|
|
84
|
+
return config.gateway?.url;
|
|
85
|
+
if (key === "creator.apiUrl")
|
|
86
|
+
return config.creator?.apiUrl;
|
|
87
|
+
if (key === "creator.projectId")
|
|
88
|
+
return config.creator?.projectId;
|
|
89
|
+
return config.creator?.channel;
|
|
90
|
+
}
|
|
91
|
+
export async function setConfigValue(key, rawValue) {
|
|
92
|
+
const config = await readSummerConfig();
|
|
93
|
+
const value = validateValue(key, rawValue);
|
|
94
|
+
if (key === "gateway.url") {
|
|
95
|
+
config.gateway = { ...config.gateway, url: value };
|
|
96
|
+
}
|
|
97
|
+
else if (key === "creator.apiUrl") {
|
|
98
|
+
config.creator = { ...config.creator, apiUrl: value };
|
|
99
|
+
}
|
|
100
|
+
else if (key === "creator.projectId") {
|
|
101
|
+
config.creator = { ...config.creator, projectId: value };
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
config.creator = { ...config.creator, channel: value };
|
|
105
|
+
}
|
|
106
|
+
await writeStoreJson(CONFIG_FILE, config);
|
|
107
|
+
return config;
|
|
108
|
+
}
|
|
109
|
+
export async function unsetConfigValue(key) {
|
|
110
|
+
const config = await readSummerConfig();
|
|
111
|
+
if (key === "gateway.url" && config.gateway)
|
|
112
|
+
delete config.gateway.url;
|
|
113
|
+
if (key === "creator.apiUrl" && config.creator)
|
|
114
|
+
delete config.creator.apiUrl;
|
|
115
|
+
if (key === "creator.projectId" && config.creator) {
|
|
116
|
+
delete config.creator.projectId;
|
|
117
|
+
}
|
|
118
|
+
if (key === "creator.channel" && config.creator)
|
|
119
|
+
delete config.creator.channel;
|
|
120
|
+
if (config.gateway && Object.keys(config.gateway).length === 0) {
|
|
121
|
+
delete config.gateway;
|
|
122
|
+
}
|
|
123
|
+
if (config.creator && Object.keys(config.creator).length === 0) {
|
|
124
|
+
delete config.creator;
|
|
125
|
+
}
|
|
126
|
+
await writeStoreJson(CONFIG_FILE, config);
|
|
127
|
+
return config;
|
|
128
|
+
}
|
|
129
|
+
export function isConfigKey(value) {
|
|
130
|
+
return CONFIG_KEYS.includes(value);
|
|
131
|
+
}
|
|
132
|
+
export async function getGatewayUrl() {
|
|
133
|
+
const environment = process.env.SUMMER_GATEWAY_URL?.trim();
|
|
134
|
+
if (environment)
|
|
135
|
+
return validateGatewayUrl(environment);
|
|
136
|
+
const config = await readSummerConfig();
|
|
137
|
+
return config.gateway?.url ?? DEFAULT_GATEWAY_URL;
|
|
138
|
+
}
|
|
139
|
+
export async function getCreatorApiUrl() {
|
|
140
|
+
const config = await readSummerConfig();
|
|
141
|
+
return config.creator?.apiUrl ?? DEFAULT_CREATOR_API_URL;
|
|
142
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export type CreatorOperation = "publish" | "releases" | "logs";
|
|
2
|
+
export type CreatorFace = "cli" | "mcp";
|
|
3
|
+
export declare const CREATOR_API_CONTRACT: "summer.creator.v1";
|
|
4
|
+
export declare const CREATOR_API_AUDIT: {
|
|
5
|
+
readonly auditedAt: "2026-07-30";
|
|
6
|
+
readonly repository: "SummerEngine/summercraft";
|
|
7
|
+
readonly contract: "summer.creator.v1";
|
|
8
|
+
readonly credentialAudience: "summercraft-creator";
|
|
9
|
+
readonly requiredServerScope: "publish";
|
|
10
|
+
readonly operations: {
|
|
11
|
+
readonly publish: {
|
|
12
|
+
readonly supported: true;
|
|
13
|
+
readonly route: "/api/creator/v1/publish";
|
|
14
|
+
};
|
|
15
|
+
readonly releases: {
|
|
16
|
+
readonly supported: true;
|
|
17
|
+
readonly route: "/api/creator/v1/releases";
|
|
18
|
+
};
|
|
19
|
+
readonly logs: {
|
|
20
|
+
readonly supported: false;
|
|
21
|
+
readonly route: null;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export declare class CreatorOperationError extends Error {
|
|
26
|
+
readonly code: string;
|
|
27
|
+
readonly operation: CreatorOperation;
|
|
28
|
+
readonly recovery: string;
|
|
29
|
+
readonly status?: number | undefined;
|
|
30
|
+
constructor(code: string, operation: CreatorOperation, message: string, recovery: string, status?: number | undefined);
|
|
31
|
+
}
|
|
32
|
+
export interface CreatorClientDependencies {
|
|
33
|
+
fetch: typeof fetch;
|
|
34
|
+
now: () => Date;
|
|
35
|
+
}
|
|
36
|
+
export interface PublishCreatorInput {
|
|
37
|
+
project?: string;
|
|
38
|
+
artifact?: string;
|
|
39
|
+
manifest?: string;
|
|
40
|
+
version?: string;
|
|
41
|
+
projectId?: string;
|
|
42
|
+
channel?: string;
|
|
43
|
+
notes?: string;
|
|
44
|
+
confirm?: boolean;
|
|
45
|
+
face: CreatorFace;
|
|
46
|
+
}
|
|
47
|
+
export interface CreatorPublishResult {
|
|
48
|
+
ok: true;
|
|
49
|
+
contract: typeof CREATOR_API_CONTRACT;
|
|
50
|
+
operation: "publish";
|
|
51
|
+
projectId: string;
|
|
52
|
+
releaseId: string;
|
|
53
|
+
version: string;
|
|
54
|
+
sha256: string;
|
|
55
|
+
sizeBytes: number;
|
|
56
|
+
status: string;
|
|
57
|
+
detail: string | null;
|
|
58
|
+
}
|
|
59
|
+
export declare function publishCreator(input: PublishCreatorInput, overrides?: Partial<CreatorClientDependencies>): Promise<CreatorPublishResult>;
|
|
60
|
+
export interface ListCreatorReleasesInput {
|
|
61
|
+
projectId?: string;
|
|
62
|
+
limit?: number;
|
|
63
|
+
cursor?: string;
|
|
64
|
+
face: CreatorFace;
|
|
65
|
+
}
|
|
66
|
+
export interface CreatorRelease {
|
|
67
|
+
releaseId: string;
|
|
68
|
+
gameId: string;
|
|
69
|
+
version: string;
|
|
70
|
+
state: "published" | "pending_review" | "retired";
|
|
71
|
+
gameStatus: string;
|
|
72
|
+
sha256: string | null;
|
|
73
|
+
sizeBytes: number | null;
|
|
74
|
+
changelog: string | null;
|
|
75
|
+
submittedAt: string;
|
|
76
|
+
artifactPlane: "r2" | "legacy";
|
|
77
|
+
}
|
|
78
|
+
export interface CreatorReleasesResult {
|
|
79
|
+
ok: true;
|
|
80
|
+
contract: typeof CREATOR_API_CONTRACT;
|
|
81
|
+
operation: "releases";
|
|
82
|
+
projectId: string;
|
|
83
|
+
releases: CreatorRelease[];
|
|
84
|
+
nextCursor: string | null;
|
|
85
|
+
}
|
|
86
|
+
export declare function listCreatorReleases(input: ListCreatorReleasesInput, overrides?: Partial<CreatorClientDependencies>): Promise<CreatorReleasesResult>;
|
|
87
|
+
export interface ReadCreatorLogsInput {
|
|
88
|
+
projectId?: string;
|
|
89
|
+
releaseId?: string;
|
|
90
|
+
limit?: number;
|
|
91
|
+
face: CreatorFace;
|
|
92
|
+
}
|
|
93
|
+
export declare function readCreatorLogs(input: ReadCreatorLogsInput): Promise<never>;
|