summer-engine 2.7.0 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/.claude-plugin/plugin.json +2 -1
  2. package/.codex-plugin/plugin.json +3 -2
  3. package/.cursor-plugin/plugin.json +2 -1
  4. package/.opencode/INSTALL.md +1 -1
  5. package/.opencode/plugins/summer.js +1 -1
  6. package/AGENTS.md +4 -4
  7. package/CHANGELOG.md +28 -0
  8. package/GEMINI.md +5 -4
  9. package/README.md +58 -21
  10. package/assets/autopilot/README.md +51 -0
  11. package/assets/autopilot/autopilot.gd +222 -0
  12. package/assets/autopilot/probe_base.gd +262 -0
  13. package/assets/autopilot/run.sh +78 -0
  14. package/commands/gameskill.md +11 -2
  15. package/dist/bin/summer.js +8 -0
  16. package/dist/commands/config.d.ts +2 -0
  17. package/dist/commands/config.js +53 -0
  18. package/dist/commands/create.d.ts +1 -0
  19. package/dist/commands/create.js +38 -6
  20. package/dist/commands/list.js +1 -1
  21. package/dist/commands/login.d.ts +19 -0
  22. package/dist/commands/login.js +136 -37
  23. package/dist/commands/logout.js +3 -16
  24. package/dist/commands/logs.d.ts +2 -0
  25. package/dist/commands/logs.js +19 -0
  26. package/dist/commands/mcp.js +7 -2
  27. package/dist/commands/open.js +1 -1
  28. package/dist/commands/publish.d.ts +2 -0
  29. package/dist/commands/publish.js +20 -0
  30. package/dist/commands/releases.d.ts +2 -0
  31. package/dist/commands/releases.js +20 -0
  32. package/dist/commands/run.js +1 -1
  33. package/dist/lib/api-client.d.ts +27 -8
  34. package/dist/lib/api-client.js +65 -14
  35. package/dist/lib/auth.d.ts +40 -12
  36. package/dist/lib/auth.js +173 -37
  37. package/dist/lib/cloud/engine-bridge.d.ts +17 -4
  38. package/dist/lib/cloud/engine-bridge.js +32 -6
  39. package/dist/lib/config.d.ts +20 -0
  40. package/dist/lib/config.js +142 -0
  41. package/dist/lib/creator.d.ts +93 -0
  42. package/dist/lib/creator.js +393 -0
  43. package/dist/lib/engine.d.ts +35 -2
  44. package/dist/lib/engine.js +240 -6
  45. package/dist/lib/public-surface-paths.d.ts +13 -0
  46. package/dist/lib/public-surface-paths.js +42 -0
  47. package/dist/lib/skills-registry.d.ts +8 -0
  48. package/dist/lib/skills-registry.js +9 -0
  49. package/dist/lib/store.d.ts +14 -0
  50. package/dist/lib/store.js +145 -0
  51. package/dist/lib/summer-compatibility.d.ts +6 -0
  52. package/dist/lib/summer-compatibility.js +22 -0
  53. package/dist/mcp/server.d.ts +8 -1
  54. package/dist/mcp/server.js +25 -4
  55. package/dist/mcp/tools/creator-tools.d.ts +2 -0
  56. package/dist/mcp/tools/creator-tools.js +105 -0
  57. package/dist/mcp/tools/debug-tools.d.ts +12 -0
  58. package/dist/mcp/tools/debug-tools.js +100 -1
  59. package/dist/mcp/tools/generate-tools.js +16 -6
  60. package/dist/mcp/tools/project-tools.js +131 -16
  61. package/dist/mcp/tools/scene-tools.js +166 -66
  62. package/dist/mcp/tools/visual-tools.js +32 -8
  63. package/dist/mcp/tools/with-engine.js +51 -19
  64. package/hooks/pre-commit-doctor.sh +0 -0
  65. package/hooks/run-hook.cmd +0 -0
  66. package/hooks/session-start.sh +5 -5
  67. package/package.json +3 -1
  68. package/references/collaborative-protocol.md +50 -0
  69. package/references/gd-style.md +121 -0
  70. package/references/godot-version.md +51 -0
  71. package/references/mcp-tools-reference.md +199 -0
  72. package/references/public-surface-language.md +30 -0
  73. package/references/summer-folder.md +90 -0
  74. package/references/template-registry.md +73 -0
  75. package/skills/2d-assets/character-portrait/SKILL.md +17 -12
  76. package/skills/2d-assets/concept-art/SKILL.md +10 -9
  77. package/skills/2d-assets/pixel-art/SKILL.md +36 -20
  78. package/skills/2d-assets/skybox-panorama/SKILL.md +32 -37
  79. package/skills/2d-assets/sprite-sheet/SKILL.md +40 -35
  80. package/skills/2d-assets/tileable-texture/SKILL.md +28 -22
  81. package/skills/2d-assets/ui-graphics/SKILL.md +46 -32
  82. package/skills/2d-assets/use-widget-asset/SKILL.md +8 -6
  83. package/skills/3d-assets/character-model/SKILL.md +34 -19
  84. package/skills/3d-assets/environment-kit/SKILL.md +11 -5
  85. package/skills/3d-assets/organic-model/SKILL.md +8 -5
  86. package/skills/3d-assets/prop-model/SKILL.md +9 -3
  87. package/skills/3d-assets/vehicle-model/SKILL.md +32 -19
  88. package/skills/README.md +1 -1
  89. package/skills/ai-and-npcs/design-npc/SKILL.md +31 -13
  90. package/skills/animation/animation-tree/SKILL.md +39 -15
  91. package/skills/animation/facial-and-lipsync/SKILL.md +25 -8
  92. package/skills/animation/generate-motion/SKILL.md +47 -15
  93. package/skills/animation/procedural-animation/SKILL.md +31 -13
  94. package/skills/animation/retarget/SKILL.md +127 -79
  95. package/skills/audio/adaptive-music/SKILL.md +24 -20
  96. package/skills/audio/ambient-bed/SKILL.md +22 -16
  97. package/skills/audio/audio-direction/SKILL.md +15 -10
  98. package/skills/audio/music-track/SKILL.md +18 -12
  99. package/skills/audio/sound-effect/SKILL.md +17 -12
  100. package/skills/audio/voice-line/SKILL.md +42 -42
  101. package/skills/character-controllers/fps-controller/SKILL.md +26 -16
  102. package/skills/debugging/debug/SKILL.md +52 -20
  103. package/skills/deployment/export-and-ship/SKILL.md +105 -38
  104. package/skills/deployment/remote-deploy/SKILL.md +56 -23
  105. package/skills/gameplay-mechanics/auto-fire-targeting/SKILL.md +26 -1
  106. package/skills/gameplay-mechanics/design-mechanic/SKILL.md +23 -15
  107. package/skills/level-design/design-level/SKILL.md +22 -8
  108. package/skills/level-design/scene-to-level/SKILL.md +13 -7
  109. package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +8 -8
  110. package/skills/multiplayer-and-networking/peer-to-peer-multiplayer/SKILL.md +2 -7
  111. package/skills/multiplayer-and-networking/setup-multiplayer/SKILL.md +26 -18
  112. package/skills/performance/tune-performance/SKILL.md +51 -32
  113. package/skills/rendering-and-lighting/3d-lighting/SKILL.md +34 -5
  114. package/skills/rendering-and-lighting/art-direction/SKILL.md +6 -8
  115. package/skills/scene-and-project/brainstorm-game/SKILL.md +3 -5
  116. package/skills/scene-and-project/make-game/SKILL.md +2 -1
  117. package/skills/scene-and-project/play/SKILL.md +10 -4
  118. package/skills/scene-and-project/scene-composition/SKILL.md +21 -8
  119. package/skills/scene-and-project/summer-cloud/SKILL.md +1 -1
  120. package/skills/ui-and-ux/ui-basics/SKILL.md +16 -7
  121. package/skills/video/animated-loop/SKILL.md +63 -39
  122. package/skills/video/cinematic-cutscene/SKILL.md +66 -30
  123. package/skills/video/trailer-shot/SKILL.md +30 -21
  124. package/skills/visual-effects/_building-blocks/additive-billboard-particles.md +25 -16
  125. package/skills/visual-effects/_building-blocks/trauma-shake-snippet.md +5 -3
  126. package/skills/visual-effects/game-feel/SKILL.md +51 -33
  127. package/skills/visual-effects/recipes/dissolve/SKILL.md +51 -9
  128. package/skills/visual-effects/recipes/fire/SKILL.md +136 -49
  129. package/skills/visual-effects/recipes/hit-spark/SKILL.md +45 -8
  130. package/skills/visual-effects/recipes/lightning/SKILL.md +59 -27
  131. package/skills/visual-effects/recipes/magic-glow/SKILL.md +59 -26
  132. package/skills/visual-effects/recipes/muzzle-flash/SKILL.md +68 -25
  133. package/skills/visual-effects/recipes/smoke/SKILL.md +89 -28
  134. package/skills/visual-effects/recipes/water-ripple/SKILL.md +83 -23
  135. package/skills/workflow/brainstorming/SKILL.md +1 -1
  136. package/skills/workflow/brainstorming/scripts/start-server.sh +0 -0
  137. package/skills/workflow/brainstorming/scripts/stop-server.sh +0 -0
  138. package/skills/workflow/debugging-game-feel/SKILL.md +29 -9
  139. package/skills/workflow/diagnosing-perf-regressions/SKILL.md +46 -21
  140. package/skills/workflow/gameskill/SKILL.md +11 -2
  141. package/skills/workflow/headless-scripting/SKILL.md +407 -0
  142. package/skills/workflow/investigating-bugs/SKILL.md +4 -2
  143. package/skills/workflow/investigating-bugs/find-polluter.sh +0 -0
  144. package/skills/workflow/mcpupdate/SKILL.md +3 -3
  145. package/skills/workflow/playtesting-a-feature/SKILL.md +94 -21
  146. package/skills/workflow/skill-create/SKILL.md +7 -7
  147. package/skills/workflow/skill-test/SKILL.md +3 -3
  148. package/skills/workflow/using-summer/SKILL.md +12 -2
  149. package/skills/workflow/verification-before-completion/SKILL.md +2 -0
  150. package/skills/workflow/writing-plans/SKILL.md +7 -7
  151. package/skills/workflow/writing-skills/SKILL.md +7 -5
  152. package/skills/workflow/writing-skills/render-graphs.js +0 -0
  153. package/skills/workflow/writing-skills/testing-skills-with-subagents.md +1 -1
@@ -2,7 +2,7 @@ import { mkdir, writeFile } from "fs/promises";
2
2
  import { tmpdir } from "os";
3
3
  import { join } from "path";
4
4
  import { randomUUID } from "node:crypto";
5
- import { getApiToken, getApiPort, checkEngineHealth } from "./engine.js";
5
+ import { getApiToken, getApiPort, checkEngineHealth, resolveEngineConnection, } from "./engine.js";
6
6
  import { classifyOpsResponse, pollOpToTerminal, } from "./async-op-lifecycle.js";
7
7
  function asRecord(value) {
8
8
  return value && typeof value === "object" && !Array.isArray(value)
@@ -74,15 +74,25 @@ export class EngineApiClient {
74
74
  // preserves the pre-2.6.6 constructor contract for existing callers that only
75
75
  // supplied a projectIdHash.
76
76
  targetIdentity;
77
- constructor(port, token, targetIdentity = {}) {
77
+ selection;
78
+ constructor(port, token, targetIdentity = {}, selection = null) {
78
79
  this.port = port;
79
80
  this.token = token;
80
81
  this.targetIdentity =
81
82
  typeof targetIdentity === "string"
82
83
  ? { projectIdHash: targetIdentity }
83
84
  : { ...targetIdentity };
84
- }
85
- static async connect() {
85
+ this.selection = selection ? { ...selection } : null;
86
+ }
87
+ static async connect(selection) {
88
+ if (selection) {
89
+ const connection = await resolveEngineConnection(selection);
90
+ return new EngineApiClient(connection.port, connection.token, {
91
+ instanceId: connection.health.instanceId,
92
+ projectId: connection.health.projectId,
93
+ projectIdHash: connection.health.projectIdHash,
94
+ }, { ...selection });
95
+ }
86
96
  const port = await getApiPort();
87
97
  const token = await getApiToken();
88
98
  if (!token) {
@@ -320,14 +330,33 @@ export class EngineApiClient {
320
330
  });
321
331
  return this.request("GET", `/api/state/read-file?${query.toString()}`);
322
332
  }
323
- async getSceneState(scenePath) {
324
- const query = scenePath
325
- ? `?scene=${encodeURIComponent(scenePath)}`
326
- : "";
333
+ async getSceneState(scenePath, options) {
334
+ // depth/limit only take effect on targeted (scene=) reads: the engine
335
+ // routes those through the live command queue and forwards every query
336
+ // param into StateProvider::scene_state (tool_net_thread.cpp
337
+ // _parse_state_args). An UNtargeted read is answered from a pre-published
338
+ // snapshot built with the defaults (depth 2, limit 200) and its query
339
+ // params are ignored — callers who need depth/limit must pass scenePath
340
+ // (summer_get_scene_tree resolves the current scene path for this).
341
+ const parts = [];
342
+ if (scenePath)
343
+ parts.push(`scene=${encodeURIComponent(scenePath)}`);
344
+ if (options?.depth !== undefined)
345
+ parts.push(`depth=${options.depth}`);
346
+ if (options?.limit !== undefined)
347
+ parts.push(`limit=${options.limit}`);
348
+ const query = parts.length ? `?${parts.join("&")}` : "";
327
349
  return this.request("GET", `/api/state/scene${query}`);
328
350
  }
329
- async getProjectState() {
330
- return this.request("GET", "/api/state/project");
351
+ async getProjectState(prefix) {
352
+ // ?prefix= is sent for forward-compatibility, but current engine builds
353
+ // IGNORE it: /api/state/project is always served from the snapshot, which
354
+ // is published with empty args (local_api_server.cpp
355
+ // _maybe_publish_snapshot), so StateProvider::project_state never sees the
356
+ // query. Callers that need a prefix subset must also filter client-side
357
+ // (summer_get_project_context does).
358
+ const query = prefix ? `?prefix=${encodeURIComponent(prefix)}` : "";
359
+ return this.request("GET", `/api/state/project${query}`);
331
360
  }
332
361
  async getDiagnostics() {
333
362
  return this.request("GET", "/api/state/diagnostics");
@@ -465,6 +494,9 @@ export class EngineApiClient {
465
494
  sceneHasCamera: boolFrom(payload.scene_has_camera),
466
495
  sceneHadLight: boolFrom(payload.scene_had_light),
467
496
  usedSyntheticCamera: boolFrom(payload.used_synthetic_camera),
497
+ framing: stringFrom(payload.framing),
498
+ framedNode: stringFrom(payload.framed_node),
499
+ renderRetries: numberFrom(payload.render_retries),
468
500
  projectMismatch,
469
501
  metadata: withoutImageData(payload),
470
502
  };
@@ -515,9 +547,14 @@ export class EngineApiClient {
515
547
  * Offscreen scene render via the `ScenePreview` op over /api/ops (no game
516
548
  * boot; physics/animations are static). Mirrors the web previewScene op input
517
549
  * (snapshot-tools.ts): { op:'ScenePreview', scene_path?, framing?, size?,
518
- * node_path? } — scene_path optional, engine defaults to the open scene. The
519
- * result carries image_base64 + mime (+ width/height) plus the P4.3 confession
520
- * fields (scene_has_camera / scene_had_light / used_synthetic_camera).
550
+ * node? } — scene_path optional, engine defaults to the open scene. `node` is
551
+ * the canonical focus-node arg (the engine also accepts the legacy web wire
552
+ * name `node_path`); an unresolvable node fails with failure_reason
553
+ * "node_not_found". framing "auto" is an alias of "iso"; the engine also
554
+ * accepts "top"/"front"/"back"/"left"/"right" and echoes the resolved framing.
555
+ * The result carries image_base64 + mime (+ width/height) plus the P4.3
556
+ * confession fields (scene_has_camera / scene_had_light /
557
+ * used_synthetic_camera) and framing / framed_node / render_retries.
521
558
  */
522
559
  async scenePreview(input) {
523
560
  const opInput = { op: "ScenePreview" };
@@ -529,7 +566,7 @@ export class EngineApiClient {
529
566
  if (input?.size)
530
567
  opInput.size = input.size;
531
568
  if (input?.nodePath)
532
- opInput.node_path = input.nodePath;
569
+ opInput.node = input.nodePath;
533
570
  let response;
534
571
  try {
535
572
  // executeOps stamps the bound identity so a drifted project is rejected
@@ -558,6 +595,20 @@ export class EngineApiClient {
558
595
  * will fail and trigger a reconnect+retry if the client really is stale.
559
596
  */
560
597
  async credentialsChanged() {
598
+ if (this.selection) {
599
+ try {
600
+ const connection = await resolveEngineConnection(this.selection);
601
+ return (connection.port !== this.port ||
602
+ connection.token !== this.token ||
603
+ connection.health.instanceId !== this.targetIdentity.instanceId);
604
+ }
605
+ catch {
606
+ // A selected editor disappearing is real drift. Dropping the cached
607
+ // client makes the next connect report which project/instance is absent
608
+ // instead of silently following another editor's global pointer.
609
+ return true;
610
+ }
611
+ }
561
612
  try {
562
613
  const [port, token] = await Promise.all([getApiPort(), getApiToken()]);
563
614
  if (!token)
@@ -1,15 +1,43 @@
1
- export declare function getSummerDir(): string;
2
- export declare function getAuthToken(): Promise<string | null>;
3
- export declare function saveAuthToken(token: string): Promise<void>;
4
- export declare function getCloudToken(): Promise<string | null>;
5
- export declare function saveCloudToken(token: string): Promise<void>;
6
- export declare function getUserInfo(): Promise<{
1
+ export { getSummerDir } from "./store.js";
2
+ export interface SummerUserInfo {
7
3
  id: string;
8
4
  email: string;
9
5
  name?: string;
10
- } | null>;
11
- export declare function saveUserInfo(info: {
12
- id: string;
13
- email: string;
14
- name?: string;
15
- }): Promise<void>;
6
+ }
7
+ export interface CredentialMetadata {
8
+ audience: string[];
9
+ scopes: string[];
10
+ tokenType?: string;
11
+ issuedAt?: string;
12
+ expiresAt?: string;
13
+ }
14
+ interface CredentialMetadataDocument {
15
+ schemaVersion: 1;
16
+ auth?: CredentialMetadata;
17
+ cloud?: CredentialMetadata;
18
+ creator?: CredentialMetadata;
19
+ }
20
+ export interface LoginSession {
21
+ token: string;
22
+ cloudToken?: string | null;
23
+ user?: SummerUserInfo;
24
+ scopes?: string[];
25
+ cloudScopes?: string[];
26
+ }
27
+ export declare function getAuthToken(): Promise<string | null>;
28
+ export declare function saveAuthToken(token: string, scopes?: string[]): Promise<void>;
29
+ export declare function getCloudToken(): Promise<string | null>;
30
+ export declare function saveCloudToken(token: string, scopes?: string[]): Promise<void>;
31
+ /**
32
+ * Summercraft creator tokens are a separate credential audience from the
33
+ * core-compatible Summer CLI JWT. Never store an sc_ token in auth-token:
34
+ * the engine reads that file and expects the summer-cli JWT contract.
35
+ */
36
+ export declare function getCreatorToken(): Promise<string | null>;
37
+ export declare function saveCreatorToken(token: string): Promise<void>;
38
+ export declare function getUserInfo(): Promise<SummerUserInfo | null>;
39
+ export declare function saveUserInfo(info: SummerUserInfo): Promise<void>;
40
+ export declare function saveLoginSession(session: LoginSession): Promise<void>;
41
+ export declare function getCredentialMetadata(): Promise<CredentialMetadataDocument>;
42
+ export declare function clearAuthCredentials(): Promise<number>;
43
+ export declare function assertCredentialScopes(metadata: CredentialMetadata | undefined, requiredScopes: string[]): void;
package/dist/lib/auth.js CHANGED
@@ -1,52 +1,188 @@
1
- import { readFile, writeFile, mkdir } from "fs/promises";
2
- import { existsSync } from "fs";
3
- import { join } from "path";
4
- import { homedir } from "os";
5
- const SUMMER_DIR = join(homedir(), ".summer");
6
- export function getSummerDir() {
7
- return SUMMER_DIR;
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
- async function ensureSummerDir() {
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 token = await readFile(join(SUMMER_DIR, "auth-token"), "utf-8");
17
- return token.trim() || null;
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
- return null;
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
- await ensureSummerDir();
25
- await writeFile(join(SUMMER_DIR, "auth-token"), token, { encoding: "utf-8", mode: 0o600 });
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
- try {
29
- const token = await readFile(join(SUMMER_DIR, "cloud-token"), "utf-8");
30
- return token.trim() || null;
31
- }
32
- catch {
33
- return null;
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
- export async function saveCloudToken(token) {
37
- await ensureSummerDir();
38
- await writeFile(join(SUMMER_DIR, "cloud-token"), token, { encoding: "utf-8", mode: 0o600 });
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
- try {
42
- const data = await readFile(join(SUMMER_DIR, "user.json"), "utf-8");
43
- return JSON.parse(data);
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
- catch {
46
- return null;
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 saveUserInfo(info) {
50
- await ensureSummerDir();
51
- await writeFile(join(SUMMER_DIR, "user.json"), JSON.stringify(info, null, 2), { encoding: "utf-8", mode: 0o600 });
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): trigger a filesystem rescan and reload the open
17
- * scene if the pull replaced it. ScanChanges is the engine-side hook; older
18
- * engines without it simply reject the op and the editor's own focus scan
19
- * picks the changes up.
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): trigger a filesystem rescan and reload the open
85
- * scene if the pull replaced it. ScanChanges is the engine-side hook; older
86
- * engines without it simply reject the op and the editor's own focus scan
87
- * picks the changes up.
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
- // Engine build without the ScanChanges op; non-fatal.
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
- // Reload is best-effort; the editor reloads from disk on focus.
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>;