summer-engine 4.5.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.
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "name": "summer",
12
12
  "source": "./",
13
- "version": "4.5.0",
13
+ "version": "4.6.0",
14
14
  "description": "The game engine built for AI. Any agent makes a multiplayer game and publishes it. A first-run guide (/summer:start), game-dev skills, and two MCP servers: the bridge to the Summer Engine desktop app, and Summer's hosted cloud server for people without it.",
15
15
  "author": {
16
16
  "name": "Summer Engine",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "summer",
4
4
  "displayName": "Summer Engine",
5
- "version": "4.5.0",
5
+ "version": "4.6.0",
6
6
  "description": "The game engine built for AI. Any agent makes a multiplayer game and publishes it. A first-run guide (/summer:start), game-dev skills, and two MCP servers: the bridge to the Summer Engine desktop app, and Summer's hosted cloud server for people without it.",
7
7
  "author": {
8
8
  "name": "Summer Engine",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_generated": "GENERATED from integrations/codex — do not edit; npm run generate:registry",
3
3
  "name": "summer",
4
- "version": "4.5.0",
4
+ "version": "4.6.0",
5
5
  "description": "The game engine built for AI. Any agent makes a multiplayer game and publishes it. Game-dev skills, lifecycle hooks, and a 125-tool MCP bridge to the Summer Engine desktop app.",
6
6
  "author": {
7
7
  "name": "Summer Engine",
@@ -3,7 +3,7 @@
3
3
  "_generated": "GENERATED from integrations/cursor — do not edit; npm run generate:registry",
4
4
  "name": "summer",
5
5
  "displayName": "Summer Engine",
6
- "version": "4.5.0",
6
+ "version": "4.6.0",
7
7
  "description": "The game engine built for AI. Any agent makes a multiplayer game and publishes it. Game-dev skills, lifecycle hooks, and a 125-tool MCP bridge to the Summer Engine desktop app.",
8
8
  "author": {
9
9
  "name": "Summer Engine",
@@ -2,7 +2,7 @@
2
2
  "_generated": "GENERATED from integrations/factory — do not edit; npm run generate:registry",
3
3
  "name": "summer",
4
4
  "description": "The game engine built for AI. Any agent makes a multiplayer game and publishes it. Game-dev skills, lifecycle hooks, and a 125-tool MCP bridge to the Summer Engine desktop app.",
5
- "version": "4.5.0",
5
+ "version": "4.6.0",
6
6
  "author": {
7
7
  "name": "Summer Engine",
8
8
  "email": "founders@summerengine.com",
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ All notable changes to summer-engine will be documented here. Following [Keep a
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [4.6.0] - 2026-10-21
8
+
9
+ A first game that looks finished, and `summer setup` for the Summer desktop app's one-click agent connect.
10
+
11
+ - **`summer setup` for installers: find the agents, read each step, run a bundled Node.js.** For the Summer desktop app's installer, which connects the person's coding agent in one click and ships its own Node.js (Claude Code and Codex install without one).
12
+ - **`summer setup --detect --json`** lists every supported agent: whether it is installed (`installed`, `programFound` and each hit in `detectedBy`: a command on PATH or in the usual install folders, a macOS app or Windows program, the agent's own folder, a VS Code extension), its MCP config file, and whether that file already lists `summer-engine`. It writes nothing.
13
+ - **`summer setup <agent> --yes --json` reports each step** in `steps`: the MCP entry, Summer Games, skills, project files and doctor, each with `ok`, a `status` and, on failure, an `error` with a `code` (`config_invalid`, `config_not_writable`, `skills_install_failed`, `runtime_not_found`, ...). A failed step no longer stops the others, and any failure exits 1. An error before setup starts (an unknown agent, a bad option) is one JSON document too. The output for people is unchanged.
14
+ - **`--runtime <node> --package-dir <dir>`** writes the MCP entry as `<node> <dir>/dist/bin/summer.js mcp` instead of `npx`, in every agent's file format. Setup checks that both exist and that the folder is the summer-engine package. Doctor's skills refresh keeps the bundled runtime.
15
+ - **First game: make it look finished.** A first game used to come out as grey boxes: the `make-game` skill told agents to build the first playable from shapes and add art later, and nothing an agent always reads said anything about looks. Now the rule sits where agents read it without choosing to:
16
+ - **MCP server instructions** open with it, inside the first 512 characters (Codex keeps the first 512): keep the features the person asked for and raise the look (sky and sun with shadows, fog, a texture on every surface, real models from the starter project and `summer_assets_search` first, never capsules or boxes as final art, one palette, a styled UI font), then play it and check a screenshot of the running game. They no longer open with "call summer_get_project_context first".
17
+ - **The project's `AGENTS.md` block** (written by `summer create` and `summer setup --yes`) has a "First game: make it look finished" section. It now says to write GDScript files directly and to create scenes and import assets through Summer, and its publishing line matches the store: a game goes live when Summer's automated checks pass, with no wait for a review.
18
+ - **`make-game`:** the first playable already looks good; generation still never blocks the first play (starter and library art first).
19
+
7
20
  ## [4.5.0] - 2026-10-19
8
21
 
9
22
  4.4.0 was not published to npm. 4.5.0 is the first release with everything in it (the [4.4.0] section below), plus these changes.
@@ -2,8 +2,10 @@ import { spawnSync } from "child_process";
2
2
  import { existsSync, readFileSync } from "fs";
3
3
  import { relative } from "path";
4
4
  import { Command, Option } from "commander";
5
- import { configureAgentMcp, configureSummerGamesMcp, resolveAgentSelection, resolveConfigScope, supportedAgents, } from "../../installer/agent-config.js";
5
+ import { configureAgentMcp, configureSummerGamesMcp, resolveAgentSelection, resolveBundledRuntime, resolveConfigScope, supportedAgents, } from "../../installer/agent-config.js";
6
6
  import { previewNote, setupSkills } from "../../installer/setup.js";
7
+ import { detectAgents } from "../../installer/agent-detect.js";
8
+ import { SetupError, toStepError } from "../../installer/setup-error.js";
7
9
  import { printDoctorResult, runDoctor } from "../../core/capabilities/doctor.js";
8
10
  import { brandLine, c, sym, tildeify } from "../../core/format.js";
9
11
  import { TOOLKIT_VERSION as cliVersion } from "../../core/version.js";
@@ -23,6 +25,8 @@ const MCP_ENTRY_FLAGS = [
23
25
  ["localDev", "--local-dev"],
24
26
  ["channel", "--channel"],
25
27
  ["profile", "--profile"],
28
+ ["runtime", "--runtime"],
29
+ ["packageDir", "--package-dir"],
26
30
  ];
27
31
  export const setupCommand = new Command("setup")
28
32
  .description("Configure Summer Engine for an AI agent and run diagnostics. For agents that can sign in to it, it also adds Summer Games (summer-games). --cli installs the skills only: the agent runs summer commands in its shell, with no MCP server.")
@@ -32,6 +36,9 @@ export const setupCommand = new Command("setup")
32
36
  .option("--dry-run", "Show planned changes without writing files")
33
37
  .option("--print", "Print the MCP config snippet instead of writing files")
34
38
  .option("--local-dev", "Point the agent at this checkout's built CLI (node <repo>/dist/bin/summer.js mcp) instead of npx summer-engine@latest — for testing unpublished builds. SUMMER_DEV=1 does the same.")
39
+ .option("--runtime <node>", "Run the MCP server with this node executable instead of npx: <node> <package-dir>/dist/bin/summer.js mcp. Needs --package-dir. For installers that ship their own Node.js")
40
+ .option("--package-dir <dir>", "The summer-engine package folder (package.json and dist/bin/summer.js) that --runtime runs")
41
+ .option("--detect", "List every supported agent: installed or not, its MCP config file, and whether it already lists summer-engine. Writes nothing; takes no agent. Use with --json")
35
42
  .option("--channel <dist-tag>", "npm dist-tag or exact version the agent's MCP entry runs: npx -y summer-engine@<dist-tag> mcp (default latest; a CLI newer than npm latest pins next when next is this version, else this version). SUMMER_CHANNEL does the same.")
36
43
  .option("--yes", "Apply practical setup steps without prompting")
37
44
  .option("--no-games", "Do not add the Summer Games MCP (summer-games: friends, parties and messages as you, signed in through summer.games)")
@@ -49,8 +56,52 @@ export const setupCommand = new Command("setup")
49
56
  // Pre-reversal opt-in, kept one release as a hidden no-op so old scripts still parse.
50
57
  .addOption(new Option("--include-preview", "No-op: preview skills install by default").hideHelp())
51
58
  .action(async (agentArg, opts) => {
52
- const agent = resolveAgentSelection(agentArg, opts.agent);
53
- const scope = resolveConfigScope(opts.scope);
59
+ if (opts.detect) {
60
+ runDetect(agentArg, opts);
61
+ return;
62
+ }
63
+ if (!opts.json) {
64
+ await setupAgent(agentArg, opts);
65
+ return;
66
+ }
67
+ // --json: one JSON document on stdout, also when setup cannot start.
68
+ try {
69
+ await setupAgent(agentArg, opts);
70
+ }
71
+ catch (error) {
72
+ console.log(JSON.stringify({ ok: false, error: toStepError(error, "setup_failed"), steps: [] }, null, 2));
73
+ process.exit(1);
74
+ }
75
+ });
76
+ /**
77
+ * Runs one step. With --json a throw becomes the step's coded error and the
78
+ * other steps still run; without it the throw ends setup as it always did.
79
+ */
80
+ async function runStep(json, fallbackCode, run) {
81
+ if (!json)
82
+ return { value: await run() };
83
+ try {
84
+ return { value: await run() };
85
+ }
86
+ catch (error) {
87
+ return { value: null, error: toStepError(error, fallbackCode) };
88
+ }
89
+ }
90
+ async function setupAgent(agentArg, opts) {
91
+ let agent;
92
+ try {
93
+ agent = resolveAgentSelection(agentArg, opts.agent);
94
+ }
95
+ catch (error) {
96
+ throw new SetupError("unknown_agent", error instanceof Error ? error.message : String(error));
97
+ }
98
+ let scope;
99
+ try {
100
+ scope = resolveConfigScope(opts.scope);
101
+ }
102
+ catch (error) {
103
+ throw new SetupError("invalid_scope", error instanceof Error ? error.message : String(error));
104
+ }
54
105
  if (opts.removeAllowRules) {
55
106
  await removeCliAllowRules(agent, scope, opts);
56
107
  return;
@@ -63,8 +114,14 @@ export const setupCommand = new Command("setup")
63
114
  if (opts.allowRules)
64
115
  console.error(` ${sym.warn()} ${c.yellow("--allow-rules works with --cli only (the MCP server asks through the agent's own tool approvals); no rules were added.")}`);
65
116
  const profile = resolveSetupProfile(opts);
66
- const defaultChannel = await resolveDefaultChannel(cliVersion);
67
- const config = await configureAgentMcp({
117
+ const runtime = resolveBundledRuntime(opts.runtime, opts.packageDir);
118
+ if (runtime && (opts.localDev || opts.channel)) {
119
+ throw new SetupError("invalid_option", `--runtime runs a bundled summer-engine; leave out ${opts.localDev ? "--local-dev" : "--channel"}.`);
120
+ }
121
+ const json = Boolean(opts.json);
122
+ // A bundled runtime runs the package beside it, so npm's tags do not matter (and no network call is made).
123
+ const defaultChannel = runtime ? { channel: "latest" } : await resolveDefaultChannel(cliVersion);
124
+ const mcp = await runStep(json, "mcp_entry_failed", () => configureAgentMcp({
68
125
  agent,
69
126
  scope,
70
127
  dryRun: opts.dryRun,
@@ -72,14 +129,19 @@ export const setupCommand = new Command("setup")
72
129
  localDev: Boolean(opts.localDev) || process.env.SUMMER_DEV === "1",
73
130
  channel: opts.channel ?? process.env.SUMMER_CHANNEL ?? defaultChannel.channel,
74
131
  profile,
75
- });
76
- const games = opts.games === false || opts.assetsOnly || opts.print
77
- ? null
78
- : await configureSummerGamesMcp({ agent, scope, dryRun: opts.dryRun });
79
- if (!opts.channel && !process.env.SUMMER_CHANNEL && defaultChannel.note && !opts.print) {
80
- console.log(` ${c.dim(defaultChannel.note)}`);
81
- }
82
- const skills = setupSkills(agent, {
132
+ runtime: runtime ?? undefined,
133
+ }));
134
+ const config = mcp.value;
135
+ const gamesSkipped = opts.games === false || opts.assetsOnly || opts.print;
136
+ const gamesStep = gamesSkipped
137
+ ? { value: null }
138
+ : await runStep(json, "summer_games_failed", () => configureSummerGamesMcp({ agent, scope, dryRun: opts.dryRun }));
139
+ const games = gamesStep.value;
140
+ if (!runtime && !opts.channel && !process.env.SUMMER_CHANNEL && defaultChannel.note && !opts.print) {
141
+ // On stderr with --json, so stdout stays one JSON document.
142
+ (json ? console.error : console.log)(` ${c.dim(defaultChannel.note)}`);
143
+ }
144
+ const skillsStep = await runStep(json, "skills_install_failed", () => setupSkills(agent, {
83
145
  dryRun: Boolean(opts.dryRun || opts.print),
84
146
  yes: Boolean(opts.yes),
85
147
  force: Boolean(opts.force),
@@ -87,26 +149,104 @@ export const setupCommand = new Command("setup")
87
149
  recommended: Boolean(opts.recommended),
88
150
  stableOnly: Boolean(opts.stableOnly),
89
151
  assetsOnly: Boolean(opts.assetsOnly),
90
- });
91
- const projectFiles = setupProjectInstructions(opts);
152
+ }));
153
+ const skills = skillsStep.value ?? { status: "failed", message: skillsStep.error?.message ?? "Skill install failed." };
154
+ const projectStep = await runStep(json, "project_files_failed", () => setupProjectInstructions(opts));
155
+ const projectFiles = projectStep.value;
92
156
  const doctor = await runDoctor({ quiet: true });
93
- if (opts.json) {
94
- console.log(JSON.stringify({
95
- ok: doctor.ok && skills.status !== "failed",
96
- config,
97
- games,
98
- skills,
99
- projectFiles,
100
- doctor,
101
- }, null, 2));
102
- }
103
- else {
104
- printSetupResult(config, games, skills, doctor, projectFiles);
157
+ if (json) {
158
+ const steps = [
159
+ mcpEntryStep(config, mcp.error),
160
+ summerGamesStep(agent, games, gamesSkipped ? "skipped" : null, gamesStep.error, Boolean(opts.dryRun)),
161
+ skillsStepResult(skills, skillsStep.error),
162
+ projectFilesStep(projectFiles, projectStep.error, opts),
163
+ doctorStep(doctor),
164
+ ];
165
+ const ok = steps.every((step) => step.ok);
166
+ console.log(JSON.stringify({ ok, agent, scope, steps, config, games, skills, projectFiles, doctor }, null, 2));
167
+ if (!ok)
168
+ process.exit(1);
169
+ return;
105
170
  }
171
+ printSetupResult(config, games, skills, doctor, projectFiles);
106
172
  if (!doctor.ok || skills.status === "failed") {
107
173
  process.exit(1);
108
174
  }
109
- });
175
+ }
176
+ function mcpEntryStep(config, error) {
177
+ if (!config)
178
+ return { step: "mcp_entry", ok: false, status: "failed", error: error ?? { code: "mcp_entry_failed", message: "The MCP entry was not written." } };
179
+ const status = config.print ? "printed" : config.dryRun ? (config.changed ? "planned" : "unchanged") : config.wrote ? "written" : "unchanged";
180
+ return { step: "mcp_entry", ok: true, status, path: config.path, ...(config.warnings.length ? { warnings: config.warnings } : {}) };
181
+ }
182
+ function summerGamesStep(agent, games, skipped, error, dryRun) {
183
+ if (error)
184
+ return { step: "summer_games", ok: false, status: "failed", error };
185
+ if (skipped)
186
+ return { step: "summer_games", ok: true, status: "skipped" };
187
+ // configureSummerGamesMcp returns null only for agents without a Summer Games entry shape.
188
+ if (!games)
189
+ return { step: "summer_games", ok: true, status: "not_supported", warnings: [`${agentLabel(agent)} gets no Summer Games entry from setup.`] };
190
+ const status = games.existingName ? "exists" : dryRun ? (games.changed ? "planned" : "unchanged") : games.wrote ? "written" : "unchanged";
191
+ return { step: "summer_games", ok: true, status, path: games.path };
192
+ }
193
+ function skillsStepResult(skills, error) {
194
+ if (skills.status === "failed") {
195
+ return {
196
+ step: "skills",
197
+ ok: false,
198
+ status: "failed",
199
+ error: error ?? { code: "skills_install_failed", message: [skills.message, skills.stderr].filter(Boolean).join(" ") },
200
+ };
201
+ }
202
+ return { step: "skills", ok: true, status: skills.status, ...(skills.destination ? { path: skills.destination } : {}) };
203
+ }
204
+ function projectFilesStep(project, error, opts) {
205
+ if (error)
206
+ return { step: "project_files", ok: false, status: "failed", error };
207
+ if (!project) {
208
+ const skipped = opts.projectFiles === false || opts.print || opts.assetsOnly;
209
+ return { step: "project_files", ok: true, status: skipped ? "skipped" : "not_in_project" };
210
+ }
211
+ // A file setup leaves alone (a link out of the project, a broken Summer block) is a warning, as in the human output.
212
+ const warnings = project.files.filter((file) => file.reason).map((file) => `${file.path}: ${file.reason}`);
213
+ return { step: "project_files", ok: true, status: project.written ? "written" : "planned", path: project.root, ...(warnings.length ? { warnings } : {}) };
214
+ }
215
+ function doctorStep(doctor) {
216
+ if (doctor.ok)
217
+ return { step: "doctor", ok: true, status: "passed" };
218
+ const failed = doctor.checks.filter((check) => check.status === "fail");
219
+ return {
220
+ step: "doctor",
221
+ ok: false,
222
+ status: "failed",
223
+ error: { code: "doctor_failed", message: failed.map((check) => `${check.label}: ${check.message}`).join("; ") || "Doctor found a problem." },
224
+ };
225
+ }
226
+ /** `summer setup --detect`: every agent, installed or not, and whether it is linked. Writes nothing. */
227
+ function runDetect(agentArg, opts) {
228
+ if (agentArg || opts.agent) {
229
+ const message = "--detect lists every agent; leave out the agent name.";
230
+ if (!opts.json)
231
+ throw new Error(message);
232
+ console.log(JSON.stringify({ ok: false, error: { code: "invalid_option", message }, agents: [] }, null, 2));
233
+ process.exit(1);
234
+ return;
235
+ }
236
+ const agents = detectAgents();
237
+ if (opts.json) {
238
+ console.log(JSON.stringify({ ok: true, os: process.platform, agents }, null, 2));
239
+ return;
240
+ }
241
+ console.log("");
242
+ for (const agent of agents) {
243
+ if (agent.status === "legacy" && !agent.installed)
244
+ continue;
245
+ const mark = agent.installed ? sym.ok() : c.dim("-");
246
+ const linked = agent.mcpEntry ? c.dim(`linked ${tildeify(agent.configPath)}`) : agent.installed ? c.dim(`not linked: summer setup ${agent.id}`) : c.dim("not found");
247
+ console.log(` ${mark} ${agent.label} ${linked}`);
248
+ }
249
+ }
110
250
  /**
111
251
  * `summer setup <agent> --cli`: the skills (the asset skills with --assets-only)
112
252
  * and no MCP server, so the agent runs Summer as `summer <area> <action>` in its
@@ -117,7 +257,7 @@ async function setupCliOnly(agent, scope, opts) {
117
257
  const spec = agentSpec(agent);
118
258
  const mcpFlags = MCP_ENTRY_FLAGS.filter(([key]) => opts[key] !== undefined && opts[key] !== false).map(([, flag]) => flag);
119
259
  if (mcpFlags.length > 0) {
120
- throw new Error(`${mcpFlags.join(", ")} sets up the MCP entry, and --cli writes none. Leave ${mcpFlags.length === 1 ? "it" : "them"} out, or leave out --cli.`);
260
+ throw new SetupError("invalid_option", `${mcpFlags.join(", ")} sets up the MCP entry, and --cli writes none. Leave ${mcpFlags.length === 1 ? "it" : "them"} out, or leave out --cli.`);
121
261
  }
122
262
  const refusal = cliSetupRefusal(spec);
123
263
  if (refusal)
@@ -278,11 +418,11 @@ async function recordedMcpEntry(agent, scope) {
278
418
  function resolveSetupProfile(opts) {
279
419
  const profile = opts.profile === undefined ? undefined : parseProfile(opts.profile);
280
420
  if (profile === null) {
281
- throw new Error(`Unknown --profile "${opts.profile}". Use one of: ${Object.keys(PROFILES).join(", ")}.`);
421
+ throw new SetupError("invalid_option", `Unknown --profile "${opts.profile}". Use one of: ${Object.keys(PROFILES).join(", ")}.`);
282
422
  }
283
423
  if (opts.assetsOnly) {
284
424
  if (profile && profile !== "assets")
285
- throw new Error("--assets-only uses the assets profile; leave out --profile.");
425
+ throw new SetupError("invalid_option", "--assets-only uses the assets profile; leave out --profile.");
286
426
  return "assets";
287
427
  }
288
428
  return profile;
@@ -305,7 +445,10 @@ function printSetupResult(config, games, skills, doctor, projectFiles) {
305
445
  else {
306
446
  console.log(` ${sym.ok()} Already linked to ${c.bold(label)} ${c.dim(tildeify(config.path))}`);
307
447
  }
308
- if (config.localDev) {
448
+ if (config.runtime) {
449
+ console.log(` ${c.dim("(bundled runtime)")} MCP server command: ${c.dim(`${config.server.command} ${config.server.args.join(" ")}`)}`);
450
+ }
451
+ else if (config.localDev) {
309
452
  console.log(` ${c.dim("(local dev)")} MCP server command: ${c.dim(`${config.server.command} ${config.server.args.join(" ")}`)}`);
310
453
  }
311
454
  else if (config.channel !== "latest") {
@@ -18,6 +18,10 @@ export interface AgentConfigOptions {
18
18
  channel?: string;
19
19
  /** MCP tool profile written as SUMMER_PROFILE; default the agent's own (full for most). */
20
20
  profile?: Profile;
21
+ /** A bundled Node runtime and summer-engine package (the Summer desktop installer):
22
+ * the entry runs `<node> <packageDir>/dist/bin/summer.js mcp` instead of npx.
23
+ * Check it with resolveBundledRuntime first. Ignores localDev and channel. */
24
+ runtime?: BundledRuntime;
21
25
  cwd?: string;
22
26
  env?: NodeJS.ProcessEnv;
23
27
  }
@@ -39,6 +43,8 @@ export interface AgentConfigResult {
39
43
  channel: string;
40
44
  /** The MCP tool profile the entry runs (full = every tool, no SUMMER_PROFILE written). */
41
45
  profile: Profile;
46
+ /** Set when the entry runs a bundled Node runtime (--runtime, --package-dir). */
47
+ runtime: BundledRuntime | null;
42
48
  warnings: string[];
43
49
  nextSteps: string[];
44
50
  }
@@ -89,6 +95,29 @@ export declare function normalizeChannel(channel: string | undefined): string;
89
95
  */
90
96
  export declare function withProfile(server: StdioMcpServerConfig, profile: Profile): StdioMcpServerConfig;
91
97
  export declare function createSummerMcpServerConfig(localDev: boolean, platform?: NodeJS.Platform, channel?: string): StdioMcpServerConfig;
98
+ /**
99
+ * A Node runtime and a summer-engine package shipped next to each other (the
100
+ * Summer desktop installer bundles both, so the person needs no Node.js of
101
+ * their own). Both are absolute paths.
102
+ */
103
+ export interface BundledRuntime {
104
+ /** The node executable (node, or node.exe on Windows). */
105
+ node: string;
106
+ /** The summer-engine package folder: the one with package.json and dist/bin/summer.js. */
107
+ packageDir: string;
108
+ }
109
+ /** The CLI entry point inside a summer-engine package folder. */
110
+ export declare function bundledCliPath(packageDir: string): string;
111
+ /**
112
+ * --runtime <node> and --package-dir <dir>: both or neither, and both must
113
+ * exist. The runtime must be an executable file (any file on Windows); the
114
+ * package folder must hold summer-engine's package.json and its built CLI.
115
+ * Returns absolute paths, so the agent's entry works from any folder.
116
+ */
117
+ export declare function resolveBundledRuntime(node: string | undefined, packageDir: string | undefined, cwd?: string): BundledRuntime | null;
118
+ /** The entry for a bundled runtime: `<node> <packageDir>/dist/bin/summer.js mcp`.
119
+ * node is a real executable, so no cmd.exe wrapper is needed on Windows. */
120
+ export declare function bundledRuntimeServerConfig(runtime: BundledRuntime): StdioMcpServerConfig;
92
121
  export declare function renderConfigSnippet(agent: SupportedAgent, server: StdioMcpServerConfig): string;
93
122
  /** The key under which the file lists MCP servers (see McpFormat in agent-table.ts). */
94
123
  export declare function topLevelKey(format: McpFormat): string;
@@ -1,9 +1,10 @@
1
- import { existsSync, readFileSync } from "fs";
1
+ import { accessSync, constants as fsConstants, existsSync, readFileSync, statSync } from "fs";
2
2
  import { copyFile, mkdir, readFile, writeFile } from "fs/promises";
3
3
  import { dirname, join, resolve } from "path";
4
4
  import { fileURLToPath } from "url";
5
5
  import { Document, isMap, parseDocument } from "yaml";
6
6
  import { PACKAGE_ROOT } from "../core/package-root.js";
7
+ import { SetupError } from "./setup-error.js";
7
8
  import { agentAliasMap, agentSpec, defaultPathContext, resolveMcpPath, supportedAgents, } from "./agent-table.js";
8
9
  export { supportedAgents } from "./agent-table.js";
9
10
  export const SUMMER_MCP_SERVER_NAME = "summer-engine";
@@ -50,7 +51,8 @@ export async function configureAgentMcp(options) {
50
51
  const channel = normalizeChannel(options.channel);
51
52
  const spec = agentSpec(options.agent);
52
53
  const profile = options.profile ?? spec.mcpProfile ?? "full";
53
- const server = withProfile(createSummerMcpServerConfig(Boolean(options.localDev), process.platform, channel), profile);
54
+ const runtime = options.runtime ?? null;
55
+ const server = withProfile(runtime ? bundledRuntimeServerConfig(runtime) : createSummerMcpServerConfig(Boolean(options.localDev), process.platform, channel), profile);
54
56
  const target = resolveConfigTarget(spec, options.scope, cwd, env);
55
57
  // An entry an earlier setup wrote where the agent no longer reads (Devin Desktop moved from ~/.codeium/windsurf).
56
58
  const moved = spec.mcp.movedFrom ? spec.mcp.movedFrom(defaultPathContext(env, cwd)) : null;
@@ -76,9 +78,10 @@ export async function configureAgentMcp(options) {
76
78
  wrote: shouldWrite && update.changed,
77
79
  dryRun,
78
80
  print,
79
- localDev: Boolean(options.localDev),
81
+ localDev: !runtime && Boolean(options.localDev),
80
82
  channel,
81
83
  profile,
84
+ runtime,
82
85
  warnings: target.warnings,
83
86
  nextSteps: [`Updated ${target.path}.`, spec.restart],
84
87
  };
@@ -320,6 +323,67 @@ export function createSummerMcpServerConfig(localDev, platform = process.platfor
320
323
  args: ["-y", spec, "mcp"],
321
324
  };
322
325
  }
326
+ /** The CLI entry point inside a summer-engine package folder. */
327
+ export function bundledCliPath(packageDir) {
328
+ return join(packageDir, "dist", "bin", "summer.js");
329
+ }
330
+ /**
331
+ * --runtime <node> and --package-dir <dir>: both or neither, and both must
332
+ * exist. The runtime must be an executable file (any file on Windows); the
333
+ * package folder must hold summer-engine's package.json and its built CLI.
334
+ * Returns absolute paths, so the agent's entry works from any folder.
335
+ */
336
+ export function resolveBundledRuntime(node, packageDir, cwd = process.cwd()) {
337
+ if (node === undefined && packageDir === undefined)
338
+ return null;
339
+ if (!node || !packageDir) {
340
+ throw new SetupError("runtime_incomplete", "--runtime and --package-dir go together: name the node executable and the summer-engine package folder.");
341
+ }
342
+ const nodePath = resolve(cwd, node);
343
+ const nodeStat = statIfExists(nodePath);
344
+ if (!nodeStat)
345
+ throw new SetupError("runtime_not_found", `--runtime ${nodePath} does not exist.`);
346
+ if (!nodeStat.isFile())
347
+ throw new SetupError("runtime_not_found", `--runtime ${nodePath} is not a file. Name the node executable itself.`);
348
+ if (process.platform !== "win32") {
349
+ try {
350
+ accessSync(nodePath, fsConstants.X_OK);
351
+ }
352
+ catch {
353
+ throw new SetupError("runtime_not_executable", `--runtime ${nodePath} is not executable.`);
354
+ }
355
+ }
356
+ const dir = resolve(cwd, packageDir);
357
+ if (!statIfExists(dir)?.isDirectory())
358
+ throw new SetupError("package_dir_not_found", `--package-dir ${dir} is not a folder.`);
359
+ let name;
360
+ try {
361
+ name = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8")).name;
362
+ }
363
+ catch {
364
+ name = undefined;
365
+ }
366
+ if (name !== "summer-engine") {
367
+ throw new SetupError("package_dir_invalid", `--package-dir ${dir} is not the summer-engine package: it has no package.json named summer-engine.`);
368
+ }
369
+ if (!statIfExists(bundledCliPath(dir))?.isFile()) {
370
+ throw new SetupError("package_dir_invalid", `--package-dir ${dir} has no built CLI at dist/bin/summer.js.`);
371
+ }
372
+ return { node: nodePath, packageDir: dir };
373
+ }
374
+ /** The entry for a bundled runtime: `<node> <packageDir>/dist/bin/summer.js mcp`.
375
+ * node is a real executable, so no cmd.exe wrapper is needed on Windows. */
376
+ export function bundledRuntimeServerConfig(runtime) {
377
+ return { command: runtime.node, args: [bundledCliPath(runtime.packageDir), "mcp"] };
378
+ }
379
+ function statIfExists(path) {
380
+ try {
381
+ return statSync(path);
382
+ }
383
+ catch {
384
+ return null;
385
+ }
386
+ }
323
387
  export function renderConfigSnippet(agent, server) {
324
388
  const format = agentSpec(agent).format;
325
389
  if (format === "toml")
@@ -449,14 +513,14 @@ async function upsertYamlConfig(path, keyPath, entry, write) {
449
513
  // would make the root a flow mapping and the whole file would render as {}.
450
514
  const doc = current.trim() === "" ? new Document({}) : parseDocument(current);
451
515
  if (doc.errors.length > 0) {
452
- throw new Error(`Could not parse YAML in ${path}: ${doc.errors[0].message}`);
516
+ throw new SetupError("config_invalid", `Could not parse YAML in ${path}: ${doc.errors[0].message}`);
453
517
  }
454
518
  if (!isMap(doc.contents)) {
455
- throw new Error(`Expected ${path} to contain a YAML mapping.`);
519
+ throw new SetupError("config_invalid", `Expected ${path} to contain a YAML mapping.`);
456
520
  }
457
521
  const parent = doc.getIn(keyPath.slice(0, -1));
458
522
  if (parent !== undefined && parent !== null && !isMap(parent)) {
459
- throw new Error(`Existing ${keyPath[0]} value in ${path} must be a mapping.`);
523
+ throw new SetupError("config_invalid", `Existing ${keyPath[0]} value in ${path} must be a mapping.`);
460
524
  }
461
525
  doc.setIn(keyPath, doc.createNode(entry));
462
526
  const next = doc.toString();
@@ -474,7 +538,7 @@ async function readJsonConfig(path) {
474
538
  content = await readFile(path, "utf-8");
475
539
  }
476
540
  catch (error) {
477
- throw new Error(`Could not read ${path}: ${formatError(error)}`);
541
+ throw new SetupError("config_unreadable", `Could not read ${path}: ${formatError(error)}`);
478
542
  }
479
543
  if (content.trim() === "")
480
544
  return {};
@@ -483,10 +547,10 @@ async function readJsonConfig(path) {
483
547
  parsed = JSON.parse(content);
484
548
  }
485
549
  catch (error) {
486
- throw new Error(`Could not parse JSON in ${path}: ${formatError(error)}`);
550
+ throw new SetupError("config_invalid", `Could not parse JSON in ${path}: ${formatError(error)}`);
487
551
  }
488
552
  if (!isJsonObject(parsed)) {
489
- throw new Error(`Expected ${path} to contain a JSON object.`);
553
+ throw new SetupError("config_invalid", `Expected ${path} to contain a JSON object.`);
490
554
  }
491
555
  return parsed;
492
556
  }
@@ -662,7 +726,7 @@ async function readTextFileIfExists(path) {
662
726
  return await readFile(path, "utf-8");
663
727
  }
664
728
  catch (error) {
665
- throw new Error(`Could not read ${path}: ${formatError(error)}`);
729
+ throw new SetupError("config_unreadable", `Could not read ${path}: ${formatError(error)}`);
666
730
  }
667
731
  }
668
732
  function upsertTomlTable(content, table, name = SUMMER_MCP_SERVER_NAME) {
@@ -716,7 +780,7 @@ function copyJsonObject(value) {
716
780
  }
717
781
  function mergeNamedObject(value, key, entry, label) {
718
782
  if (value !== undefined && !isJsonObject(value)) {
719
- throw new Error(`Existing ${label} value must be a JSON object.`);
783
+ throw new SetupError("config_invalid", `Existing ${label} value must be a JSON object.`);
720
784
  }
721
785
  return {
722
786
  ...(isJsonObject(value) ? value : {}),
@@ -0,0 +1,44 @@
1
+ import { type ConfigScope, type McpFormat, type PathContext, type SupportedAgent } from "./agent-table.js";
2
+ import { type SetupStepError } from "./setup-error.js";
3
+ import type { StdioMcpServerConfig } from "./agent-config.js";
4
+ export interface DetectHit {
5
+ kind: "bin" | "app" | "folder" | "extension";
6
+ path: string;
7
+ }
8
+ export interface DetectedAgent {
9
+ id: SupportedAgent;
10
+ label: string;
11
+ /** legacy: the product was retired or renamed; setup still writes it, with a warning. */
12
+ status: "active" | "legacy";
13
+ installed: boolean;
14
+ /** A command, app or extension was found, not only a folder. A folder can outlive the
15
+ * program, or be one an earlier Summer setup made for its skills or config. */
16
+ programFound: boolean;
17
+ /** Every place that showed the agent is installed (empty when not installed). */
18
+ detectedBy: DetectHit[];
19
+ /** The MCP config file `summer setup <id>` writes at user scope (project scope for agents with no user file). */
20
+ configPath: string;
21
+ configScope: ConfigScope;
22
+ configFormat: McpFormat;
23
+ configExists: boolean;
24
+ /** The config file lists a summer-engine MCP server, in any shape. */
25
+ mcpEntry: boolean;
26
+ /** The command the summer-engine entry runs, when it is a local (stdio) entry. */
27
+ mcpServer: StdioMcpServerConfig | null;
28
+ /** Setup also adds the Summer Games MCP for this agent. */
29
+ summerGames: boolean;
30
+ /** Setup also installs skills for this agent. */
31
+ skills: boolean;
32
+ /** Set when the config file exists but cannot be read. */
33
+ configError?: SetupStepError;
34
+ }
35
+ export interface DetectOptions {
36
+ ctx?: PathContext;
37
+ /** Folders searched for macOS app bundles; default /Applications and ~/Applications. */
38
+ macAppDirs?: string[];
39
+ /** Shared command folders outside home searched on macOS and Linux; default Homebrew's /opt/homebrew/bin and /usr/local/bin. */
40
+ systemBinDirs?: string[];
41
+ }
42
+ export declare function detectAgents(options?: DetectOptions): DetectedAgent[];
43
+ /** PATH first, then the install folders a GUI-launched process's PATH leaves out. */
44
+ export declare function searchedBinDirs(ctx: PathContext, systemBinDirs?: string[]): string[];