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,760 @@
1
+ /**
2
+ * Installing Summer Engine: `summer install`, the engine step of `summer
3
+ * dev`, and the MCP's `projects.open_local` (with the person's yes). Never
4
+ * exits the process; every failure is an EngineInstallError whose reason is
5
+ * the usage line's error code. The `summer install` command is
6
+ * src/cli/commands/install.ts.
7
+ */
8
+ import { execFileSync, execSync } from "child_process";
9
+ import { createInterface } from "node:readline/promises";
10
+ import { createHash } from "crypto";
11
+ import { accessSync, chmodSync, constants as fsConstants, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statfsSync, statSync, symlinkSync, writeFileSync, } from "fs";
12
+ import { pipeline } from "stream/promises";
13
+ import { Readable } from "stream";
14
+ import { homedir, tmpdir, platform } from "os";
15
+ import { basename, dirname, join, resolve } from "path";
16
+ import { getSummerDir } from "./store.js";
17
+ import { processIsAlive } from "./util/process.js";
18
+ import { sleep } from "./util/sleep.js";
19
+ import { DEFAULT_DOWNLOAD_BYTES, ENGINE_BINARY_ENV, EngineInstallError, INSTALL_RETRY, INSTALL_SPACE_FACTOR, LINUX_ENGINE_BINARY_NAME, asInstallError, checkDiskSpace, chooseMacDestination, errorDetail, formatSize, recordEngineInstall, windowsEnginePaths, findExtractedEngineBinary, linuxArchiveKind, linuxEngineInstallDir, pickLinuxReleaseAsset, resolveLinuxInstallSource, } from "./engine-install.js";
20
+ export { EngineInstallError };
21
+ const RELEASES_URL = "https://www.summerengine.com/api/desktop/releases";
22
+ const DOWNLOAD_PAGE = "https://summerengine.com/download";
23
+ /** Guard a download URL from the releases endpoint. A missing/empty URL (an
24
+ * outdated or misconfigured endpoint) otherwise becomes a cryptic
25
+ * fetch(undefined) failure — surface it clearly with a manual fallback. */
26
+ export function requireDownloadUrl(value, label) {
27
+ if (!value || value.trim().length === 0) {
28
+ throw new EngineInstallError("release_info", `Release info is missing the ${label} download URL; the releases endpoint may be ` +
29
+ `outdated. Download manually from ${DOWNLOAD_PAGE}`);
30
+ }
31
+ return value;
32
+ }
33
+ /** Verify a downloaded file's sha256 against the expected hash from the releases
34
+ * endpoint. Skipped when no expected hash is published. Catches a truncated or
35
+ * corrupted download (a ~700 MB install over a flaky network) before it becomes a
36
+ * confusing DMG-mount / installer failure downstream. */
37
+ export function verifyChecksum(actual, expected, label) {
38
+ if (!expected || expected.trim().length === 0)
39
+ return; // nothing published to verify against
40
+ if (actual.toLowerCase() !== expected.toLowerCase()) {
41
+ throw new EngineInstallError("checksum", `${label} checksum mismatch: the download was corrupted or incomplete ` +
42
+ `(expected ${expected.slice(0, 12)}…, got ${actual.slice(0, 12)}…). Run: ${INSTALL_RETRY}`);
43
+ }
44
+ }
45
+ // The bundle-version readers live in core (src/core/launch-posture.ts) because
46
+ // `summer run` needs the installed version BEFORE the engine is up to decide
47
+ // whether it may pass --summer-background; re-exported here for install.test.ts.
48
+ import { parseMacBundleVersion, readMacBundleVersion } from "./launch-posture.js";
49
+ export { parseMacBundleVersion, readMacBundleVersion };
50
+ export function isSameVersion(installed, latest) {
51
+ if (!installed)
52
+ return false;
53
+ const norm = (value) => value.trim().replace(/^v/i, "");
54
+ return norm(installed) === norm(latest);
55
+ }
56
+ /** Decide what `summer install` may do about an existing /Applications/Summer.app.
57
+ * Replacing an installed engine is destructive, so it needs --yes or a TTY
58
+ * prompt; an equal version is a no-op success. */
59
+ export function planMacInstall(input) {
60
+ if (!input.exists)
61
+ return { action: "install" };
62
+ if (isSameVersion(input.installedVersion, input.latestVersion)) {
63
+ return { action: "up-to-date", version: input.latestVersion };
64
+ }
65
+ if (input.yes)
66
+ return { action: "install" };
67
+ if (input.isTTY)
68
+ return { action: "needs-confirmation", installed: input.installedVersion };
69
+ return {
70
+ action: "refuse",
71
+ reason: `Summer Engine ${input.installedVersion ?? "(unknown version)"} is already installed; ` +
72
+ `updating to v${input.latestVersion} replaces it. To confirm, run: ${input.retry ?? INSTALL_RETRY} --yes`,
73
+ };
74
+ }
75
+ /** Replace destApp with the bundle on the mounted DMG WITHOUT a window where
76
+ * no engine exists: copy to <destApp>.new first, and only after that
77
+ * succeeds remove the old bundle and rename the staged copy into place. A
78
+ * failed copy leaves the existing install untouched. */
79
+ export function replaceMacApp(mountPoint, destApp, exec, staged = `${destApp}.new`) {
80
+ exec(`rm -rf "${staged}"`);
81
+ try {
82
+ exec(`cp -R "${mountPoint}/Summer.app" "${staged}"`);
83
+ }
84
+ catch (err) {
85
+ try {
86
+ exec(`rm -rf "${staged}"`);
87
+ }
88
+ catch { /* best effort */ }
89
+ throw err;
90
+ }
91
+ exec(`rm -rf "${destApp}"`);
92
+ exec(`mv "${staged}" "${destApp}"`);
93
+ }
94
+ /** The mount point hdiutil attach printed (the /Volumes/... column), or null. */
95
+ export function parseMountPoint(hdiutilOutput) {
96
+ return (hdiutilOutput
97
+ .split("\n")
98
+ .filter((line) => line.includes("/Volumes/"))
99
+ .map((line) => line.trim().split("\t").pop()?.trim())
100
+ .find(Boolean) ?? null);
101
+ }
102
+ /** Mount the DMG and return its mount point; any failure is `mount`. */
103
+ export function attachDmg(dmgPath, exec) {
104
+ let output;
105
+ try {
106
+ output = exec(`hdiutil attach "${dmgPath}" -nobrowse -noverify -noautoopen`);
107
+ }
108
+ catch (err) {
109
+ throw asInstallError(err, "mount", (detail) => `Could not open the downloaded DMG (${detail}). Download it from ${DOWNLOAD_PAGE} and drag Summer to Applications.`);
110
+ }
111
+ const mountPoint = parseMountPoint(output);
112
+ if (!mountPoint) {
113
+ throw new EngineInstallError("mount", `The downloaded DMG opened without a mount point. Download it from ${DOWNLOAD_PAGE} and drag Summer to Applications.`);
114
+ }
115
+ return mountPoint;
116
+ }
117
+ /** Eject the DMG, best effort: a busy volume ("resource busy") gets one retry
118
+ * with -force, and a failure here never fails an install that already
119
+ * succeeded. Returns whether it ejected. */
120
+ export function detachDmg(mountPoint, exec) {
121
+ for (const command of [`hdiutil detach "${mountPoint}" -quiet`, `hdiutil detach "${mountPoint}" -quiet -force`]) {
122
+ try {
123
+ exec(command);
124
+ return true;
125
+ }
126
+ catch {
127
+ // retry once with -force, then give up
128
+ }
129
+ }
130
+ return false;
131
+ }
132
+ /** Whether the engine executable at this path is running: pgrep on a pattern
133
+ * anchored to the start of the command line, so a process that only mentions
134
+ * the path in its arguments does not count. pgrep exits 1 on no match. */
135
+ export function isEngineRunning(binary, execFile) {
136
+ const pattern = `^${binary.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}( |$)`;
137
+ try {
138
+ return execFile("pgrep", ["-f", pattern]).trim().length > 0;
139
+ }
140
+ catch {
141
+ return false;
142
+ }
143
+ }
144
+ /** Remove partial downloads / staged bundles when the user interrupts. The
145
+ * returned function removes the handlers; every install path calls it. */
146
+ function cleanupOnSignal(paths) {
147
+ const handler = () => {
148
+ for (const path of paths) {
149
+ try {
150
+ rmSync(path, { recursive: true, force: true });
151
+ }
152
+ catch { /* best effort */ }
153
+ }
154
+ process.exit(130);
155
+ };
156
+ process.on("SIGINT", handler);
157
+ process.on("SIGTERM", handler);
158
+ return () => {
159
+ process.off("SIGINT", handler);
160
+ process.off("SIGTERM", handler);
161
+ };
162
+ }
163
+ async function confirm(question) {
164
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
165
+ try {
166
+ const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
167
+ return answer === "y" || answer === "yes";
168
+ }
169
+ finally {
170
+ rl.close();
171
+ }
172
+ }
173
+ /** Velopack Setup.exe install args. Velopack is NOT NSIS: silent install is
174
+ * `--silent` and a custom directory is `--installto <dir>` (per Velopack's
175
+ * setup.exe CLI). The old NSIS flags (/S, /D=) are rejected by the Velopack
176
+ * installer with "unexpected argument '/S' found" — this is the wizard bug. */
177
+ export function windowsInstallerArgs(customPath) {
178
+ return customPath ? `--silent --installto "${customPath}"` : "--silent";
179
+ }
180
+ /** Printed after an install: the first start of a new engine is slow and silent. */
181
+ export const FIRST_START_NOTE = "The first start of a new engine version can take several minutes with no output " +
182
+ "(the system checks the new app and the engine builds its caches). Later starts are fast.";
183
+ /** Where the Velopack installer put Summer.exe: <dir>/current/Summer.exe for --path, else the default locations. */
184
+ export function windowsInstalledBinary(customPath, exists = existsSync) {
185
+ const candidates = customPath
186
+ ? [join(customPath, "current", "Summer.exe"), join(customPath, "Summer.exe")]
187
+ : windowsEnginePaths();
188
+ return candidates.find((path) => exists(path)) ?? null;
189
+ }
190
+ /**
191
+ * Download progress: rewrites one line on a terminal; elsewhere (an agent,
192
+ * a log file) one line per 10%, so a 700 MB download is about ten lines
193
+ * instead of hundreds of kilobytes.
194
+ */
195
+ export function createProgressReporter(isTTY, write) {
196
+ let lastStep = -1;
197
+ return (downloaded, total) => {
198
+ if (total <= 0)
199
+ return;
200
+ const pct = Math.min(100, Math.floor((downloaded / total) * 100));
201
+ const mb = (downloaded / 1024 / 1024).toFixed(1);
202
+ if (isTTY) {
203
+ write(`\r ${pct}% (${mb}MB)`);
204
+ return;
205
+ }
206
+ const step = Math.floor(pct / 10);
207
+ if (step > lastStep) {
208
+ lastStep = step;
209
+ write(` ${pct}% (${mb}MB)\n`);
210
+ }
211
+ };
212
+ }
213
+ /** The nearest folder that exists at or above `path`. */
214
+ function existingAncestor(path) {
215
+ let dir = resolve(path);
216
+ while (!existsSync(dir) && dirname(dir) !== dir)
217
+ dir = dirname(dir);
218
+ return dir;
219
+ }
220
+ function freeSpaceOf(path) {
221
+ const dir = existingAncestor(path);
222
+ const stats = statfsSync(dir);
223
+ return { bytes: Number(stats.bavail) * Number(stats.bsize), volume: String(statSync(dir).dev) };
224
+ }
225
+ /** Whether this user may create or replace `path` (judged on the nearest existing folder). */
226
+ function isWritable(path) {
227
+ try {
228
+ accessSync(existingAncestor(path), fsConstants.W_OK);
229
+ return true;
230
+ }
231
+ catch {
232
+ return false;
233
+ }
234
+ }
235
+ function defaultInstallDeps() {
236
+ return {
237
+ platform: platform(),
238
+ arch: process.arch,
239
+ exec: (command, inherit) => {
240
+ if (!inherit)
241
+ return execSync(command, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
242
+ execSync(command, { stdio: "inherit" });
243
+ return "";
244
+ },
245
+ execFile: (file, args) => execFileSync(file, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }),
246
+ fetch: (input, init) => fetch(input, init),
247
+ freeSpace: freeSpaceOf,
248
+ canWrite: isWritable,
249
+ tmpDir: tmpdir(),
250
+ home: homedir(),
251
+ env: process.env,
252
+ confirm,
253
+ record: recordEngineInstall,
254
+ stdoutIsTTY: Boolean(process.stdout.isTTY),
255
+ };
256
+ }
257
+ /**
258
+ * Install Summer Engine for this platform: `summer install`, and the engine
259
+ * step of `summer dev`. Never exits the process and asks on stdin only when
260
+ * `isTTY` is true; every failure is an EngineInstallError whose reason is the
261
+ * telemetry code and whose message ends in one next step.
262
+ */
263
+ export async function installEngine(opts, deps = {}) {
264
+ const d = { ...defaultInstallDeps(), ...deps };
265
+ const log = opts.log ?? ((line) => console.log(line));
266
+ // `summer install`, `summer dev --yes` and projects.open_local can start at
267
+ // once: one install at a time; the next one then finds the engine in place.
268
+ return withInstallLock(log, () => installUnlocked(opts, d, log));
269
+ }
270
+ /** ~/.summer/install.lock: held while one install runs. A holder whose
271
+ * process is gone, or that is hours old, is stale and removed. */
272
+ export const INSTALL_LOCK_DIR = "install.lock";
273
+ const INSTALL_LOCK_STALE_MS = 2 * 60 * 60_000;
274
+ async function withInstallLock(log, fn) {
275
+ const lockDir = join(getSummerDir(), INSTALL_LOCK_DIR);
276
+ mkdirSync(dirname(lockDir), { recursive: true, mode: 0o700 });
277
+ let said = false;
278
+ while (true) {
279
+ try {
280
+ mkdirSync(lockDir, { mode: 0o700 });
281
+ }
282
+ catch (err) {
283
+ if (err.code !== "EEXIST")
284
+ throw err;
285
+ const owner = lockOwner(lockDir);
286
+ if (!owner)
287
+ continue; // released meanwhile
288
+ if ((owner.pid && !processIsAlive(owner.pid)) || Date.now() - owner.createdAt > INSTALL_LOCK_STALE_MS) {
289
+ takeOverStaleLock(lockDir, owner);
290
+ continue;
291
+ }
292
+ if (!said)
293
+ log("Another Summer Engine install is running; waiting for it to finish...");
294
+ said = true;
295
+ await sleep(1_000);
296
+ continue;
297
+ }
298
+ try {
299
+ writeFileSync(join(lockDir, "owner.json"), JSON.stringify({ pid: process.pid, createdAt: Date.now() }), { mode: 0o600 });
300
+ return await fn();
301
+ }
302
+ finally {
303
+ rmSync(lockDir, { recursive: true, force: true });
304
+ }
305
+ }
306
+ }
307
+ /** Who holds the lock: owner.json, or (just made, not written yet) the folder's age. Null when it is gone. */
308
+ function lockOwner(lockDir) {
309
+ try {
310
+ const owner = JSON.parse(readFileSync(join(lockDir, "owner.json"), "utf-8"));
311
+ if (typeof owner.pid === "number" && typeof owner.createdAt === "number")
312
+ return { pid: owner.pid, createdAt: owner.createdAt };
313
+ }
314
+ catch {
315
+ // just created by its holder, or broken: judged by age
316
+ }
317
+ try {
318
+ return { pid: 0, createdAt: statSync(lockDir).mtimeMs };
319
+ }
320
+ catch {
321
+ return null;
322
+ }
323
+ }
324
+ /**
325
+ * Remove a stale lock so that only one waiter wins: rename it aside (one
326
+ * rename of a folder succeeds), then check that the folder moved is still
327
+ * the stale one judged. If a live holder took the lock in between, put its
328
+ * lock back.
329
+ */
330
+ export function takeOverStaleLock(lockDir, stale) {
331
+ const aside = `${lockDir}.stale-${process.pid}-${Date.now()}`;
332
+ try {
333
+ renameSync(lockDir, aside);
334
+ }
335
+ catch {
336
+ return; // another waiter took it over first, or it was released
337
+ }
338
+ const moved = lockOwner(aside);
339
+ if (moved && moved.pid === stale.pid && moved.createdAt === stale.createdAt) {
340
+ rmSync(aside, { recursive: true, force: true });
341
+ return;
342
+ }
343
+ try {
344
+ renameSync(aside, lockDir);
345
+ }
346
+ catch {
347
+ rmSync(aside, { recursive: true, force: true });
348
+ }
349
+ }
350
+ async function installUnlocked(opts, d, log) {
351
+ if (d.platform === "linux")
352
+ return installLinux(opts, d, log);
353
+ if (d.platform !== "darwin" && d.platform !== "win32") {
354
+ throw new EngineInstallError("unsupported_platform", `Summer Engine does not install on ${d.platform}; it runs on macOS, Windows and Linux (x86_64). ` +
355
+ `See ${DOWNLOAD_PAGE}`);
356
+ }
357
+ log("Fetching latest release info...");
358
+ let releases;
359
+ try {
360
+ const res = await d.fetch(RELEASES_URL, { signal: AbortSignal.timeout(10000) });
361
+ if (!res.ok)
362
+ throw new Error(`HTTP ${res.status}`);
363
+ releases = (await res.json());
364
+ if (!releases?.latest)
365
+ throw new Error("no latest release in the answer");
366
+ }
367
+ catch (err) {
368
+ throw new EngineInstallError("release_info", `Could not fetch the Summer Engine release info (${errorDetail(err)}). Download the engine from ${DOWNLOAD_PAGE}`);
369
+ }
370
+ return d.platform === "darwin" ? installMac(releases, opts, d, log) : installWindows(releases, opts, d, log);
371
+ }
372
+ /** The command that repeats this install. */
373
+ function retryCommand(opts) {
374
+ return opts.path ? `${INSTALL_RETRY} --path "${opts.path}"` : INSTALL_RETRY;
375
+ }
376
+ function nextSteps(log) {
377
+ log("\nNext steps:");
378
+ log(" summer dev # Create a starter project and open it");
379
+ }
380
+ /** Start a download: the open response and its size (0 when the server sends none). */
381
+ async function openDownload(url, fetchFn) {
382
+ let res;
383
+ try {
384
+ res = await fetchFn(url);
385
+ }
386
+ catch (err) {
387
+ throw new EngineInstallError("download", `Download failed: ${errorDetail(err)}. Check your connection, then run: ${INSTALL_RETRY}`);
388
+ }
389
+ if (!res.ok || !res.body) {
390
+ throw new EngineInstallError("download", `Download failed: HTTP ${res.status}. Check your connection, then run: ${INSTALL_RETRY}`);
391
+ }
392
+ return { res, bytes: parseInt(res.headers.get("content-length") || "0", 10) || 0 };
393
+ }
394
+ /** Refuse before writing a byte when the download or the install would not fit. */
395
+ function requireSpace(res, needs, freeSpace) {
396
+ try {
397
+ checkDiskSpace(needs, freeSpace);
398
+ }
399
+ catch (err) {
400
+ void res.body?.cancel().catch(() => { });
401
+ throw err;
402
+ }
403
+ }
404
+ /** Stream the body to disk, hashing as it goes. */
405
+ async function saveDownload(res, dest, report) {
406
+ mkdirSync(dirname(dest), { recursive: true });
407
+ const fileStream = createWriteStream(dest);
408
+ const readable = Readable.fromWeb(res.body);
409
+ // Hash while streaming (the 'data' listener and the pipe both see every chunk
410
+ // in flowing mode), so we get integrity for free without a second read pass.
411
+ const hash = createHash("sha256");
412
+ let downloaded = 0;
413
+ const total = parseInt(res.headers.get("content-length") || "0", 10);
414
+ readable.on("data", (chunk) => {
415
+ hash.update(chunk);
416
+ downloaded += chunk.length;
417
+ report(downloaded, total);
418
+ });
419
+ await pipeline(readable, fileStream);
420
+ return { sha256: hash.digest("hex") };
421
+ }
422
+ /** Download, check the space first, verify the hash. The file is removed on failure. */
423
+ async function downloadVerified(url, dest, needs, label, expectedSha256, d, log) {
424
+ try {
425
+ const { res, bytes } = await openDownload(url, d.fetch);
426
+ requireSpace(res, needs(bytes || DEFAULT_DOWNLOAD_BYTES), d.freeSpace);
427
+ log(`Downloading ${label}${bytes ? ` (${formatSize(bytes)})` : ""}...`);
428
+ const report = createProgressReporter(d.stdoutIsTTY, d.stdoutIsTTY ? (text) => process.stdout.write(text) : (text) => log(text.replace(/\n$/, "")));
429
+ const { sha256 } = await saveDownload(res, dest, report);
430
+ if (d.stdoutIsTTY)
431
+ process.stdout.write("\n");
432
+ verifyChecksum(sha256, expectedSha256, label);
433
+ }
434
+ catch (err) {
435
+ try {
436
+ rmSync(dest, { force: true });
437
+ }
438
+ catch { /* best effort */ }
439
+ throw asInstallError(err, "download", (detail) => `The download stopped (${detail}). Check your connection, then run: ${INSTALL_RETRY}`);
440
+ }
441
+ }
442
+ async function recordInstall(d, binary, version, log) {
443
+ try {
444
+ await d.record(binary, version);
445
+ }
446
+ catch (err) {
447
+ // The engine is installed; only the pointer for later commands is missing.
448
+ log(`Note: could not record the install (${errorDetail(err)}); set SUMMER_BIN=${binary} if summer cannot find it.`);
449
+ }
450
+ }
451
+ async function installMac(releases, opts, d, log) {
452
+ const info = releases.latest.macos;
453
+ if (!info) {
454
+ throw new EngineInstallError("release_info", `The release info lists no macOS build. Download the engine from ${DOWNLOAD_PAGE}`);
455
+ }
456
+ const dmgUrl = requireDownloadUrl(info.dmg_url, "macOS DMG");
457
+ log(`Latest version: ${info.version}`);
458
+ const canReplace = (app) => d.canWrite(dirname(app)) && d.canWrite(app);
459
+ const { destApp, fallback } = chooseMacDestination({ customPath: opts.path, home: d.home, canReplace });
460
+ if (!canReplace(destApp)) {
461
+ throw new EngineInstallError("permission", `This user cannot write to ${existsSync(destApp) ? destApp : dirname(destApp)}. ` +
462
+ `Install into a folder you own: ${INSTALL_RETRY} --path <folder>`);
463
+ }
464
+ if (fallback)
465
+ log(`/Applications is not writable for this user, so the engine goes to ${destApp}.`);
466
+ const destBinary = join(destApp, "Contents", "MacOS", "Summer");
467
+ const exists = existsSync(destApp);
468
+ const installedVersion = exists ? readMacBundleVersion(destApp) : null;
469
+ const plan = planMacInstall({
470
+ exists,
471
+ installedVersion,
472
+ latestVersion: info.version,
473
+ yes: Boolean(opts.yes),
474
+ isTTY: Boolean(opts.isTTY),
475
+ retry: retryCommand(opts),
476
+ });
477
+ if (plan.action === "up-to-date") {
478
+ await recordInstall(d, destBinary, plan.version, log);
479
+ log(`Summer Engine v${plan.version} is already installed at ${destApp} (up to date).`);
480
+ return { binary: destBinary, version: plan.version, destination: destApp, alreadyInstalled: true };
481
+ }
482
+ if (plan.action === "refuse")
483
+ throw new EngineInstallError("needs_confirmation", plan.reason);
484
+ if (plan.action === "needs-confirmation") {
485
+ log(`Summer Engine ${plan.installed ?? "(unknown version)"} is installed at ${destApp}.`);
486
+ if (!(await d.confirm(`Replace it with v${info.version}?`))) {
487
+ log("Cancelled. Nothing was changed.");
488
+ return { binary: destBinary, version: installedVersion, destination: destApp, alreadyInstalled: true };
489
+ }
490
+ }
491
+ const refuseWhileRunning = () => {
492
+ if (existsSync(destApp) && isEngineRunning(destBinary, d.execFile)) {
493
+ throw new EngineInstallError("app_running", `Summer Engine is running from ${destApp}, so it cannot be replaced now. ` +
494
+ `Quit Summer Engine, then run: ${INSTALL_RETRY} --yes`);
495
+ }
496
+ };
497
+ refuseWhileRunning();
498
+ // Names of this process's own: an install another process left behind never shares them.
499
+ const dmgPath = join(d.tmpDir, `Summer-v${info.version}-${process.pid}.dmg`);
500
+ const staged = `${destApp}.new-${process.pid}`;
501
+ const releaseSignals = cleanupOnSignal([dmgPath, staged]);
502
+ try {
503
+ await downloadVerified(dmgUrl, dmgPath, (bytes) => [
504
+ { path: d.tmpDir, bytes },
505
+ { path: dirname(destApp), bytes: bytes * INSTALL_SPACE_FACTOR },
506
+ ], `Summer Engine v${info.version} DMG`, info.dmg_sha256, d, log);
507
+ log("Mounting DMG...");
508
+ const mountPoint = attachDmg(dmgPath, d.exec);
509
+ try {
510
+ // Again after the download: the person may have opened Summer meanwhile.
511
+ refuseWhileRunning();
512
+ log(`Installing to ${destApp}...`);
513
+ mkdirSync(dirname(destApp), { recursive: true });
514
+ // A staged copy an interrupted install left behind: its process is gone (`.new-<pid>`, or the older `.new`).
515
+ for (const name of readdirSync(dirname(destApp))) {
516
+ const left = new RegExp(`^${basename(destApp).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.new(?:-(\\d+))?$`).exec(name);
517
+ if (left && (!left[1] || !processIsAlive(Number(left[1]))))
518
+ rmSync(join(dirname(destApp), name), { recursive: true, force: true });
519
+ }
520
+ replaceMacApp(mountPoint, destApp, (command) => { d.exec(command); }, staged);
521
+ }
522
+ catch (err) {
523
+ throw asInstallError(err, "copy", (detail) => `Could not copy Summer.app to ${destApp} (${detail}); ` +
524
+ `${exists ? "the existing engine was left in place" : "nothing was installed"}. ` +
525
+ `Install it by hand from ${DOWNLOAD_PAGE}`);
526
+ }
527
+ finally {
528
+ if (!detachDmg(mountPoint, d.exec))
529
+ log(`Note: could not eject ${mountPoint}; eject it in Finder later.`);
530
+ }
531
+ }
532
+ finally {
533
+ try {
534
+ rmSync(dmgPath, { force: true });
535
+ }
536
+ catch { /* best effort */ }
537
+ releaseSignals();
538
+ }
539
+ log("Done!");
540
+ await recordInstall(d, destBinary, info.version, log);
541
+ log(`\nSummer Engine v${info.version} installed to ${destApp}`);
542
+ log(FIRST_START_NOTE);
543
+ nextSteps(log);
544
+ return { binary: destBinary, version: info.version, destination: destApp, alreadyInstalled: false };
545
+ }
546
+ async function installWindows(releases, opts, d, log) {
547
+ const info = releases.latest.windows;
548
+ if (!info) {
549
+ throw new EngineInstallError("release_info", `The release info lists no Windows build. Download the engine from ${DOWNLOAD_PAGE}`);
550
+ }
551
+ const exeUrl = requireDownloadUrl(info.url, "Windows installer");
552
+ log(`Latest version: ${info.version}`);
553
+ const installDir = opts.path ?? join(d.env.LOCALAPPDATA ?? d.home, "SummerEngine");
554
+ if (!d.canWrite(installDir)) {
555
+ throw new EngineInstallError("permission", `This user cannot write to ${installDir}. Install into a folder you own: ${INSTALL_RETRY} --path <folder>`);
556
+ }
557
+ const exePath = join(d.tmpDir, `Summer-v${info.version}-${process.pid}.exe`);
558
+ const releaseSignals = cleanupOnSignal([exePath]);
559
+ try {
560
+ await downloadVerified(exeUrl, exePath, (bytes) => [
561
+ { path: d.tmpDir, bytes },
562
+ { path: installDir, bytes: bytes * INSTALL_SPACE_FACTOR },
563
+ ], `Summer Engine v${info.version} installer`, info.sha256, d, log);
564
+ }
565
+ finally {
566
+ releaseSignals();
567
+ }
568
+ log("Running installer...");
569
+ try {
570
+ d.exec(`"${exePath}" ${windowsInstallerArgs(opts.path)}`, true);
571
+ }
572
+ catch (err) {
573
+ // The installer stays on disk so the next step can name it.
574
+ throw asInstallError(err, "installer", (detail) => `The Summer Engine installer failed (${detail}). Run it yourself: ${exePath}`);
575
+ }
576
+ try {
577
+ rmSync(exePath, { force: true });
578
+ }
579
+ catch { /* best effort */ }
580
+ const installed = windowsInstalledBinary(opts.path);
581
+ if (installed)
582
+ await recordInstall(d, installed, info.version, log);
583
+ log(`\nSummer Engine v${info.version} installed${installed ? ` to ${installed}` : "!"}`);
584
+ log(FIRST_START_NOTE);
585
+ nextSteps(log);
586
+ return {
587
+ binary: installed,
588
+ version: info.version,
589
+ destination: installed ? dirname(installed) : installDir,
590
+ alreadyInstalled: false,
591
+ };
592
+ }
593
+ // ------------------------------------------------------------------- Linux
594
+ // Source resolution, release picking, and archive detection live in
595
+ // core/engine-install.ts; this section is the process I/O around them.
596
+ function walkFiles(dir) {
597
+ const out = [];
598
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
599
+ const path = join(dir, entry.name);
600
+ if (entry.isDirectory())
601
+ out.push(...walkFiles(path));
602
+ else if (entry.isFile())
603
+ out.push(path);
604
+ }
605
+ return out;
606
+ }
607
+ /** Register an existing binary the same way a download install does: the
608
+ * canonical ~/.summer/engine/summer-linux-x86_64 path points at it, so
609
+ * `summer run`/`summer doctor` find it even when the env var is not set. */
610
+ function registerExistingLinuxBinary(sourcePath, destDir, destBinary, log) {
611
+ const absolute = resolve(sourcePath);
612
+ if (!existsSync(absolute)) {
613
+ throw new EngineInstallError("copy", `${ENGINE_BINARY_ENV} points at ${absolute}, but nothing exists there. ` +
614
+ `Set it to the engine binary, then run: ${INSTALL_RETRY}`);
615
+ }
616
+ try {
617
+ chmodSync(absolute, 0o755);
618
+ }
619
+ catch {
620
+ // Best effort — a read-only mount still works if already executable.
621
+ }
622
+ try {
623
+ mkdirSync(destDir, { recursive: true });
624
+ if (absolute !== destBinary) {
625
+ rmSync(destBinary, { force: true });
626
+ symlinkSync(absolute, destBinary);
627
+ }
628
+ }
629
+ catch (err) {
630
+ throw asInstallError(err, "copy", (detail) => `Could not link ${destBinary} to ${absolute} (${detail}). Run: ${INSTALL_RETRY} --path <folder>`);
631
+ }
632
+ log(`Registered existing engine binary: ${absolute}`);
633
+ log(` -> ${destBinary}`);
634
+ }
635
+ async function fetchLinuxReleaseAsset(apiUrl, fetchFn) {
636
+ const res = await fetchFn(apiUrl, {
637
+ headers: { Accept: "application/vnd.github+json" },
638
+ signal: AbortSignal.timeout(15000),
639
+ });
640
+ if (!res.ok)
641
+ throw new Error(`HTTP ${res.status}`);
642
+ return pickLinuxReleaseAsset(await res.json());
643
+ }
644
+ /** The published .sha256 for a Linux artifact; a missing one skips the check, as before. */
645
+ async function fetchLinuxChecksum(sha256Url, fetchFn) {
646
+ if (!sha256Url)
647
+ return undefined;
648
+ try {
649
+ const res = await fetchFn(sha256Url, { signal: AbortSignal.timeout(15000) });
650
+ return res.ok ? (await res.text()).trim().split(/\s+/, 1)[0] : undefined;
651
+ }
652
+ catch (err) {
653
+ throw new EngineInstallError("download", `Could not fetch the Linux artifact checksum (${errorDetail(err)}). Check your connection, then run: ${INSTALL_RETRY}`);
654
+ }
655
+ }
656
+ async function installLinux(opts, d, log) {
657
+ if (d.arch !== "x64") {
658
+ throw new EngineInstallError("unsupported_platform", `Linux ${d.arch} has no published Summer Engine artifact yet (x86_64 only). ` +
659
+ `Build from source, set ${ENGINE_BINARY_ENV} to the binary, then run: ${INSTALL_RETRY}`);
660
+ }
661
+ const destDir = opts.path ? resolve(opts.path) : linuxEngineInstallDir();
662
+ const destBinary = join(destDir, LINUX_ENGINE_BINARY_NAME);
663
+ const source = resolveLinuxInstallSource(d.env);
664
+ if (source.kind === "binary") {
665
+ registerExistingLinuxBinary(source.path, destDir, destBinary, log);
666
+ await recordInstall(d, destBinary, null, log);
667
+ printLinuxNextSteps(destBinary, log);
668
+ return { binary: destBinary, version: null, destination: destDir, alreadyInstalled: true };
669
+ }
670
+ if (!d.canWrite(destDir)) {
671
+ throw new EngineInstallError("permission", `This user cannot write to ${destDir}. Install into a folder you own: ${INSTALL_RETRY} --path <folder>`);
672
+ }
673
+ let url = source.kind === "url" ? source.url : "";
674
+ let sha256Url;
675
+ let versionLabel = "custom";
676
+ if (source.kind === "release") {
677
+ log("Fetching latest Linux release info...");
678
+ try {
679
+ const asset = await fetchLinuxReleaseAsset(source.apiUrl, d.fetch);
680
+ url = asset.url;
681
+ sha256Url = asset.sha256Url;
682
+ versionLabel = asset.version;
683
+ }
684
+ catch (err) {
685
+ throw new EngineInstallError("release_info", `Could not resolve a Linux release artifact (${errorDetail(err)}). See ${DOWNLOAD_PAGE}`);
686
+ }
687
+ log(`Latest Linux release: ${versionLabel}`);
688
+ }
689
+ const kind = linuxArchiveKind(url);
690
+ const stamp = Date.now();
691
+ const downloadPath = join(d.tmpDir, `summer-engine-${stamp}.${kind === "binary" ? "bin" : kind === "zip" ? "zip" : "tar.gz"}`);
692
+ const extractDir = join(d.tmpDir, `summer-engine-extract-${stamp}`);
693
+ const releaseSignals = cleanupOnSignal([downloadPath, extractDir]);
694
+ try {
695
+ const expected = await fetchLinuxChecksum(sha256Url, d.fetch);
696
+ await downloadVerified(url, downloadPath, (bytes) => [
697
+ { path: d.tmpDir, bytes },
698
+ { path: destDir, bytes: bytes * INSTALL_SPACE_FACTOR },
699
+ ], "Linux artifact", expected, d, log);
700
+ let extractedBinary = downloadPath;
701
+ if (kind !== "binary") {
702
+ try {
703
+ mkdirSync(extractDir, { recursive: true });
704
+ d.exec(kind === "tar.gz"
705
+ ? `tar -xzf "${downloadPath}" -C "${extractDir}"`
706
+ : `unzip -oq "${downloadPath}" -d "${extractDir}"`);
707
+ }
708
+ catch (err) {
709
+ throw asInstallError(err, "download", (detail) => `Could not unpack the Linux artifact (${detail}). Run: ${INSTALL_RETRY}`);
710
+ }
711
+ const found = findExtractedEngineBinary(walkFiles(extractDir));
712
+ if (!found) {
713
+ throw new EngineInstallError("download", `The downloaded archive contains no engine binary (expected ${LINUX_ENGINE_BINARY_NAME}). See ${DOWNLOAD_PAGE}`);
714
+ }
715
+ extractedBinary = found;
716
+ }
717
+ // Replace, never edit in place: an engine reading its own binary mid-write
718
+ // is worse than a moment with no file. rename first (same tmpfs), copy as
719
+ // the cross-device fallback.
720
+ try {
721
+ mkdirSync(destDir, { recursive: true });
722
+ rmSync(destBinary, { force: true });
723
+ try {
724
+ renameSync(extractedBinary, destBinary);
725
+ }
726
+ catch {
727
+ d.exec(`cp "${extractedBinary}" "${destBinary}"`);
728
+ }
729
+ chmodSync(destBinary, 0o755);
730
+ }
731
+ catch (err) {
732
+ throw asInstallError(err, "copy", (detail) => `Could not put the engine at ${destBinary} (${detail}). Run: ${INSTALL_RETRY} --path <folder>`);
733
+ }
734
+ }
735
+ finally {
736
+ rmSync(downloadPath, { force: true });
737
+ rmSync(extractDir, { recursive: true, force: true });
738
+ releaseSignals();
739
+ }
740
+ const version = versionLabel === "custom" ? null : versionLabel;
741
+ await recordInstall(d, destBinary, version, log);
742
+ log(`\nSummer Engine (${versionLabel}) installed to ${destBinary}`);
743
+ log(FIRST_START_NOTE);
744
+ printLinuxNextSteps(destBinary, log);
745
+ return { binary: destBinary, version, destination: destDir, alreadyInstalled: false };
746
+ }
747
+ function printLinuxNextSteps(binary, log) {
748
+ log("\nNext steps:");
749
+ log(" summer dev # Create a starter project and open it");
750
+ log(" summer doctor # Verify the setup");
751
+ log(` ${binary} --headless --editor --path <project> # Headless engine`);
752
+ }
753
+ export async function downloadFile(url, dest) {
754
+ const { res } = await openDownload(url, (input, init) => fetch(input, init));
755
+ const isTTY = Boolean(process.stdout.isTTY);
756
+ const result = await saveDownload(res, dest, createProgressReporter(isTTY, (text) => process.stdout.write(text)));
757
+ if (isTTY)
758
+ process.stdout.write("\n");
759
+ return result;
760
+ }