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
@@ -1,14 +1,16 @@
1
1
  import { Command } from "commander";
2
- import { existsSync, mkdirSync, writeFileSync } from "fs";
3
- import { join, resolve } from "path";
2
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
3
+ import { dirname, join, resolve } from "path";
4
+ import { fileURLToPath } from "url";
4
5
  import { fetchRemoteTemplates, cloneTemplate, matchTemplate } from "../lib/remote-templates.js";
6
+ import { SUMMER_ENGINE_COMPATIBILITY } from "../lib/summer-compatibility.js";
5
7
  const BUILTIN_TEMPLATES = [
6
8
  {
7
9
  name: "empty",
8
10
  description: "Empty 3D project with just a root node",
9
11
  builtin: true,
10
12
  generate: (dir, name) => {
11
- writeFileSync(join(dir, "project.godot"), projectGodot(name, "res://main.tscn"));
13
+ writeFileSync(join(dir, "project.godot"), renderProjectSettings(name, "res://main.tscn"));
12
14
  writeFileSync(join(dir, "main.tscn"), emptyScene());
13
15
  },
14
16
  },
@@ -17,11 +19,34 @@ const BUILTIN_TEMPLATES = [
17
19
  description: "3D scene with camera, light, and floor",
18
20
  builtin: true,
19
21
  generate: (dir, name) => {
20
- writeFileSync(join(dir, "project.godot"), projectGodot(name, "res://main.tscn"));
22
+ writeFileSync(join(dir, "project.godot"), renderProjectSettings(name, "res://main.tscn"));
21
23
  writeFileSync(join(dir, "main.tscn"), basicScene3D());
22
24
  },
23
25
  },
24
26
  ];
27
+ /**
28
+ * Copy the autopilot verification scaffold into a fresh project.
29
+ *
30
+ * Every project gets a working example of how to prove its own gameplay: a probe
31
+ * that drives the player with real input, saves rendered frames, asserts, and
32
+ * exits. Without it every agent reinvents verification from scratch, and most
33
+ * settle for asking the user to play the game instead.
34
+ *
35
+ * Never overwrites — an existing tests/autopilot/ is the user's, not ours.
36
+ */
37
+ function scaffoldAutopilot(projectDir) {
38
+ const here = dirname(fileURLToPath(import.meta.url));
39
+ // dist/commands/create.js -> package root
40
+ const source = resolve(here, "..", "..", "assets", "autopilot");
41
+ const target = join(projectDir, "tests", "autopilot");
42
+ if (!existsSync(source) || existsSync(target))
43
+ return false;
44
+ mkdirSync(target, { recursive: true });
45
+ for (const entry of readdirSync(source)) {
46
+ copyFileSync(join(source, entry), join(target, entry));
47
+ }
48
+ return true;
49
+ }
25
50
  export const createCommand = new Command("create")
26
51
  .description("Create a new Summer Engine project. Built-in templates work offline; community templates clone from github.com/SummerEngine/template-*.")
27
52
  .argument("<template>", `Template slug. Built-in: ${BUILTIN_TEMPLATES.map((t) => t.name).join(", ")}. Or any community slug — see "summer list templates".`)
@@ -39,9 +64,12 @@ export const createCommand = new Command("create")
39
64
  console.log(`Creating project from built-in '${templateName}' template...`);
40
65
  mkdirSync(fullPath, { recursive: true });
41
66
  builtin.generate(fullPath, dirName);
67
+ const scaffolded = scaffoldAutopilot(fullPath);
42
68
  console.log(`\nProject created at ${fullPath}`);
43
69
  console.log("\nNext steps:");
44
70
  console.log(` summer run ${dirName}`);
71
+ if (scaffolded)
72
+ console.log(` bash ${dirName}/tests/autopilot/run.sh (verify the game without opening it)`);
45
73
  console.log(" Ask your agent to run summer:brainstorm-game to create .summer/GameSoul.md");
46
74
  return;
47
75
  }
@@ -83,21 +111,25 @@ export const createCommand = new Command("create")
83
111
  console.error(err.message);
84
112
  process.exit(1);
85
113
  }
114
+ const scaffolded = scaffoldAutopilot(fullPath);
86
115
  console.log(`\nProject created at ${fullPath}`);
87
116
  console.log("Source: " + remote.url);
88
117
  console.log("\nNext steps:");
89
118
  console.log(` summer run ${dirName}`);
119
+ if (scaffolded)
120
+ console.log(` bash ${dirName}/tests/autopilot/run.sh (verify the game without opening it)`);
90
121
  console.log(" Ask your agent to run summer:brainstorm-game to create .summer/GameSoul.md");
91
122
  });
92
- function projectGodot(name, mainScene) {
123
+ export function renderProjectSettings(name, mainScene) {
93
124
  return `; Summer Engine Project
94
125
  ; Generated by summer-engine CLI
126
+ ; Technical base ${SUMMER_ENGINE_COMPATIBILITY.currentTechnicalBaseVersion}; Summer follows upstream continuously
95
127
 
96
128
  [application]
97
129
 
98
130
  config/name="${name}"
99
131
  run/main_scene="${mainScene}"
100
- config/features=PackedStringArray("4.5")
132
+ config/features=PackedStringArray("${SUMMER_ENGINE_COMPATIBILITY.projectFeatureTag}")
101
133
 
102
134
  [rendering]
103
135
 
@@ -55,7 +55,7 @@ function listProjects() {
55
55
  }
56
56
  }
57
57
  if (projects.length === 0) {
58
- console.log("No Summer Engine / Godot projects found in current directory.");
58
+ console.log("No Summer Engine projects found in current directory.");
59
59
  console.log("\nCreate one: summer create 3d-basic my-game");
60
60
  return;
61
61
  }
@@ -1,2 +1,21 @@
1
1
  import { Command } from "commander";
2
2
  export declare const loginCommand: Command;
3
+ export interface CreatorLoginDependencies {
4
+ openUrl: (url: string) => Promise<unknown>;
5
+ readSecret: (prompt: string) => Promise<string>;
6
+ log: (message: string) => void;
7
+ }
8
+ /**
9
+ * The browser session mints the token; the token is pasted once into a hidden
10
+ * prompt. This deliberately does not replace the core Summer login JWT.
11
+ */
12
+ export declare function runCreatorLogin(overrides?: Partial<CreatorLoginDependencies>): Promise<void>;
13
+ export interface LoginDependencies {
14
+ fetch: typeof fetch;
15
+ openUrl: (url: string) => Promise<unknown>;
16
+ randomId: () => string;
17
+ sleep: (ms: number) => Promise<void>;
18
+ now: () => number;
19
+ log: (message: string) => void;
20
+ }
21
+ export declare function runLogin(overrides?: Partial<LoginDependencies>): Promise<void>;
@@ -1,49 +1,155 @@
1
1
  import { Command } from "commander";
2
2
  import { randomUUID } from "crypto";
3
3
  import open from "open";
4
- import { getAuthToken, saveAuthToken, saveCloudToken, saveUserInfo } from "../lib/auth.js";
5
- const GATEWAY_URL = process.env.SUMMER_GATEWAY_URL || "https://www.summerengine.com";
4
+ import { getAuthToken, getCreatorToken, saveCreatorToken, saveLoginSession, } from "../lib/auth.js";
5
+ import { getCreatorApiUrl, getGatewayUrl } from "../lib/config.js";
6
6
  const POLL_INTERVAL_MS = 2000;
7
- const POLL_TIMEOUT_MS = 120000;
7
+ // One generous window on ONE session id. First-time users may need to create an
8
+ // account and confirm an email before they can approve the CLI. The gateway
9
+ // never expires a pending session (the completed payload waits under this id
10
+ // for pickup), so the worst mistake would be rotating ids mid-wait — the user
11
+ // would approve the original browser tab while we poll a different session.
12
+ const POLL_TIMEOUT_MS = 900000;
13
+ const HEARTBEAT_MS = 30000;
8
14
  export const loginCommand = new Command("login")
9
15
  .description("Sign in to Summer Engine via your browser")
16
+ .option("--creator", "Connect a separately scoped Summercraft creator token for publishing")
10
17
  .option("--force", "Force re-authentication even if already logged in")
11
18
  .action(async (opts) => {
19
+ if (opts.creator) {
20
+ const existingCreator = await getCreatorToken();
21
+ if (existingCreator && !opts.force) {
22
+ console.log("A creator token is already connected. Use --creator --force to replace it.");
23
+ return;
24
+ }
25
+ await runCreatorLogin();
26
+ return;
27
+ }
12
28
  const existing = await getAuthToken();
13
29
  if (existing && !opts.force) {
14
30
  console.log("Already logged in. Use --force to re-authenticate.");
15
31
  return;
16
32
  }
17
- await doLogin();
33
+ await runLogin();
18
34
  });
19
- async function doLogin() {
20
- const sessionId = randomUUID();
21
- const loginUrl = `${GATEWAY_URL}/login?cli_session=${sessionId}`;
22
- console.log("Sign in at: " + loginUrl);
23
- console.log("");
35
+ async function readHiddenLine(prompt) {
36
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
37
+ process.stdout.write(prompt);
38
+ let value = "";
39
+ for await (const chunk of process.stdin)
40
+ value += String(chunk);
41
+ return value.trim();
42
+ }
43
+ process.stdout.write(prompt);
44
+ const input = process.stdin;
45
+ const wasRaw = input.isRaw;
46
+ input.setRawMode(true);
47
+ input.resume();
48
+ return new Promise((resolve, reject) => {
49
+ let value = "";
50
+ const finish = (error) => {
51
+ input.off("data", onData);
52
+ input.setRawMode(Boolean(wasRaw));
53
+ input.pause();
54
+ process.stdout.write("\n");
55
+ if (error)
56
+ reject(error);
57
+ else
58
+ resolve(value.trim());
59
+ };
60
+ const onData = (chunk) => {
61
+ for (const char of String(chunk)) {
62
+ if (char === "\u0003") {
63
+ finish(new Error("Creator token entry cancelled."));
64
+ return;
65
+ }
66
+ if (char === "\r" || char === "\n" || char === "\u0004") {
67
+ finish();
68
+ return;
69
+ }
70
+ if (char === "\u007f" || char === "\b") {
71
+ value = value.slice(0, -1);
72
+ }
73
+ else if (char >= " ") {
74
+ value += char;
75
+ }
76
+ }
77
+ };
78
+ input.on("data", onData);
79
+ });
80
+ }
81
+ const defaultCreatorLoginDependencies = {
82
+ openUrl: open,
83
+ readSecret: readHiddenLine,
84
+ log: console.log,
85
+ };
86
+ /**
87
+ * The browser session mints the token; the token is pasted once into a hidden
88
+ * prompt. This deliberately does not replace the core Summer login JWT.
89
+ */
90
+ export async function runCreatorLogin(overrides = {}) {
91
+ const deps = { ...defaultCreatorLoginDependencies, ...overrides };
92
+ const settingsUrl = `${await getCreatorApiUrl()}/creator/settings/tokens`;
93
+ deps.log(`Opening creator token settings: ${settingsUrl}`);
94
+ deps.log('Mint a token with the exact "publish" scope, copy its one-time sc_ value, then return here.');
24
95
  try {
25
- await open(loginUrl);
96
+ await deps.openUrl(settingsUrl);
26
97
  }
27
98
  catch {
28
- console.log("Could not open browser. Copy the URL above and open it manually.");
29
- console.log("");
99
+ deps.log("Could not open the browser. Copy the URL above and open it manually.");
30
100
  }
31
- console.log("Waiting for authentication...");
32
- const pollUrl = `${GATEWAY_URL}/api/auth/cli-login?session=${sessionId}`;
33
- const startTime = Date.now();
101
+ const token = await deps.readSecret("Creator token (input hidden): ");
102
+ await saveCreatorToken(token);
103
+ deps.log("Creator publishing connected. Your core Summer login token was not changed.");
104
+ }
105
+ const defaultDependencies = {
106
+ fetch,
107
+ openUrl: open,
108
+ randomId: randomUUID,
109
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
110
+ now: Date.now,
111
+ log: console.log,
112
+ };
113
+ export async function runLogin(overrides = {}) {
114
+ const deps = { ...defaultDependencies, ...overrides };
115
+ const gatewayUrl = await getGatewayUrl();
116
+ const sessionId = deps.randomId();
117
+ const loginUrl = `${gatewayUrl}/login?cli_session=${encodeURIComponent(sessionId)}`;
118
+ deps.log("Sign in at: " + loginUrl);
119
+ deps.log("");
120
+ try {
121
+ await deps.openUrl(loginUrl);
122
+ }
123
+ catch {
124
+ deps.log("Could not open browser. Copy the URL above and open it manually.");
125
+ deps.log("");
126
+ }
127
+ deps.log("Waiting for authentication...");
128
+ const pollUrl = `${gatewayUrl}/api/auth/cli-login?session=${encodeURIComponent(sessionId)}`;
129
+ const startTime = deps.now();
130
+ let lastHeartbeat = startTime;
34
131
  let lastError = null;
35
- while (Date.now() - startTime < POLL_TIMEOUT_MS) {
36
- await sleep(POLL_INTERVAL_MS);
132
+ while (deps.now() - startTime < POLL_TIMEOUT_MS) {
133
+ await deps.sleep(POLL_INTERVAL_MS);
134
+ if (deps.now() - lastHeartbeat >= HEARTBEAT_MS) {
135
+ lastHeartbeat = deps.now();
136
+ deps.log('Still waiting — finish signing in (or creating your account) in the browser, then click "Yes, Sign In". Same link: ' +
137
+ loginUrl);
138
+ }
37
139
  try {
38
- const res = await fetch(pollUrl, {
140
+ const res = await deps.fetch(pollUrl, {
39
141
  signal: AbortSignal.timeout(5000),
40
142
  });
41
143
  if (!res.ok) {
42
144
  if (res.status === 503) {
43
- lastError = "Server not ready (Redis/auth not configured). Try again later.";
145
+ lastError =
146
+ "The login service is unavailable (Redis/auth is not configured).";
147
+ }
148
+ else if (res.status >= 400 && res.status < 500) {
149
+ lastError = `The login request was rejected (${res.status}).`;
44
150
  }
45
151
  else if (res.status >= 500) {
46
- lastError = `Server error (${res.status}). Try again later.`;
152
+ lastError = `The login service failed (${res.status}).`;
47
153
  }
48
154
  continue;
49
155
  }
@@ -51,28 +157,21 @@ async function doLogin() {
51
157
  if (data.status === "pending")
52
158
  continue;
53
159
  if (data.status === "complete" && data.token) {
54
- await saveAuthToken(data.token);
55
- if (data.cloudToken) {
56
- await saveCloudToken(data.cloudToken);
57
- }
58
- if (data.user) {
59
- await saveUserInfo(data.user);
60
- }
61
- console.log(`\nLogged in as ${data.user?.email || "unknown"}`);
160
+ await saveLoginSession({
161
+ token: data.token,
162
+ cloudToken: data.cloudToken,
163
+ user: data.user,
164
+ scopes: data.scopes,
165
+ cloudScopes: data.cloudScopes,
166
+ });
167
+ deps.log(`\nLogged in as ${data.user?.email || "unknown"}`);
62
168
  return;
63
169
  }
170
+ lastError = "The login service returned an incomplete authentication result.";
64
171
  }
65
172
  catch (err) {
66
173
  lastError = err instanceof Error ? err.message : "Network error";
67
174
  }
68
175
  }
69
- console.error("\nLogin timed out. Please try again.");
70
- if (lastError) {
71
- console.error(`Last error: ${lastError}`);
72
- }
73
- console.error("\nDid you click \"Yes, Sign In\" in the browser after opening the URL?");
74
- process.exit(1);
75
- }
76
- function sleep(ms) {
77
- return new Promise((resolve) => setTimeout(resolve, ms));
176
+ throw new Error(`Login timed out after ${Math.round(POLL_TIMEOUT_MS / 60000)} minutes.${lastError ? ` Last error: ${lastError}` : ""} Recovery: run "summer login" again and complete "Yes, Sign In" in the browser tab it opens.`);
78
177
  }
@@ -1,23 +1,10 @@
1
1
  import { Command } from "commander";
2
- import { unlink } from "fs/promises";
3
- import { join } from "path";
4
- import { getSummerDir } from "../lib/auth.js";
2
+ import { clearAuthCredentials } from "../lib/auth.js";
5
3
  export const logoutCommand = new Command("logout")
6
4
  .description("Sign out and clear stored auth tokens")
7
5
  .action(async () => {
8
- const dir = getSummerDir();
9
- const files = ["auth-token", "cloud-token", "user.json"];
10
- let cleared = false;
11
- for (const file of files) {
12
- try {
13
- await unlink(join(dir, file));
14
- cleared = true;
15
- }
16
- catch {
17
- // File doesn't exist, that's fine
18
- }
19
- }
20
- if (cleared) {
6
+ const removed = await clearAuthCredentials();
7
+ if (removed > 0) {
21
8
  console.log("Logged out. Auth tokens cleared.");
22
9
  }
23
10
  else {
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const logsCommand: Command;
@@ -0,0 +1,19 @@
1
+ import { Command } from "commander";
2
+ import { readCreatorLogs } from "../lib/creator.js";
3
+ export const logsCommand = new Command("logs")
4
+ .description("Read creator runtime logs")
5
+ .option("--project-id <id>", "Creator project ID")
6
+ .option("--release <id>", "Restrict logs to one release")
7
+ .option("--limit <count>", "Maximum log entries to return", "100")
8
+ .action(async (opts) => {
9
+ const limit = Number.parseInt(opts.limit, 10);
10
+ if (!Number.isInteger(limit) || limit < 1 || limit > 1000) {
11
+ throw new Error("--limit must be between 1 and 1000. Recovery: pass a whole number in that range.");
12
+ }
13
+ await readCreatorLogs({
14
+ projectId: opts.projectId,
15
+ releaseId: opts.release,
16
+ limit,
17
+ face: "cli",
18
+ });
19
+ });
@@ -3,8 +3,13 @@ import { configureAgentMcp, parseAgent, parseScope, supportedAgents, } from "../
3
3
  import { startMcpServer } from "../mcp/server.js";
4
4
  export const mcpCommand = new Command("mcp")
5
5
  .description("Start the MCP server for AI tool integration (Cursor, Claude Code, etc.)")
6
- .action(async () => {
7
- await startMcpServer();
6
+ .option("--project <path>", "Bind local engine tools to the editor running this project")
7
+ .option("--instance <id>", "Bind local engine tools to one exact Summer editor instance")
8
+ .action(async (opts) => {
9
+ await startMcpServer({
10
+ projectPath: opts.project,
11
+ instanceId: opts.instance,
12
+ });
8
13
  });
9
14
  mcpCommand
10
15
  .command("setup <agent>")
@@ -13,7 +13,7 @@ export const openCommand = new Command("open")
13
13
  }
14
14
  if (!existsSync(`${fullPath}/project.godot`)) {
15
15
  console.error(`No project.godot found in ${fullPath}\n` +
16
- "This doesn't look like a Godot/Summer Engine project.\n" +
16
+ "This doesn't look like a Summer Engine project.\n" +
17
17
  "Create one with: summer create 3d-basic my-game");
18
18
  process.exit(1);
19
19
  }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const publishCommand: Command;
@@ -0,0 +1,20 @@
1
+ import { Command } from "commander";
2
+ import { publishCreator } from "../lib/creator.js";
3
+ export const publishCommand = new Command("publish")
4
+ .description("Publish a confirmed creator release")
5
+ .argument("[project]", "Project root. Defaults to the current directory.")
6
+ .requiredOption("--artifact <path>", "Exact exported Summer .pck artifact")
7
+ .requiredOption("--version <value>", "Immutable release version")
8
+ .option("--manifest <path>", "Optional JSON release manifest")
9
+ .option("--project-id <id>", "Creator project ID")
10
+ .option("--channel <name>", "Release channel")
11
+ .option("--notes <text>", "Release notes")
12
+ .option("--confirm", "Confirm the exact project and channel after reviewing them")
13
+ .action(async (project, opts) => {
14
+ const result = await publishCreator({
15
+ project,
16
+ ...opts,
17
+ face: "cli",
18
+ });
19
+ console.log(JSON.stringify(result, null, 2));
20
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const releasesCommand: Command;
@@ -0,0 +1,20 @@
1
+ import { Command } from "commander";
2
+ import { listCreatorReleases } from "../lib/creator.js";
3
+ export const releasesCommand = new Command("releases")
4
+ .description("List creator releases for a project")
5
+ .option("--project-id <id>", "Creator project ID")
6
+ .option("--limit <count>", "Maximum releases to return", "20")
7
+ .option("--cursor <value>", "Opaque cursor returned by the previous page")
8
+ .action(async (opts) => {
9
+ const limit = Number.parseInt(opts.limit, 10);
10
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
11
+ throw new Error("--limit must be between 1 and 100. Recovery: pass a whole number in that range.");
12
+ }
13
+ const result = await listCreatorReleases({
14
+ projectId: opts.projectId,
15
+ limit,
16
+ cursor: opts.cursor,
17
+ face: "cli",
18
+ });
19
+ console.log(JSON.stringify(result, null, 2));
20
+ });
@@ -100,7 +100,7 @@ export const runCommand = new Command("run")
100
100
  }
101
101
  if (!existsSync(join(fullProjectPath, "project.godot"))) {
102
102
  console.error(`No project.godot found in ${fullProjectPath}\n` +
103
- "This doesn't look like a Godot/Summer Engine project.");
103
+ "This doesn't look like a Summer Engine project.");
104
104
  process.exit(1);
105
105
  }
106
106
  }
@@ -1,3 +1,4 @@
1
+ import { type EngineSelection } from "./engine.js";
1
2
  export type EngineSnapshot = {
2
3
  ok: boolean;
3
4
  localPath?: string;
@@ -28,6 +29,15 @@ export type EngineSnapshot = {
28
29
  sceneHasCamera?: boolean;
29
30
  sceneHadLight?: boolean;
30
31
  usedSyntheticCamera?: boolean;
32
+ /** Scene-preview only: the framing the engine actually rendered with
33
+ * ("auto" resolves to "iso"). */
34
+ framing?: string;
35
+ /** Scene-preview only: path (relative to the scene root) of the node the
36
+ * camera framed when nodePath was given. */
37
+ framedNode?: string;
38
+ /** Scene-preview only: how many blank-readback retries the engine needed
39
+ * before the capture produced a non-blank frame (0 = clean first read). */
40
+ renderRetries?: number;
31
41
  /** Set true when the bound projectIdHash no longer matches the engine's live
32
42
  * health hash at capture time — the frame may be from the WRONG project
33
43
  * (item 4, client-side drift check). */
@@ -42,8 +52,9 @@ export declare class EngineApiClient {
42
52
  private port;
43
53
  private token;
44
54
  private targetIdentity;
45
- constructor(port: number, token: string, targetIdentity?: EngineTargetIdentity | string);
46
- static connect(): Promise<EngineApiClient>;
55
+ private selection;
56
+ constructor(port: number, token: string, targetIdentity?: EngineTargetIdentity | string, selection?: EngineSelection | null);
57
+ static connect(selection?: EngineSelection): Promise<EngineApiClient>;
47
58
  /** The projectIdHash this session is bound to (undefined if none was reported
48
59
  * at connect — e.g. no project open yet). */
49
60
  getBoundProjectIdHash(): string | undefined;
@@ -84,8 +95,11 @@ export declare class EngineApiClient {
84
95
  */
85
96
  executeIdentityBoundOps(ops: Record<string, unknown>[], options?: Record<string, unknown>): Promise<unknown>;
86
97
  readProjectFile(path: string, maxBytes?: number): Promise<unknown>;
87
- getSceneState(scenePath?: string): Promise<unknown>;
88
- getProjectState(): Promise<unknown>;
98
+ getSceneState(scenePath?: string, options?: {
99
+ depth?: number;
100
+ limit?: number;
101
+ }): Promise<unknown>;
102
+ getProjectState(prefix?: string): Promise<unknown>;
89
103
  getDiagnostics(): Promise<unknown>;
90
104
  inspectNode(path: string): Promise<unknown>;
91
105
  inspectResource(path: string): Promise<unknown>;
@@ -115,13 +129,18 @@ export declare class EngineApiClient {
115
129
  * Offscreen scene render via the `ScenePreview` op over /api/ops (no game
116
130
  * boot; physics/animations are static). Mirrors the web previewScene op input
117
131
  * (snapshot-tools.ts): { op:'ScenePreview', scene_path?, framing?, size?,
118
- * node_path? } — scene_path optional, engine defaults to the open scene. The
119
- * result carries image_base64 + mime (+ width/height) plus the P4.3 confession
120
- * fields (scene_has_camera / scene_had_light / used_synthetic_camera).
132
+ * node? } — scene_path optional, engine defaults to the open scene. `node` is
133
+ * the canonical focus-node arg (the engine also accepts the legacy web wire
134
+ * name `node_path`); an unresolvable node fails with failure_reason
135
+ * "node_not_found". framing "auto" is an alias of "iso"; the engine also
136
+ * accepts "top"/"front"/"back"/"left"/"right" and echoes the resolved framing.
137
+ * The result carries image_base64 + mime (+ width/height) plus the P4.3
138
+ * confession fields (scene_has_camera / scene_had_light /
139
+ * used_synthetic_camera) and framing / framed_node / render_retries.
121
140
  */
122
141
  scenePreview(input?: {
123
142
  scenePath?: string;
124
- framing?: "auto" | "top" | "front" | "iso";
143
+ framing?: "auto" | "iso" | "top" | "front" | "back" | "left" | "right";
125
144
  size?: [number, number];
126
145
  nodePath?: string;
127
146
  }): Promise<EngineSnapshot>;