summer-engine 4.3.0 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +3 -1
  3. package/.codex-plugin/plugin.json +5 -3
  4. package/.cursor-plugin/plugin.json +4 -2
  5. package/.factory-plugin/plugin.json +4 -2
  6. package/AGENTS.md +2 -2
  7. package/CHANGELOG.md +100 -0
  8. package/README.md +67 -27
  9. package/commands/start.md +19 -15
  10. package/commands/summer.md +1 -0
  11. package/dist/cli/command-usage.d.ts +3 -1
  12. package/dist/cli/command-usage.js +10 -4
  13. package/dist/cli/commands/create.d.ts +1 -25
  14. package/dist/cli/commands/create.js +10 -138
  15. package/dist/cli/commands/dev.d.ts +66 -0
  16. package/dist/cli/commands/dev.js +234 -0
  17. package/dist/cli/commands/install.d.ts +15 -58
  18. package/dist/cli/commands/install.js +31 -481
  19. package/dist/cli/commands/login.js +1 -1
  20. package/dist/cli/commands/ops.d.ts +17 -1
  21. package/dist/cli/commands/ops.js +97 -9
  22. package/dist/cli/commands/preview.d.ts +58 -0
  23. package/dist/cli/commands/preview.js +181 -0
  24. package/dist/cli/commands/run.d.ts +1 -17
  25. package/dist/cli/commands/run.js +17 -206
  26. package/dist/cli/commands/setup.js +175 -32
  27. package/dist/cli/index.js +5 -0
  28. package/dist/core/agent-host.d.ts +3 -0
  29. package/dist/core/agent-host.js +1 -1
  30. package/dist/core/api-client.d.ts +10 -2
  31. package/dist/core/api-client.js +13 -8
  32. package/dist/core/capabilities/agent-playbook.js +1 -0
  33. package/dist/core/capabilities/apps.d.ts +9 -9
  34. package/dist/core/capabilities/asset-import.d.ts +4 -1
  35. package/dist/core/capabilities/asset-import.js +15 -6
  36. package/dist/core/capabilities/capture-gameplay.js +1 -1
  37. package/dist/core/capabilities/connect-signal.d.ts +2 -2
  38. package/dist/core/capabilities/content-make/index.d.ts +81 -0
  39. package/dist/core/capabilities/content-make/index.js +706 -0
  40. package/dist/core/capabilities/content-make/media.d.ts +57 -0
  41. package/dist/core/capabilities/content-make/media.js +172 -0
  42. package/dist/core/capabilities/content-make/plan.d.ts +130 -0
  43. package/dist/core/capabilities/content-make/plan.js +433 -0
  44. package/dist/core/capabilities/content-make/render.d.ts +64 -0
  45. package/dist/core/capabilities/content-make/render.js +370 -0
  46. package/dist/core/capabilities/content-make/study.d.ts +13 -0
  47. package/dist/core/capabilities/content-make/study.js +165 -0
  48. package/dist/core/capabilities/content-make/types.d.ts +173 -0
  49. package/dist/core/capabilities/content-make/types.js +6 -0
  50. package/dist/core/capabilities/content-make/voice.d.ts +7 -0
  51. package/dist/core/capabilities/content-make/voice.js +27 -0
  52. package/dist/core/capabilities/events.d.ts +2 -2
  53. package/dist/core/capabilities/export-game.d.ts +4 -1
  54. package/dist/core/capabilities/export-game.js +4 -3
  55. package/dist/core/capabilities/export-presets.d.ts +2 -1
  56. package/dist/core/capabilities/export-presets.js +2 -1
  57. package/dist/core/capabilities/go-live.d.ts +14 -0
  58. package/dist/core/capabilities/go-live.js +408 -0
  59. package/dist/core/capabilities/image-generation.d.ts +6 -6
  60. package/dist/core/capabilities/item-manifest.d.ts +92 -0
  61. package/dist/core/capabilities/item-manifest.js +247 -0
  62. package/dist/core/capabilities/placement.d.ts +7 -7
  63. package/dist/core/capabilities/projects-local.d.ts +68 -0
  64. package/dist/core/capabilities/projects-local.js +172 -0
  65. package/dist/core/capabilities/publish-build.d.ts +13 -2
  66. package/dist/core/capabilities/publish-build.js +87 -21
  67. package/dist/core/capabilities/publish-download.js +1 -1
  68. package/dist/core/capabilities/runtime-control.d.ts +7 -2
  69. package/dist/core/capabilities/runtime-control.js +27 -1
  70. package/dist/core/capabilities/seeing/args.d.ts +2 -2
  71. package/dist/core/capabilities/store-checks.d.ts +45 -0
  72. package/dist/core/capabilities/store-checks.js +85 -0
  73. package/dist/core/capabilities/store-details.d.ts +37 -0
  74. package/dist/core/capabilities/store-details.js +94 -0
  75. package/dist/core/capabilities/summer-bundle.d.ts +21 -2
  76. package/dist/core/capabilities/summer-bundle.js +54 -7
  77. package/dist/core/capabilities/sync-items.d.ts +89 -0
  78. package/dist/core/capabilities/sync-items.js +352 -0
  79. package/dist/core/capabilities/tool-dispatch.js +66 -6
  80. package/dist/core/capabilities/video-ogv.d.ts +49 -0
  81. package/dist/core/capabilities/video-ogv.js +260 -0
  82. package/dist/core/capability-skew.js +2 -2
  83. package/dist/core/cli-usage-beacon.js +26 -2
  84. package/dist/core/cli-usage.d.ts +5 -2
  85. package/dist/core/cli-usage.js +8 -2
  86. package/dist/core/engine-install.d.ts +53 -0
  87. package/dist/core/engine-install.js +93 -0
  88. package/dist/core/engine-installer.d.ts +134 -0
  89. package/dist/core/engine-installer.js +760 -0
  90. package/dist/core/engine-launch.d.ts +126 -0
  91. package/dist/core/engine-launch.js +398 -0
  92. package/dist/core/engine-self-start.d.ts +63 -0
  93. package/dist/core/engine-self-start.js +282 -0
  94. package/dist/core/engine.d.ts +7 -1
  95. package/dist/core/engine.js +9 -2
  96. package/dist/core/error-codes.d.ts +4 -1
  97. package/dist/core/error-codes.js +7 -3
  98. package/dist/core/library-read.d.ts +2 -2
  99. package/dist/core/library-search.d.ts +2 -2
  100. package/dist/core/local-project.d.ts +50 -0
  101. package/dist/core/local-project.js +234 -0
  102. package/dist/core/ops/areas.js +9 -3
  103. package/dist/core/ops/catalog.d.ts +9 -0
  104. package/dist/core/ops/catalog.js +56 -4
  105. package/dist/core/ops/engine-answer.d.ts +4 -1
  106. package/dist/core/ops/engine-answer.js +16 -3
  107. package/dist/core/ops/engine.js +4 -4
  108. package/dist/core/ops/save-files.d.ts +6 -1
  109. package/dist/core/ops/save-files.js +67 -50
  110. package/dist/core/ops/search.js +9 -2
  111. package/dist/core/preview/input.d.ts +35 -0
  112. package/dist/core/preview/input.js +105 -0
  113. package/dist/core/preview/launch-config.d.ts +52 -0
  114. package/dist/core/preview/launch-config.js +137 -0
  115. package/dist/core/preview/page.d.ts +22 -0
  116. package/dist/core/preview/page.js +232 -0
  117. package/dist/core/preview/preview-source.d.ts +179 -0
  118. package/dist/core/preview/preview-source.js +350 -0
  119. package/dist/core/preview/server.d.ts +26 -0
  120. package/dist/core/preview/server.js +218 -0
  121. package/dist/core/telemetry.d.ts +20 -16
  122. package/dist/core/telemetry.js +92 -16
  123. package/dist/core/templates.d.ts +20 -0
  124. package/dist/core/templates.js +300 -3
  125. package/dist/core/tool-profiles.d.ts +2 -2
  126. package/dist/core/tool-profiles.js +1 -0
  127. package/dist/core/usage-fields.d.ts +41 -0
  128. package/dist/core/usage-fields.js +150 -0
  129. package/dist/core/util/sleep.d.ts +3 -0
  130. package/dist/core/util/sleep.js +12 -0
  131. package/dist/installer/agent-config.d.ts +29 -0
  132. package/dist/installer/agent-config.js +75 -11
  133. package/dist/installer/agent-detect.d.ts +44 -0
  134. package/dist/installer/agent-detect.js +166 -0
  135. package/dist/installer/agent-table.d.ts +19 -0
  136. package/dist/installer/agent-table.js +130 -0
  137. package/dist/installer/setup-error.d.ts +19 -0
  138. package/dist/installer/setup-error.js +34 -0
  139. package/dist/installer/version-check.d.ts +5 -0
  140. package/dist/installer/version-check.js +21 -0
  141. package/dist/mcp/ops-surface.d.ts +7 -0
  142. package/dist/mcp/ops-surface.js +40 -3
  143. package/dist/mcp/server.d.ts +14 -4
  144. package/dist/mcp/server.js +42 -8
  145. package/dist/mcp/tool-surface.d.ts +8 -0
  146. package/dist/mcp/tool-surface.js +19 -2
  147. package/dist/mcp/tools/creator-tools.js +92 -14
  148. package/dist/mcp/tools/debug-tools.js +2 -0
  149. package/dist/mcp/tools/local-project-tools.d.ts +6 -0
  150. package/dist/mcp/tools/local-project-tools.js +33 -0
  151. package/dist/mcp/tools/navigation-tools.js +2 -1
  152. package/dist/mcp/tools/project-tools.js +5 -8
  153. package/dist/mcp/tools/seeing-tools.js +1 -1
  154. package/dist/mcp/tools/visual-tools.js +1 -1
  155. package/dist/mcp/tools/with-engine.js +1 -1
  156. package/dist/project-memory/agent-instructions.js +11 -3
  157. package/gemini-extension.json +3 -1
  158. package/library/references/mcp-tools-reference/mcp-tools-reference.md +18 -5
  159. package/library/skills/README.md +12 -8
  160. package/library/skills/animated-loop/SKILL.md +10 -16
  161. package/library/skills/cinematic-cutscene/SKILL.md +7 -18
  162. package/library/skills/headless-scripting/SKILL.md +7 -4
  163. package/library/skills/in-game-purchases/SKILL.md +25 -8
  164. package/library/skills/in-game-purchases/resource.yaml +2 -1
  165. package/library/skills/launch-ready/SKILL.md +351 -0
  166. package/library/skills/launch-ready/resource.yaml +45 -0
  167. package/library/skills/make-content/SKILL.md +202 -0
  168. package/library/skills/make-content/resource.yaml +32 -0
  169. package/library/skills/make-game/SKILL.md +6 -4
  170. package/library/skills/make-game/resource.yaml +2 -1
  171. package/library/skills/navigate-summer/SKILL.md +4 -2
  172. package/library/skills/new-project/SKILL.md +15 -22
  173. package/library/skills/new-project/resource.yaml +2 -1
  174. package/library/skills/painted-25d-world/SKILL.md +366 -0
  175. package/library/skills/painted-25d-world/_building-blocks/blender-route.md +33 -0
  176. package/library/skills/painted-25d-world/_building-blocks/blockout_builder.gd +149 -0
  177. package/library/skills/painted-25d-world/_building-blocks/capture_paint_view.gd +32 -0
  178. package/library/skills/painted-25d-world/_building-blocks/paint_projection.gdshader +23 -0
  179. package/library/skills/painted-25d-world/_building-blocks/paintcam_check.gd +25 -0
  180. package/library/skills/painted-25d-world/_building-blocks/painted_door.gd +17 -0
  181. package/library/skills/painted-25d-world/_building-blocks/painted_location.gd +56 -0
  182. package/library/skills/painted-25d-world/_building-blocks/painted_player.gd +125 -0
  183. package/library/skills/painted-25d-world/_building-blocks/painted_world.gd +111 -0
  184. package/library/skills/painted-25d-world/_building-blocks/tools/cam.py +57 -0
  185. package/library/skills/painted-25d-world/_building-blocks/tools/detail4k.py +81 -0
  186. package/library/skills/painted-25d-world/_building-blocks/tools/overlay.py +17 -0
  187. package/library/skills/painted-25d-world/_building-blocks/tools/preview.py +60 -0
  188. package/library/skills/painted-25d-world/resource.yaml +39 -0
  189. package/library/skills/publish-your-game/SKILL.md +13 -14
  190. package/library/skills/publish-your-game/resource.yaml +4 -3
  191. package/library/skills/release-checklist/SKILL.md +188 -0
  192. package/library/skills/release-checklist/resource.yaml +40 -0
  193. package/library/skills/release-readiness/SKILL.md +1 -1
  194. package/library/skills/release-readiness/resource.yaml +1 -1
  195. package/library/skills/store-art/SKILL.md +1 -1
  196. package/library/skills/store-art/resource.yaml +1 -1
  197. package/library/skills/store-listing/SKILL.md +1 -1
  198. package/library/skills/store-listing/resource.yaml +1 -1
  199. package/library/skills/summer-cli/SKILL.md +2 -0
  200. package/library/skills/summer-store/SKILL.md +295 -39
  201. package/library/skills/summer-store/resource.yaml +9 -3
  202. package/library/skills/trailer-shot/SKILL.md +3 -3
  203. package/library/templates/README.md +5 -0
  204. package/library/tools/content-make/resource.yaml +132 -0
  205. package/library/tools/frame-shot/GUIDE.md +1 -1
  206. package/library/tools/go-live/resource.yaml +67 -0
  207. package/library/tools/import-from-url/GUIDE.md +1 -0
  208. package/library/tools/import-from-url-batch/GUIDE.md +1 -1
  209. package/library/tools/play/GUIDE.md +2 -0
  210. package/library/tools/projects-create-local/resource.yaml +55 -0
  211. package/library/tools/projects-open-local/resource.yaml +51 -0
  212. package/library/tools/publish-build/resource.yaml +3 -3
  213. package/library/tools/screenshot/GUIDE.md +1 -1
  214. package/library/tools/store-set-details/resource.yaml +114 -0
  215. package/library/tools/sync-items/resource.yaml +72 -0
  216. package/package.json +1 -1
  217. package/registry/generated/aliases.json +6 -0
  218. package/registry/generated/counts.json +3 -3
  219. package/registry/generated/gemini-extension.json +3 -1
  220. package/registry/generated/index.json +463 -37
  221. package/registry/generated/marketplace.claude.json +1 -1
  222. package/registry/generated/plugin.claude-directory.json +4 -2
  223. package/registry/generated/plugin.claude.json +3 -1
  224. package/registry/generated/plugin.codex.json +5 -3
  225. package/registry/generated/plugin.cursor.json +4 -2
  226. package/registry/generated/plugin.factory.json +4 -2
  227. package/registry/generated/server.json +2 -2
  228. package/registry/generated/skills-index.md +12 -8
  229. package/registry/generated/skills-registry.json +62 -5
  230. package/registry/operations/legacy-names.json +40 -0
  231. package/registry/operations/public-catalog.json +10144 -387
@@ -0,0 +1,126 @@
1
+ import { type EngineHealth } from "./engine.js";
2
+ import { ENGINE_BIN_ENV } from "./engine-install.js";
3
+ import { type LaunchPosture } from "./launch-posture.js";
4
+ /** How long `summer run` waits for the local API after a launch. */
5
+ export declare const RUN_WAIT_MS = 20000;
6
+ /** spawn_failed's code when the editor closed cleanly before its API answered, with no successor: not a broken install. */
7
+ export declare const CLOSED_EARLY = "exited with code 0";
8
+ /** Another launch holds ~/.summer/launch.lock: it is starting an editor right now. */
9
+ export declare class LaunchLockBusyError extends Error {
10
+ constructor();
11
+ }
12
+ /** Hold ~/.summer/launch.lock while `fn` runs. A launch that waits longer
13
+ * than the stale age calls `heartbeat` so other launchers keep waiting. */
14
+ export declare function withLaunchLock<T>(fn: (heartbeat: () => Promise<void>) => Promise<T>, maxWaitMs?: number): Promise<T>;
15
+ export type RunBinaryResolution = {
16
+ binary: string;
17
+ source: "--bin" | typeof ENGINE_BIN_ENV | "installed";
18
+ } | {
19
+ binary: null;
20
+ error: string;
21
+ };
22
+ /**
23
+ * Which engine executable a launch uses (and probes with --help):
24
+ * `--bin <path>`, else SUMMER_BIN, else the installed engine
25
+ * (findEngineBinary, which also honours the older SUMMER_ENGINE_BINARY).
26
+ * An explicit override is never silently swapped for the installed engine:
27
+ * a missing path or a bare `.app` bundle is an error, because the caller
28
+ * asked for THAT build.
29
+ */
30
+ export declare function resolveRunBinary(binFlag: string | undefined, env?: NodeJS.ProcessEnv): RunBinaryResolution;
31
+ /** The editor the last `summer dev` / `summer run` / MCP launch started: ~/.summer/last-launch.json. */
32
+ export declare const LAST_LAUNCH_FILE = "last-launch.json";
33
+ export interface LastLaunch {
34
+ projectPath: string;
35
+ pid: number;
36
+ at: number;
37
+ /** The folder the launch ran in (`summer dev` makes the project below it). */
38
+ cwd?: string;
39
+ }
40
+ /** Past this the launched editor has registered, or it never will. */
41
+ export declare const LAST_LAUNCH_MAX_MS: number;
42
+ /** The last launch while it is recent and its editor process is alive, else
43
+ * null; a stale record is deleted, so a reused pid never blocks a project. */
44
+ export declare function lastLaunch(now?: number): LastLaunch | null;
45
+ export type OpenEditor = {
46
+ kind: "none";
47
+ } | {
48
+ kind: "this";
49
+ port: number;
50
+ health: EngineHealth;
51
+ }
52
+ /** An editor process has this project but does not answer: busy, or still importing. */
53
+ | {
54
+ kind: "starting";
55
+ port: number | null;
56
+ pid?: number;
57
+ } | {
58
+ kind: "other";
59
+ port: number;
60
+ health: EngineHealth;
61
+ project: string;
62
+ };
63
+ /**
64
+ * Whether an editor already has this project open, has another project open,
65
+ * is starting on it, or nothing answers. Live editors publish
66
+ * ~/.summer/instances/<id>.json with their project root; an entry whose
67
+ * process is alive counts as this project's editor even when it does not
68
+ * answer (a long import), and so does the last launch while its process
69
+ * lives (it registers only once it is up). Older engines publish only the
70
+ * global port pointer, whose /api/health does not say which project is open
71
+ * (so any editor there counts as "another project" unless no project was asked for).
72
+ */
73
+ export declare function findOpenEditor(projectPath: string | null): Promise<OpenEditor>;
74
+ /** Whether an editor process was started on this project (its command line
75
+ * has `--path <project> --editor`, or `-e` after the editor restarts
76
+ * itself), whether or not it answers yet. */
77
+ export declare function editorStartingFor(projectPath: string): boolean;
78
+ export interface LaunchOutput {
79
+ log: (line: string) => void;
80
+ error: (line: string) => void;
81
+ }
82
+ export interface LaunchEditorOptions {
83
+ /** Directory holding project.godot, or null for a bare editor (project manager). */
84
+ projectPath: string | null;
85
+ posture: LaunchPosture;
86
+ /** `--bin`; else SUMMER_BIN, else the installed engine (resolveRunBinary). */
87
+ bin?: string;
88
+ /** How long to wait for the local API after launching. Default RUN_WAIT_MS. */
89
+ waitMs?: number;
90
+ /** One "still starting" line this often while waiting; 0 = none. */
91
+ progressEveryMs?: number;
92
+ /** An absolute end (ms) for the whole launch, lock wait and --help probe
93
+ * included (the MCP's per-call deadline); the API wait is what is left. */
94
+ deadline?: number;
95
+ out: LaunchOutput;
96
+ }
97
+ export type LaunchEditorResult = {
98
+ status: "running";
99
+ launched: boolean;
100
+ port: number;
101
+ health: EngineHealth;
102
+ pid?: number;
103
+ } | {
104
+ status: "other_project";
105
+ project: string;
106
+ } | {
107
+ status: "no_binary";
108
+ error: string;
109
+ } | {
110
+ status: "spawn_failed";
111
+ binary: string;
112
+ code: string;
113
+ }
114
+ /** Launched (or found starting), but the API did not answer within waitMs (a first import can take minutes). */
115
+ | {
116
+ status: "starting";
117
+ binary: string | null;
118
+ pid?: number;
119
+ };
120
+ /**
121
+ * Open the project in an editor unless one already has it: refuse a second
122
+ * editor while another project is open, launch with the posture's flags
123
+ * (direct executable, detached), and poll /api/health until it answers or
124
+ * waitMs passes. Prints what it does through `out`; the caller words the end.
125
+ */
126
+ export declare function launchEditor(options: LaunchEditorOptions): Promise<LaunchEditorResult>;
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Launch Summer Engine on a project and wait until its local API answers:
3
+ * the one launch path behind `summer run`, `summer dev` and the MCP's
4
+ * self-start (src/mcp/server.ts getClient), so all three agree on the
5
+ * posture (launch-posture.ts), the launch lock, and when an editor counts as
6
+ * already open.
7
+ */
8
+ import { execFileSync, spawn } from "child_process";
9
+ import { readFileSync, realpathSync, rmSync } from "fs";
10
+ import { mkdir, readFile, rm, writeFile } from "fs/promises";
11
+ import { join, resolve } from "path";
12
+ import { getSummerDir } from "./auth.js";
13
+ import { writeStoreJson } from "./store.js";
14
+ import { checkEngineHealth, getApiPort, listEngineInstances } from "./engine.js";
15
+ import { describeEngineExecutableProblem, ENGINE_BIN_ENV, engineBinaryOverride, findEngineBinary, } from "./engine-install.js";
16
+ import { advertisedBackgroundPosture, backgroundLaunchSupport, detectBackgroundLaunchSupport, planLaunch, BACKGROUND_LAUNCH_FLAG, BACKGROUND_LAUNCH_MIN_ENGINE_VERSION, } from "./launch-posture.js";
17
+ import { processIsAlive } from "./util/process.js";
18
+ import { sleep } from "./util/sleep.js";
19
+ const LAUNCH_LOCK_STALE_MS = 60_000;
20
+ const LAUNCH_LOCK_WAIT_MS = 15_000;
21
+ /** How long `summer run` waits for the local API after a launch. */
22
+ export const RUN_WAIT_MS = 20_000;
23
+ /** spawn_failed's code when the editor closed cleanly before its API answered, with no successor: not a broken install. */
24
+ export const CLOSED_EARLY = "exited with code 0";
25
+ async function removeStaleLaunchLock(lockDir) {
26
+ const ownerPath = join(lockDir, "owner.json");
27
+ try {
28
+ const raw = await readFile(ownerPath, "utf-8");
29
+ const owner = JSON.parse(raw);
30
+ const pid = typeof owner.pid === "number" ? owner.pid : 0;
31
+ const createdAt = typeof owner.createdAt === "number" ? owner.createdAt : 0;
32
+ const staleByTime = Date.now() - createdAt > LAUNCH_LOCK_STALE_MS;
33
+ const staleByPid = !processIsAlive(pid);
34
+ if (!staleByTime && !staleByPid) {
35
+ return false;
36
+ }
37
+ }
38
+ catch {
39
+ // Broken owner metadata should not permanently block launches.
40
+ }
41
+ await rm(lockDir, { recursive: true, force: true });
42
+ return true;
43
+ }
44
+ /** Another launch holds ~/.summer/launch.lock: it is starting an editor right now. */
45
+ export class LaunchLockBusyError extends Error {
46
+ constructor() {
47
+ super("Another `summer run` launch is still in progress. Try again in a few seconds.");
48
+ this.name = "LaunchLockBusyError";
49
+ }
50
+ }
51
+ /** Hold ~/.summer/launch.lock while `fn` runs. A launch that waits longer
52
+ * than the stale age calls `heartbeat` so other launchers keep waiting. */
53
+ export async function withLaunchLock(fn, maxWaitMs = LAUNCH_LOCK_WAIT_MS) {
54
+ const summerDir = getSummerDir();
55
+ await mkdir(summerDir, { recursive: true, mode: 0o700 });
56
+ const lockDir = join(summerDir, "launch.lock");
57
+ const ownerPath = join(lockDir, "owner.json");
58
+ const startedAt = Date.now();
59
+ const writeOwner = () => writeFile(ownerPath, JSON.stringify({ pid: process.pid, createdAt: Date.now() }, null, 2), {
60
+ encoding: "utf-8",
61
+ mode: 0o600,
62
+ });
63
+ while (true) {
64
+ try {
65
+ await mkdir(lockDir, { mode: 0o700 });
66
+ try {
67
+ await writeOwner();
68
+ return await fn(() => writeOwner().catch(() => undefined));
69
+ }
70
+ finally {
71
+ await rm(lockDir, { recursive: true, force: true });
72
+ }
73
+ }
74
+ catch (error) {
75
+ const code = error.code;
76
+ if (code !== "EEXIST") {
77
+ throw error;
78
+ }
79
+ if (await removeStaleLaunchLock(lockDir)) {
80
+ continue;
81
+ }
82
+ if (Date.now() - startedAt >= maxWaitMs) {
83
+ throw new LaunchLockBusyError();
84
+ }
85
+ await sleep(100);
86
+ }
87
+ }
88
+ }
89
+ /**
90
+ * Which engine executable a launch uses (and probes with --help):
91
+ * `--bin <path>`, else SUMMER_BIN, else the installed engine
92
+ * (findEngineBinary, which also honours the older SUMMER_ENGINE_BINARY).
93
+ * An explicit override is never silently swapped for the installed engine:
94
+ * a missing path or a bare `.app` bundle is an error, because the caller
95
+ * asked for THAT build.
96
+ */
97
+ export function resolveRunBinary(binFlag, env = process.env) {
98
+ const fromEnv = engineBinaryOverride(env);
99
+ const explicit = binFlag?.trim()
100
+ ? { source: "--bin", path: binFlag.trim() }
101
+ : fromEnv?.name === ENGINE_BIN_ENV
102
+ ? { source: ENGINE_BIN_ENV, path: fromEnv.path }
103
+ : null;
104
+ if (explicit) {
105
+ const path = resolve(explicit.path);
106
+ const problem = describeEngineExecutableProblem(path, explicit.source);
107
+ return problem ? { binary: null, error: problem } : { binary: path, source: explicit.source };
108
+ }
109
+ const installed = findEngineBinary();
110
+ if (!installed) {
111
+ return {
112
+ binary: null,
113
+ error: "Summer Engine not found. Install it first:\n" +
114
+ " summer install\n" +
115
+ " or download from https://summerengine.com/download\n" +
116
+ `To launch a build that is not installed, pass --bin <executable> or set ${ENGINE_BIN_ENV}.`,
117
+ };
118
+ }
119
+ return { binary: installed, source: "installed" };
120
+ }
121
+ function canonical(path) {
122
+ try {
123
+ return realpathSync(path);
124
+ }
125
+ catch {
126
+ return resolve(path);
127
+ }
128
+ }
129
+ /** The editor the last `summer dev` / `summer run` / MCP launch started: ~/.summer/last-launch.json. */
130
+ export const LAST_LAUNCH_FILE = "last-launch.json";
131
+ /** Past this the launched editor has registered, or it never will. */
132
+ export const LAST_LAUNCH_MAX_MS = 10 * 60_000;
133
+ /** The last launch while it is recent and its editor process is alive, else
134
+ * null; a stale record is deleted, so a reused pid never blocks a project. */
135
+ export function lastLaunch(now = Date.now()) {
136
+ const path = join(getSummerDir(), LAST_LAUNCH_FILE);
137
+ try {
138
+ const record = JSON.parse(readFileSync(path, "utf-8"));
139
+ if (typeof record.projectPath === "string" && typeof record.pid === "number" && typeof record.at === "number") {
140
+ if (now - record.at <= LAST_LAUNCH_MAX_MS && processIsAlive(record.pid))
141
+ return record;
142
+ }
143
+ rmSync(path, { force: true });
144
+ }
145
+ catch {
146
+ // no record
147
+ }
148
+ return null;
149
+ }
150
+ /** Forget the last launch when it names this editor process (it exited). */
151
+ function forgetLaunch(pid) {
152
+ const path = join(getSummerDir(), LAST_LAUNCH_FILE);
153
+ try {
154
+ if (JSON.parse(readFileSync(path, "utf-8")).pid === pid)
155
+ rmSync(path, { force: true });
156
+ }
157
+ catch {
158
+ // no record
159
+ }
160
+ }
161
+ /**
162
+ * Whether an editor already has this project open, has another project open,
163
+ * is starting on it, or nothing answers. Live editors publish
164
+ * ~/.summer/instances/<id>.json with their project root; an entry whose
165
+ * process is alive counts as this project's editor even when it does not
166
+ * answer (a long import), and so does the last launch while its process
167
+ * lives (it registers only once it is up). Older engines publish only the
168
+ * global port pointer, whose /api/health does not say which project is open
169
+ * (so any editor there counts as "another project" unless no project was asked for).
170
+ */
171
+ export async function findOpenEditor(projectPath) {
172
+ if (projectPath) {
173
+ const root = canonical(projectPath);
174
+ const instances = await listEngineInstances(Date.now(), getSummerDir(), { anyHeartbeat: true });
175
+ for (const instance of instances.filter((i) => i.resourceRoot === root)) {
176
+ const health = await checkEngineHealth(instance.port);
177
+ return health ? { kind: "this", port: instance.port, health } : { kind: "starting", port: instance.port, pid: instance.pid };
178
+ }
179
+ const last = lastLaunch();
180
+ if (last && canonical(last.projectPath) === root)
181
+ return { kind: "starting", port: null, pid: last.pid };
182
+ if (editorStartingFor(root))
183
+ return { kind: "starting", port: null };
184
+ for (const instance of instances) {
185
+ const health = await checkEngineHealth(instance.port);
186
+ if (!health)
187
+ continue;
188
+ const project = instance.projectName ? `${instance.projectName} (${instance.resourceRoot})` : instance.resourceRoot;
189
+ return { kind: "other", port: instance.port, health, project };
190
+ }
191
+ }
192
+ const port = await getApiPort();
193
+ const health = await checkEngineHealth(port);
194
+ if (!health)
195
+ return { kind: "none" };
196
+ const running = health.project_path ? resolve(health.project_path) : null;
197
+ if (!projectPath || running === resolve(projectPath))
198
+ return { kind: "this", port, health };
199
+ return { kind: "other", port, health, project: health.project_path ?? health.project_name ?? "unknown" };
200
+ }
201
+ /** Whether an editor process was started on this project (its command line
202
+ * has `--path <project> --editor`, or `-e` after the editor restarts
203
+ * itself), whether or not it answers yet. */
204
+ export function editorStartingFor(projectPath) {
205
+ return editorPidFor(projectPath) !== null;
206
+ }
207
+ /** The pid of an editor process started on this project, or null (none; Windows has no pgrep). */
208
+ function editorPidFor(projectPath) {
209
+ if (process.platform === "win32")
210
+ return null;
211
+ const pattern = `--path ${projectPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} (--editor|-e)( |$)`;
212
+ try {
213
+ const pid = Number(execFileSync("pgrep", ["-f", "--", pattern], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim().split(/\s+/)[0]);
214
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
215
+ }
216
+ catch {
217
+ return null; // pgrep exits 1 when nothing matches
218
+ }
219
+ }
220
+ /** The editor that took over the project after the launched one exited cleanly (it restarted itself): a live registered instance, or an editor process on the project. */
221
+ async function successorFor(projectPath, exited) {
222
+ const root = canonical(projectPath);
223
+ const instance = (await listEngineInstances(Date.now(), getSummerDir(), { anyHeartbeat: true })).find((candidate) => candidate.resourceRoot === root && candidate.pid !== exited);
224
+ if (instance)
225
+ return instance.pid;
226
+ const pid = editorPidFor(root);
227
+ return pid !== null && pid !== exited ? pid : null;
228
+ }
229
+ /** How long a clean exit of the launched editor may be a restart before it counts as a failed start. */
230
+ const RESTART_GRACE_MS = 5_000;
231
+ /**
232
+ * Open the project in an editor unless one already has it: refuse a second
233
+ * editor while another project is open, launch with the posture's flags
234
+ * (direct executable, detached), and poll /api/health until it answers or
235
+ * waitMs passes. Prints what it does through `out`; the caller words the end.
236
+ */
237
+ export async function launchEditor(options) {
238
+ const { out, posture, deadline } = options;
239
+ // The canonical path: the same form the registry, the pgrep check and the spawn use.
240
+ const projectPath = options.projectPath ? canonical(options.projectPath) : null;
241
+ const progressEveryMs = options.progressEveryMs ?? 0;
242
+ const lockWaitMs = deadline !== undefined ? Math.min(LAUNCH_LOCK_WAIT_MS, Math.max(0, deadline - Date.now())) : LAUNCH_LOCK_WAIT_MS;
243
+ return withLaunchLock(async (heartbeat) => {
244
+ // Poll /api/health (the instance's port when known, else the pointer)
245
+ // until the editor answers or waitMs passes; `onUp` adds the caller's lines.
246
+ const waitForApi = async (port, failed, onUp) => {
247
+ const startTime = Date.now();
248
+ const end = deadline ?? startTime + (options.waitMs ?? RUN_WAIT_MS);
249
+ let nextProgress = startTime + progressEveryMs;
250
+ let nextHeartbeat = startTime + 10_000;
251
+ while (Date.now() < end) {
252
+ await sleep(500);
253
+ const failure = await failed();
254
+ if (failure)
255
+ return failure;
256
+ const probe = port ?? (await getApiPort());
257
+ const health = await checkEngineHealth(probe);
258
+ if (health) {
259
+ out.log(`Summer Engine running (v${health.version}) on port ${probe}`);
260
+ if (health.project_name) {
261
+ out.log(` Project: ${health.project_name}`);
262
+ }
263
+ onUp(health);
264
+ return { status: "running", launched: true, port: probe, health };
265
+ }
266
+ if (progressEveryMs > 0 && Date.now() >= nextProgress) {
267
+ out.log(` Still starting (${Math.round((Date.now() - startTime) / 1000)} s); the first open imports the project's assets...`);
268
+ nextProgress += progressEveryMs;
269
+ }
270
+ if (Date.now() >= nextHeartbeat) {
271
+ nextHeartbeat += 10_000;
272
+ await heartbeat();
273
+ }
274
+ }
275
+ return await failed();
276
+ };
277
+ const open = await findOpenEditor(projectPath);
278
+ if (open.kind === "this") {
279
+ out.log(`Summer Engine is already running (v${open.health.version}) on port ${open.port}`);
280
+ if (open.health.project_name)
281
+ out.log(` Project: ${open.health.project_name}`);
282
+ if (open.health.project_path)
283
+ out.log(` Path: ${open.health.project_path}`);
284
+ return { status: "running", launched: false, port: open.port, health: open.health };
285
+ }
286
+ if (open.kind === "other") {
287
+ out.log(`Summer Engine is already running (v${open.health.version}) on port ${open.port}`);
288
+ out.log(` Current project: ${open.project}`);
289
+ out.error(`Refusing to launch a second editor for: ${projectPath}\n` +
290
+ "Close or switch the current Summer Engine project first, then run this command again.");
291
+ return { status: "other_project", project: open.project };
292
+ }
293
+ // An editor already has this project but does not answer yet (importing,
294
+ // or busy): wait for it, never start a second one.
295
+ if (open.kind === "starting") {
296
+ out.log(`Summer Engine is already starting with ${projectPath}; waiting for it...`);
297
+ const waited = await waitForApi(open.port, async () => null, () => { });
298
+ return waited?.status === "running" ? { ...waited, launched: false } : (waited ?? { status: "starting", binary: null, pid: open.pid });
299
+ }
300
+ const resolved = resolveRunBinary(options.bin);
301
+ if (resolved.binary === null) {
302
+ out.error(resolved.error);
303
+ return { status: "no_binary", error: resolved.error };
304
+ }
305
+ const binary = resolved.binary;
306
+ if (resolved.source !== "installed") {
307
+ out.log(`Using engine from ${resolved.source}: ${binary}`);
308
+ }
309
+ // The engine is not running yet, so /api/health cannot tell us whether
310
+ // it honours --summer-background. Ask THAT binary itself (`--help`, which
311
+ // exits headless before any window exists; cached per install) with the
312
+ // installed version as pre-check and fallback; pass the flag only when
313
+ // the engine is known to support it. Focus launches have nothing to decide.
314
+ const support = posture === "background" ? await detectBackgroundLaunchSupport(binary) : backgroundLaunchSupport(null);
315
+ const plan = planLaunch(posture, support);
316
+ const args = ["--editor", ...plan.extraArgs];
317
+ if (projectPath) {
318
+ args.unshift("--path", projectPath);
319
+ }
320
+ if (plan.note)
321
+ out.log(plan.note);
322
+ out.log(plan.background
323
+ ? "Launching Summer Engine in the background (the window will not take focus until you click it)..."
324
+ : "Launching Summer Engine...");
325
+ // Direct executable, never `open -a`: LaunchServices activates whatever
326
+ // it opens and would defeat --summer-background regardless of flags.
327
+ const child = spawn(binary, args, { detached: true, stdio: "ignore" });
328
+ // A stale or non-executable binary surfaces as an async "error" event
329
+ // (ENOENT/EACCES). Without a listener Node raises it as an uncaught
330
+ // exception with a raw stack; catch it and report the path instead. An
331
+ // editor that exits with an error before its API answers (no display, a
332
+ // crash on open, killed by the system) ends the wait at once.
333
+ // A clean exit (code 0) may be the editor restarting itself: the new
334
+ // process for the project is followed, and only an exit with no
335
+ // successor within RESTART_GRACE_MS counts as a failed start.
336
+ const spawnState = { error: null, exit: null, cleanExitAt: null };
337
+ child.on("error", (error) => {
338
+ spawnState.error = error;
339
+ });
340
+ child.on("exit", (code, signal) => {
341
+ if (code === 0 && !signal) {
342
+ spawnState.cleanExitAt = Date.now();
343
+ return;
344
+ }
345
+ spawnState.exit = signal ? `killed by ${signal}` : `exited with code ${code}`;
346
+ if (child.pid)
347
+ forgetLaunch(child.pid);
348
+ });
349
+ child.unref();
350
+ if (projectPath && child.pid) {
351
+ // Other launchers (another `summer dev`, the MCP) wait for this editor
352
+ // until it registers, and the MCP finds the project through it.
353
+ await writeStoreJson(LAST_LAUNCH_FILE, { projectPath, pid: child.pid, at: Date.now(), cwd: canonical(process.cwd()) }).catch(() => undefined);
354
+ }
355
+ let editorPid = child.pid;
356
+ const spawnFailed = async () => {
357
+ if (spawnState.cleanExitAt !== null) {
358
+ const successor = projectPath ? await successorFor(projectPath, child.pid) : null;
359
+ if (successor && projectPath) {
360
+ // It restarted itself: wait for the new process, and let other launchers know it.
361
+ spawnState.cleanExitAt = null;
362
+ editorPid = successor;
363
+ await writeStoreJson(LAST_LAUNCH_FILE, { projectPath, pid: successor, at: Date.now(), cwd: canonical(process.cwd()) }).catch(() => undefined);
364
+ return null;
365
+ }
366
+ if (Date.now() - spawnState.cleanExitAt < RESTART_GRACE_MS)
367
+ return null;
368
+ if (child.pid)
369
+ forgetLaunch(child.pid);
370
+ out.error(`Summer Engine closed before it opened ${projectPath ?? "the editor"} (exit code 0). ` +
371
+ "Run the same command again; if it closes again, open the project in Summer Engine to see why.");
372
+ return { status: "spawn_failed", binary, code: CLOSED_EARLY };
373
+ }
374
+ const code = spawnState.error ? (spawnState.error.code ?? spawnState.error.message) : spawnState.exit;
375
+ if (!code)
376
+ return null;
377
+ out.error(`Summer Engine binary failed to start: ${code} (${binary})\n` +
378
+ "Re-run 'summer install' to repair the installation, or check the binary path.");
379
+ return { status: "spawn_failed", binary, code };
380
+ };
381
+ const result = await waitForApi(null, spawnFailed, (health) => {
382
+ // Background was wanted but the flag was withheld, and the running
383
+ // engine now says (capabilities.launchPostures, else its version)
384
+ // that it would have honoured it: say so, so the next launch can be
385
+ // fixed rather than silently repeating a focus launch.
386
+ if (posture === "background" && !plan.background) {
387
+ const advert = advertisedBackgroundPosture(health.capabilities);
388
+ const wouldHonour = advert ?? backgroundLaunchSupport(health.version).supported;
389
+ if (wouldHonour) {
390
+ out.log(` Note: this engine (v${health.version}) ${advert ? "advertises" : "should support"} background launches (${BACKGROUND_LAUNCH_FLAG}, ${BACKGROUND_LAUNCH_MIN_ENGINE_VERSION}+), but that could not be confirmed before launch on this platform.`);
391
+ }
392
+ }
393
+ });
394
+ if (result?.status === "running")
395
+ return { ...result, pid: editorPid };
396
+ return result ?? { status: "starting", binary, pid: editorPid };
397
+ }, lockWaitMs);
398
+ }
@@ -0,0 +1,63 @@
1
+ import { EngineApiClient } from "./api-client.js";
2
+ import { type EngineSelection } from "./engine.js";
3
+ import { launchEditor } from "./engine-launch.js";
4
+ import { installEngine } from "./engine-installer.js";
5
+ import { EngineNotRunningError } from "./error-codes.js";
6
+ export declare const AUTO_LAUNCH_ENV = "SUMMER_AUTO_LAUNCH";
7
+ /** One tool call ends within this, all steps included (under a 60 s client timeout). */
8
+ export declare const SELF_START_WAIT_MS = 40000;
9
+ export declare const DEV_COMMAND = "npx -y summer-engine@latest dev";
10
+ export declare const SELF_START_RECOVERY: {
11
+ notInstalledHere: string;
12
+ notInstalled: string;
13
+ noProject: string;
14
+ busy: string;
15
+ launchBusy: string;
16
+ noDisplay: string;
17
+ otherProject: (open: string, wanted: string) => string;
18
+ starting: (root: string) => string;
19
+ installing: (line: string) => string;
20
+ closedEarly: (root: string) => string;
21
+ launchFailed: (code: string) => string;
22
+ autoLaunchOff: (root: string) => string;
23
+ };
24
+ export interface SelfStartDeps {
25
+ launch?: typeof launchEditor;
26
+ connect?: (selection: EngineSelection | undefined) => Promise<EngineApiClient>;
27
+ install?: typeof installEngine;
28
+ log?: (line: string) => void;
29
+ waitMs?: number;
30
+ env?: NodeJS.ProcessEnv;
31
+ now?: () => number;
32
+ }
33
+ /** Test-only: forget the last answer only (withEngine's immediate retry window). */
34
+ export declare function _resetRecentForTests(): void;
35
+ /** Test-only: forget the launches and the install this process made. */
36
+ export declare function _resetSelfStartForTests(): void;
37
+ /**
38
+ * The project this session works on: --project, else the folder the agent
39
+ * started in, else the project the last `summer dev` / `summer run` opened
40
+ * from this folder (it makes a subfolder the agent did not start in), while
41
+ * that launch is recent, its editor lives and the project is still there.
42
+ */
43
+ export declare function sessionProject(selection: EngineSelection | undefined): Promise<string | null>;
44
+ /** Linux with no display server: an editor window cannot open (containers, SSH, cloud agents). */
45
+ export declare function hasNoDisplay(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
46
+ export interface OpenProjectOptions {
47
+ /** Install a missing engine: only after the person said yes. */
48
+ install?: boolean;
49
+ /** The error to build answers from (a failed connect), or the op's own. */
50
+ notRunning?: EngineNotRunningError;
51
+ }
52
+ /**
53
+ * Open `root` in the editor and return a connected client: launch it unless
54
+ * an editor has it, wait, and never launch it twice. Throws an
55
+ * EngineNotRunningError whose recovery is the one next step for the case.
56
+ */
57
+ export declare function openProjectInEditor(root: string, selection: EngineSelection | undefined, options?: OpenProjectOptions, deps?: SelfStartDeps): Promise<EngineApiClient>;
58
+ /**
59
+ * Open the session's project and return a connected client, or throw an
60
+ * EngineNotRunningError whose recovery is the one next step for this case.
61
+ * Never installs: that needs the person's yes (projects.open_local).
62
+ */
63
+ export declare function selfStartEngine(selection: EngineSelection | undefined, notRunning: EngineNotRunningError, deps?: SelfStartDeps): Promise<EngineApiClient>;