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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -1
- package/.codex-plugin/plugin.json +5 -3
- package/.cursor-plugin/plugin.json +4 -2
- package/.factory-plugin/plugin.json +4 -2
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +100 -0
- package/README.md +67 -27
- package/commands/start.md +19 -15
- package/commands/summer.md +1 -0
- package/dist/cli/command-usage.d.ts +3 -1
- package/dist/cli/command-usage.js +10 -4
- package/dist/cli/commands/create.d.ts +1 -25
- package/dist/cli/commands/create.js +10 -138
- package/dist/cli/commands/dev.d.ts +66 -0
- package/dist/cli/commands/dev.js +234 -0
- package/dist/cli/commands/install.d.ts +15 -58
- package/dist/cli/commands/install.js +31 -481
- package/dist/cli/commands/login.js +1 -1
- package/dist/cli/commands/ops.d.ts +17 -1
- package/dist/cli/commands/ops.js +97 -9
- package/dist/cli/commands/preview.d.ts +58 -0
- package/dist/cli/commands/preview.js +181 -0
- package/dist/cli/commands/run.d.ts +1 -17
- package/dist/cli/commands/run.js +17 -206
- package/dist/cli/commands/setup.js +175 -32
- package/dist/cli/index.js +5 -0
- package/dist/core/agent-host.d.ts +3 -0
- package/dist/core/agent-host.js +1 -1
- package/dist/core/api-client.d.ts +10 -2
- package/dist/core/api-client.js +13 -8
- package/dist/core/capabilities/agent-playbook.js +1 -0
- package/dist/core/capabilities/apps.d.ts +9 -9
- package/dist/core/capabilities/asset-import.d.ts +4 -1
- package/dist/core/capabilities/asset-import.js +15 -6
- package/dist/core/capabilities/capture-gameplay.js +1 -1
- package/dist/core/capabilities/connect-signal.d.ts +2 -2
- package/dist/core/capabilities/content-make/index.d.ts +81 -0
- package/dist/core/capabilities/content-make/index.js +706 -0
- package/dist/core/capabilities/content-make/media.d.ts +57 -0
- package/dist/core/capabilities/content-make/media.js +172 -0
- package/dist/core/capabilities/content-make/plan.d.ts +130 -0
- package/dist/core/capabilities/content-make/plan.js +433 -0
- package/dist/core/capabilities/content-make/render.d.ts +64 -0
- package/dist/core/capabilities/content-make/render.js +370 -0
- package/dist/core/capabilities/content-make/study.d.ts +13 -0
- package/dist/core/capabilities/content-make/study.js +165 -0
- package/dist/core/capabilities/content-make/types.d.ts +173 -0
- package/dist/core/capabilities/content-make/types.js +6 -0
- package/dist/core/capabilities/content-make/voice.d.ts +7 -0
- package/dist/core/capabilities/content-make/voice.js +27 -0
- package/dist/core/capabilities/events.d.ts +2 -2
- package/dist/core/capabilities/export-game.d.ts +4 -1
- package/dist/core/capabilities/export-game.js +4 -3
- package/dist/core/capabilities/export-presets.d.ts +2 -1
- package/dist/core/capabilities/export-presets.js +2 -1
- package/dist/core/capabilities/go-live.d.ts +14 -0
- package/dist/core/capabilities/go-live.js +408 -0
- package/dist/core/capabilities/image-generation.d.ts +6 -6
- package/dist/core/capabilities/item-manifest.d.ts +92 -0
- package/dist/core/capabilities/item-manifest.js +247 -0
- package/dist/core/capabilities/placement.d.ts +7 -7
- package/dist/core/capabilities/projects-local.d.ts +68 -0
- package/dist/core/capabilities/projects-local.js +172 -0
- package/dist/core/capabilities/publish-build.d.ts +13 -2
- package/dist/core/capabilities/publish-build.js +87 -21
- package/dist/core/capabilities/publish-download.js +1 -1
- package/dist/core/capabilities/runtime-control.d.ts +7 -2
- package/dist/core/capabilities/runtime-control.js +27 -1
- package/dist/core/capabilities/seeing/args.d.ts +2 -2
- package/dist/core/capabilities/store-checks.d.ts +45 -0
- package/dist/core/capabilities/store-checks.js +85 -0
- package/dist/core/capabilities/store-details.d.ts +37 -0
- package/dist/core/capabilities/store-details.js +94 -0
- package/dist/core/capabilities/summer-bundle.d.ts +21 -2
- package/dist/core/capabilities/summer-bundle.js +54 -7
- package/dist/core/capabilities/sync-items.d.ts +89 -0
- package/dist/core/capabilities/sync-items.js +352 -0
- package/dist/core/capabilities/tool-dispatch.js +66 -6
- package/dist/core/capabilities/video-ogv.d.ts +49 -0
- package/dist/core/capabilities/video-ogv.js +260 -0
- package/dist/core/capability-skew.js +2 -2
- package/dist/core/cli-usage-beacon.js +26 -2
- package/dist/core/cli-usage.d.ts +5 -2
- package/dist/core/cli-usage.js +8 -2
- package/dist/core/engine-install.d.ts +53 -0
- package/dist/core/engine-install.js +93 -0
- package/dist/core/engine-installer.d.ts +134 -0
- package/dist/core/engine-installer.js +760 -0
- package/dist/core/engine-launch.d.ts +126 -0
- package/dist/core/engine-launch.js +398 -0
- package/dist/core/engine-self-start.d.ts +63 -0
- package/dist/core/engine-self-start.js +282 -0
- package/dist/core/engine.d.ts +7 -1
- package/dist/core/engine.js +9 -2
- package/dist/core/error-codes.d.ts +4 -1
- package/dist/core/error-codes.js +7 -3
- package/dist/core/library-read.d.ts +2 -2
- package/dist/core/library-search.d.ts +2 -2
- package/dist/core/local-project.d.ts +50 -0
- package/dist/core/local-project.js +234 -0
- package/dist/core/ops/areas.js +9 -3
- package/dist/core/ops/catalog.d.ts +9 -0
- package/dist/core/ops/catalog.js +56 -4
- package/dist/core/ops/engine-answer.d.ts +4 -1
- package/dist/core/ops/engine-answer.js +16 -3
- package/dist/core/ops/engine.js +4 -4
- package/dist/core/ops/save-files.d.ts +6 -1
- package/dist/core/ops/save-files.js +67 -50
- package/dist/core/ops/search.js +9 -2
- package/dist/core/preview/input.d.ts +35 -0
- package/dist/core/preview/input.js +105 -0
- package/dist/core/preview/launch-config.d.ts +52 -0
- package/dist/core/preview/launch-config.js +137 -0
- package/dist/core/preview/page.d.ts +22 -0
- package/dist/core/preview/page.js +232 -0
- package/dist/core/preview/preview-source.d.ts +179 -0
- package/dist/core/preview/preview-source.js +350 -0
- package/dist/core/preview/server.d.ts +26 -0
- package/dist/core/preview/server.js +218 -0
- package/dist/core/telemetry.d.ts +20 -16
- package/dist/core/telemetry.js +92 -16
- package/dist/core/templates.d.ts +20 -0
- package/dist/core/templates.js +300 -3
- package/dist/core/tool-profiles.d.ts +2 -2
- package/dist/core/tool-profiles.js +1 -0
- package/dist/core/usage-fields.d.ts +41 -0
- package/dist/core/usage-fields.js +150 -0
- package/dist/core/util/sleep.d.ts +3 -0
- package/dist/core/util/sleep.js +12 -0
- package/dist/installer/agent-config.d.ts +29 -0
- package/dist/installer/agent-config.js +75 -11
- package/dist/installer/agent-detect.d.ts +44 -0
- package/dist/installer/agent-detect.js +166 -0
- package/dist/installer/agent-table.d.ts +19 -0
- package/dist/installer/agent-table.js +130 -0
- package/dist/installer/setup-error.d.ts +19 -0
- package/dist/installer/setup-error.js +34 -0
- package/dist/installer/version-check.d.ts +5 -0
- package/dist/installer/version-check.js +21 -0
- package/dist/mcp/ops-surface.d.ts +7 -0
- package/dist/mcp/ops-surface.js +40 -3
- package/dist/mcp/server.d.ts +14 -4
- package/dist/mcp/server.js +42 -8
- package/dist/mcp/tool-surface.d.ts +8 -0
- package/dist/mcp/tool-surface.js +19 -2
- package/dist/mcp/tools/creator-tools.js +92 -14
- package/dist/mcp/tools/debug-tools.js +2 -0
- package/dist/mcp/tools/local-project-tools.d.ts +6 -0
- package/dist/mcp/tools/local-project-tools.js +33 -0
- package/dist/mcp/tools/navigation-tools.js +2 -1
- package/dist/mcp/tools/project-tools.js +5 -8
- package/dist/mcp/tools/seeing-tools.js +1 -1
- package/dist/mcp/tools/visual-tools.js +1 -1
- package/dist/mcp/tools/with-engine.js +1 -1
- package/dist/project-memory/agent-instructions.js +11 -3
- package/gemini-extension.json +3 -1
- package/library/references/mcp-tools-reference/mcp-tools-reference.md +18 -5
- package/library/skills/README.md +12 -8
- package/library/skills/animated-loop/SKILL.md +10 -16
- package/library/skills/cinematic-cutscene/SKILL.md +7 -18
- package/library/skills/headless-scripting/SKILL.md +7 -4
- package/library/skills/in-game-purchases/SKILL.md +25 -8
- package/library/skills/in-game-purchases/resource.yaml +2 -1
- package/library/skills/launch-ready/SKILL.md +351 -0
- package/library/skills/launch-ready/resource.yaml +45 -0
- package/library/skills/make-content/SKILL.md +202 -0
- package/library/skills/make-content/resource.yaml +32 -0
- package/library/skills/make-game/SKILL.md +6 -4
- package/library/skills/make-game/resource.yaml +2 -1
- package/library/skills/navigate-summer/SKILL.md +4 -2
- package/library/skills/new-project/SKILL.md +15 -22
- package/library/skills/new-project/resource.yaml +2 -1
- package/library/skills/painted-25d-world/SKILL.md +366 -0
- package/library/skills/painted-25d-world/_building-blocks/blender-route.md +33 -0
- package/library/skills/painted-25d-world/_building-blocks/blockout_builder.gd +149 -0
- package/library/skills/painted-25d-world/_building-blocks/capture_paint_view.gd +32 -0
- package/library/skills/painted-25d-world/_building-blocks/paint_projection.gdshader +23 -0
- package/library/skills/painted-25d-world/_building-blocks/paintcam_check.gd +25 -0
- package/library/skills/painted-25d-world/_building-blocks/painted_door.gd +17 -0
- package/library/skills/painted-25d-world/_building-blocks/painted_location.gd +56 -0
- package/library/skills/painted-25d-world/_building-blocks/painted_player.gd +125 -0
- package/library/skills/painted-25d-world/_building-blocks/painted_world.gd +111 -0
- package/library/skills/painted-25d-world/_building-blocks/tools/cam.py +57 -0
- package/library/skills/painted-25d-world/_building-blocks/tools/detail4k.py +81 -0
- package/library/skills/painted-25d-world/_building-blocks/tools/overlay.py +17 -0
- package/library/skills/painted-25d-world/_building-blocks/tools/preview.py +60 -0
- package/library/skills/painted-25d-world/resource.yaml +39 -0
- package/library/skills/publish-your-game/SKILL.md +13 -14
- package/library/skills/publish-your-game/resource.yaml +4 -3
- package/library/skills/release-checklist/SKILL.md +188 -0
- package/library/skills/release-checklist/resource.yaml +40 -0
- package/library/skills/release-readiness/SKILL.md +1 -1
- package/library/skills/release-readiness/resource.yaml +1 -1
- package/library/skills/store-art/SKILL.md +1 -1
- package/library/skills/store-art/resource.yaml +1 -1
- package/library/skills/store-listing/SKILL.md +1 -1
- package/library/skills/store-listing/resource.yaml +1 -1
- package/library/skills/summer-cli/SKILL.md +2 -0
- package/library/skills/summer-store/SKILL.md +295 -39
- package/library/skills/summer-store/resource.yaml +9 -3
- package/library/skills/trailer-shot/SKILL.md +3 -3
- package/library/templates/README.md +5 -0
- package/library/tools/content-make/resource.yaml +132 -0
- package/library/tools/frame-shot/GUIDE.md +1 -1
- package/library/tools/go-live/resource.yaml +67 -0
- package/library/tools/import-from-url/GUIDE.md +1 -0
- package/library/tools/import-from-url-batch/GUIDE.md +1 -1
- package/library/tools/play/GUIDE.md +2 -0
- package/library/tools/projects-create-local/resource.yaml +55 -0
- package/library/tools/projects-open-local/resource.yaml +51 -0
- package/library/tools/publish-build/resource.yaml +3 -3
- package/library/tools/screenshot/GUIDE.md +1 -1
- package/library/tools/store-set-details/resource.yaml +114 -0
- package/library/tools/sync-items/resource.yaml +72 -0
- package/package.json +1 -1
- package/registry/generated/aliases.json +6 -0
- package/registry/generated/counts.json +3 -3
- package/registry/generated/gemini-extension.json +3 -1
- package/registry/generated/index.json +463 -37
- package/registry/generated/marketplace.claude.json +1 -1
- package/registry/generated/plugin.claude-directory.json +4 -2
- package/registry/generated/plugin.claude.json +3 -1
- package/registry/generated/plugin.codex.json +5 -3
- package/registry/generated/plugin.cursor.json +4 -2
- package/registry/generated/plugin.factory.json +4 -2
- package/registry/generated/server.json +2 -2
- package/registry/generated/skills-index.md +12 -8
- package/registry/generated/skills-registry.json +62 -5
- package/registry/operations/legacy-names.json +40 -0
- package/registry/operations/public-catalog.json +10144 -387
package/dist/cli/commands/ops.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { CLI_AREAS } from "../../core/ops/areas.js";
|
|
3
|
-
import { localEntries, localInputSchema, localWireForm, missingInput } from "../../core/ops/catalog.js";
|
|
3
|
+
import { closestActions, localEntries, localInputSchema, localWireForm, missingInput, resolveCliOp } from "../../core/ops/catalog.js";
|
|
4
|
+
import { shippedCloudEntries, shippedWireForm } from "../../core/ops/public-catalog.js";
|
|
4
5
|
import { answerOfHosted, callHostedV4, describeFromShipped, hostedCallFailure, searchEverywhere } from "../../core/ops/cloud.js";
|
|
5
|
-
import { errorAnswer, isLocalOp, localToolsFor,
|
|
6
|
+
import { errorAnswer, isLocalOp, localToolsFor, OP_NAME, resolveOpName } from "../../core/ops/contract.js";
|
|
6
7
|
import { ENGINE_SPLIT_OPS, engineOpDef, splitToolArgs } from "../../core/ops/engine.js";
|
|
7
|
-
import { engineFailure, engineNotRunning } from "../../core/ops/engine-answer.js";
|
|
8
|
+
import { codedLocalFailure, engineFailure, engineNotRunning } from "../../core/ops/engine-answer.js";
|
|
8
9
|
import { loadLibraryIndex } from "../../core/library-search.js";
|
|
9
10
|
import { FileSaver, withOutField } from "../../core/ops/save-files.js";
|
|
10
11
|
import { dispatchTool, EngineUnavailableError, ToolResultError } from "../../core/capabilities/tool-dispatch.js";
|
|
@@ -113,8 +114,12 @@ async function runLocal(op, call, deps) {
|
|
|
113
114
|
const split = ENGINE_SPLIT_OPS[op];
|
|
114
115
|
const entry = localEntries().find((candidate) => candidate.name === op);
|
|
115
116
|
const tool = split?.tool ?? entry?.mcpTool ?? localToolsFor(op)[0];
|
|
116
|
-
if (!tool)
|
|
117
|
-
|
|
117
|
+
if (!tool) {
|
|
118
|
+
const [area] = op.split(".");
|
|
119
|
+
const closest = closestActions(op).map((action) => `summer ${area} ${action}`);
|
|
120
|
+
const hint = closest.length ? ` Closest: ${closest.join(", ")}. All of them: summer ${area} --help` : "";
|
|
121
|
+
return errorAnswer("not_found", `No operation is named "${op}".${hint}`, { next: { op: "ops.search", input: { query: op } } });
|
|
122
|
+
}
|
|
118
123
|
// maxCharacters cuts the text an MCP client gets; the CLI prints the whole answer.
|
|
119
124
|
const { maxCharacters, ...input } = call.input;
|
|
120
125
|
const uncut = maxCharacters === undefined ? undefined : ["maxCharacters: the CLI prints the whole answer; cut it in the shell (for example | head -c 8000)."];
|
|
@@ -135,7 +140,9 @@ async function runLocal(op, call, deps) {
|
|
|
135
140
|
if (error instanceof EngineUnavailableError)
|
|
136
141
|
return engineNotRunning();
|
|
137
142
|
if (error instanceof ToolResultError) {
|
|
138
|
-
|
|
143
|
+
if (def)
|
|
144
|
+
return engineFailure(JSON.stringify(error.result), op, { effect: def.effect, guide });
|
|
145
|
+
return codedLocalFailure(error.result) ?? errorAnswer("invalid_input", error.message, { details: { tool: error.result } });
|
|
139
146
|
}
|
|
140
147
|
return errorAnswer("invalid_input", error instanceof Error ? error.message : String(error), { next: describe });
|
|
141
148
|
}
|
|
@@ -196,12 +203,19 @@ async function waitFor(answer, deps) {
|
|
|
196
203
|
/** A value as one shell word: single quotes, so the shell expands nothing in it. */
|
|
197
204
|
const shellWord = (value) => (/^[\w./:@+=-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`);
|
|
198
205
|
/** The answer for a person; `out` is the --out to keep in the printed wait command. */
|
|
206
|
+
/** A failure's problem list (a manifest's fields), one per line: in the answer's details or its tool's structured result. */
|
|
207
|
+
function problemLines(details) {
|
|
208
|
+
const tool = details?.tool && typeof details.tool === "object" ? details.tool : undefined;
|
|
209
|
+
const problems = Array.isArray(details?.problems) ? details.problems : Array.isArray(tool?.problems) ? tool.problems : [];
|
|
210
|
+
const lines = problems.filter((problem) => typeof problem === "string");
|
|
211
|
+
return lines.length ? `\n${lines.map((problem) => ` - ${problem}`).join("\n")}` : "";
|
|
212
|
+
}
|
|
199
213
|
export function formatAnswer(answer, out) {
|
|
200
214
|
const waitFlags = (id) => `summer jobs wait --job-id ${id}${out ? ` --out ${shellWord(out)}` : ""}`;
|
|
201
215
|
if (!answer.ok) {
|
|
202
216
|
const next = answer.error.next;
|
|
203
217
|
const step = !next ? "" : "op" in next ? `\nNext: ${next.op}` : "url" in next ? `\nNext: ${next.label}: ${next.url}` : `\nNext: ${next.say}`;
|
|
204
|
-
return `Error (${answer.error.code}): ${answer.error.message}${step}`;
|
|
218
|
+
return `Error (${answer.error.code}): ${answer.error.message}${problemLines(answer.error.details)}${step}`;
|
|
205
219
|
}
|
|
206
220
|
switch (answer.status) {
|
|
207
221
|
case "done":
|
|
@@ -294,15 +308,89 @@ export async function describeFromCli(op, deps = defaultOpsCliDeps, options = {}
|
|
|
294
308
|
deps.printError(`Warning: ${warning}`);
|
|
295
309
|
return answer.ok ? 0 : 1;
|
|
296
310
|
}
|
|
311
|
+
const HELP_WORDS = new Set(["--help", "-h", "help"]);
|
|
312
|
+
/** gameId -> game-id, look_at -> look-at: the flag a field takes. */
|
|
313
|
+
const flagOf = (field) => field.replace(/_/g, "-").replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
314
|
+
const VIEW_FLAGS = "[--input '<json>'] [--dry-run] [--wait] [--json]";
|
|
315
|
+
/** `summer <area> --help`: the area's actions, from this package and the shipped catalog (no network). */
|
|
316
|
+
export function areaHelp(area) {
|
|
317
|
+
const titles = new Map();
|
|
318
|
+
for (const entry of [...localEntries(), ...shippedCloudEntries()]) {
|
|
319
|
+
if (entry.name.startsWith(`${area}.`) && !titles.has(entry.name))
|
|
320
|
+
titles.set(entry.name, entry.title);
|
|
321
|
+
}
|
|
322
|
+
const actions = [...titles].map(([name, title]) => [flagOf(name.slice(area.length + 1)), title]).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
323
|
+
const width = Math.max(0, ...actions.map(([action]) => action.length));
|
|
324
|
+
return [
|
|
325
|
+
`Usage: summer ${area} <action> [--<field> <value> ...] ${VIEW_FLAGS}`,
|
|
326
|
+
"",
|
|
327
|
+
...(actions.length ? ["Actions:", ...actions.map(([action, title]) => ` ${action.padEnd(width)} ${title}`)] : [`This version lists no ${area} operations; signed in, summer ops search ${area} finds the server's.`]),
|
|
328
|
+
"",
|
|
329
|
+
`One action's fields: summer ${area} <action> --help`,
|
|
330
|
+
].join("\n");
|
|
331
|
+
}
|
|
332
|
+
/** One operation's help: its command, what it does, who approves, the sign-in and each field as a flag. */
|
|
333
|
+
export function operationHelp(operation) {
|
|
334
|
+
const [area, action] = operation.name.split(".");
|
|
335
|
+
const command = action ? `summer ${area} ${flagOf(action)}` : `summer ${area}`;
|
|
336
|
+
const required = new Set(operation.input?.required ?? []);
|
|
337
|
+
const fields = Object.entries(operation.input?.properties ?? {}).map(([field, schema]) => {
|
|
338
|
+
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
339
|
+
const flag = `--${flagOf(field)}${type === "boolean" ? "" : ` <${type ?? "value"}>`}`;
|
|
340
|
+
const description = schema.description ?? "";
|
|
341
|
+
return [flag, `${required.has(field) ? "Required. " : ""}${description}`.trim()];
|
|
342
|
+
});
|
|
343
|
+
const width = Math.max(0, ...fields.map(([flag]) => flag.length));
|
|
344
|
+
const signIn = operation.auth === "account" ? "Needs a sign-in: summer login (the store sign-in alone: summer login --store)." : "Works signed out.";
|
|
345
|
+
return [
|
|
346
|
+
`Usage: ${command} ${fields.length ? "[--<field> <value> ...] " : ""}${VIEW_FLAGS}`,
|
|
347
|
+
"",
|
|
348
|
+
`${operation.title ?? operation.name}${operation.summary ? `: ${operation.summary}` : ""}`,
|
|
349
|
+
`Effect: ${operation.effect ?? "unknown"}. Approval: ${operation.approval ?? "none"}. ${signIn}`,
|
|
350
|
+
...(fields.length ? ["", "Fields:", ...fields.map(([flag, text]) => ` ${flag.padEnd(width)} ${text}`)] : []),
|
|
351
|
+
"",
|
|
352
|
+
`The exact input: summer ops describe ${operation.name}`,
|
|
353
|
+
].join("\n");
|
|
354
|
+
}
|
|
355
|
+
/** `summer <area> <action> --help`: from this package or the shipped catalog; the server only for an operation newer than both. */
|
|
356
|
+
export async function operationHelpFromCli(op, deps = defaultOpsCliDeps) {
|
|
357
|
+
const name = resolveOpName(op);
|
|
358
|
+
let operation = (localWireForm(name) ?? shippedWireForm(name));
|
|
359
|
+
if (!operation) {
|
|
360
|
+
const asked = await runCloud("ops.describe", { input: { name }, json: true, dryRun: false, wait: false }, deps);
|
|
361
|
+
if (!asked.ok || asked.status !== "done") {
|
|
362
|
+
deps.printError(formatAnswer(asked));
|
|
363
|
+
return 1;
|
|
364
|
+
}
|
|
365
|
+
operation = asked.result.operation ?? null;
|
|
366
|
+
if (!operation) {
|
|
367
|
+
deps.printError(formatAnswer(errorAnswer("not_found", `No operation is named "${name}".`, { next: { op: "ops.search", input: { query: name } } })));
|
|
368
|
+
return 1;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
deps.print(operationHelp(operation));
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
297
374
|
function areaCommand(area) {
|
|
298
375
|
return new Command(area)
|
|
299
376
|
.description(`Summer ${area} operations: summer ${area} <action> --field value. Find them: summer ops search ${area}`)
|
|
300
|
-
.argument("
|
|
377
|
+
.argument("[action]", "The operation's action, e.g. list or set-base")
|
|
301
378
|
.allowUnknownOption(true)
|
|
302
379
|
.allowExcessArguments(true)
|
|
303
380
|
.helpOption(false)
|
|
304
381
|
.action(async (action, _options, command) => {
|
|
305
|
-
|
|
382
|
+
// Our own help: commander's would list no actions and no fields.
|
|
383
|
+
if (action === undefined || HELP_WORDS.has(action)) {
|
|
384
|
+
(action === undefined ? defaultOpsCliDeps.printError : defaultOpsCliDeps.print)(areaHelp(area));
|
|
385
|
+
process.exitCode = action === undefined ? 1 : 0;
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const argv = command.args.slice(1);
|
|
389
|
+
// A 3.x tool name as the action runs its operation (resolveCliOp).
|
|
390
|
+
const op = resolveCliOp(area, action);
|
|
391
|
+
process.exitCode = argv.some((token) => HELP_WORDS.has(token) && token !== "help")
|
|
392
|
+
? await operationHelpFromCli(op)
|
|
393
|
+
: await runOpFromCli(op, argv);
|
|
306
394
|
});
|
|
307
395
|
}
|
|
308
396
|
export function opsCliCommands() {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* summer preview — the running game in a local web page, so an agent's own
|
|
3
|
+
* browser (Claude Code Desktop's Browser pane, the Codex app's in-app browser)
|
|
4
|
+
* can see it, screenshot it and check it after every edit.
|
|
5
|
+
*
|
|
6
|
+
* summer preview http://127.0.0.1:5199 (next free port if taken)
|
|
7
|
+
* summer preview --port 5300 exactly that port (Desktop rewrites this one)
|
|
8
|
+
* summer preview --open also open it in the default browser
|
|
9
|
+
*
|
|
10
|
+
* Run it in the folder the agent session works in: the project itself, a
|
|
11
|
+
* folder inside it, or a folder above it (a repo with the game in game/). The
|
|
12
|
+
* preview shows that project's editor only, and adds a summer-preview entry to
|
|
13
|
+
* <that folder>/.claude/launch.json (never changing other entries), which is
|
|
14
|
+
* where Desktop reads it when the session started there; Desktop then starts
|
|
15
|
+
* the preview itself and verifies changes with it. --no-launch-json skips that.
|
|
16
|
+
* The server, the page and the writer live in core/preview/.
|
|
17
|
+
*/
|
|
18
|
+
import { Command } from "commander";
|
|
19
|
+
import { type EngineSelection } from "../../core/engine.js";
|
|
20
|
+
import type { PreviewEngine } from "../../core/preview/preview-source.js";
|
|
21
|
+
import { type PreviewServer } from "../../core/preview/server.js";
|
|
22
|
+
export interface PreviewCommandOptions {
|
|
23
|
+
port?: string;
|
|
24
|
+
launchJson?: boolean;
|
|
25
|
+
open?: boolean;
|
|
26
|
+
json?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface PreviewIo {
|
|
29
|
+
cwd: string;
|
|
30
|
+
env: NodeJS.ProcessEnv;
|
|
31
|
+
out(line: string): void;
|
|
32
|
+
err(line: string): void;
|
|
33
|
+
connect(selection: EngineSelection | undefined): Promise<PreviewEngine>;
|
|
34
|
+
openUrl(url: string): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/** --port asks for exactly that port; without it, 5199 or the next free one.
|
|
37
|
+
* A PORT variable is not read: shells often export one for a web app, and
|
|
38
|
+
* Desktop passes the port it picked in --port. */
|
|
39
|
+
export declare function resolvePreviewPort(flag: string | undefined): {
|
|
40
|
+
port: number;
|
|
41
|
+
strict: boolean;
|
|
42
|
+
};
|
|
43
|
+
export interface PreviewProject {
|
|
44
|
+
/** The folder with project.godot, or null outside any project. */
|
|
45
|
+
projectRoot: string | null;
|
|
46
|
+
/** Where .claude/launch.json goes: the folder the command runs in. */
|
|
47
|
+
sessionRoot: string;
|
|
48
|
+
/** The entry's cwd when the project sits below the session folder. */
|
|
49
|
+
entryCwd?: string;
|
|
50
|
+
}
|
|
51
|
+
/** The project `summer preview` is for: at or above `cwd` (the same walk as
|
|
52
|
+
* `summer setup`), else the one project below it. Several below is an error. */
|
|
53
|
+
export declare function findPreviewProject(cwd: string): PreviewProject;
|
|
54
|
+
/** Start the preview; resolves with the running server (the command keeps it
|
|
55
|
+
* open until Ctrl-C). Throws on a bad --port, a taken explicit port, or
|
|
56
|
+
* several projects below the folder. */
|
|
57
|
+
export declare function runPreview(opts: PreviewCommandOptions, io: PreviewIo): Promise<PreviewServer>;
|
|
58
|
+
export declare const previewCommand: Command;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* summer preview — the running game in a local web page, so an agent's own
|
|
3
|
+
* browser (Claude Code Desktop's Browser pane, the Codex app's in-app browser)
|
|
4
|
+
* can see it, screenshot it and check it after every edit.
|
|
5
|
+
*
|
|
6
|
+
* summer preview http://127.0.0.1:5199 (next free port if taken)
|
|
7
|
+
* summer preview --port 5300 exactly that port (Desktop rewrites this one)
|
|
8
|
+
* summer preview --open also open it in the default browser
|
|
9
|
+
*
|
|
10
|
+
* Run it in the folder the agent session works in: the project itself, a
|
|
11
|
+
* folder inside it, or a folder above it (a repo with the game in game/). The
|
|
12
|
+
* preview shows that project's editor only, and adds a summer-preview entry to
|
|
13
|
+
* <that folder>/.claude/launch.json (never changing other entries), which is
|
|
14
|
+
* where Desktop reads it when the session started there; Desktop then starts
|
|
15
|
+
* the preview itself and verifies changes with it. --no-launch-json skips that.
|
|
16
|
+
* The server, the page and the writer live in core/preview/.
|
|
17
|
+
*/
|
|
18
|
+
import { Command } from "commander";
|
|
19
|
+
import { existsSync, readdirSync, realpathSync } from "node:fs";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { join, parse, relative, resolve, sep } from "node:path";
|
|
22
|
+
import open from "open";
|
|
23
|
+
import { EngineApiClient } from "../../core/api-client.js";
|
|
24
|
+
import { engineSelectionFromEnv } from "../../core/engine.js";
|
|
25
|
+
import { c, sym, tildeify } from "../../core/format.js";
|
|
26
|
+
import { DEFAULT_PREVIEW_PORT, previewLaunchEntry, writeClaudeLaunchConfig, } from "../../core/preview/launch-config.js";
|
|
27
|
+
import { PortInUseError, startPreviewServer } from "../../core/preview/server.js";
|
|
28
|
+
import { findSetupProjectRoot } from "../../project-memory/agent-instructions.js";
|
|
29
|
+
/** --port asks for exactly that port; without it, 5199 or the next free one.
|
|
30
|
+
* A PORT variable is not read: shells often export one for a web app, and
|
|
31
|
+
* Desktop passes the port it picked in --port. */
|
|
32
|
+
export function resolvePreviewPort(flag) {
|
|
33
|
+
if (flag === undefined || flag.trim() === "")
|
|
34
|
+
return { port: DEFAULT_PREVIEW_PORT, strict: false };
|
|
35
|
+
const port = Number(flag.trim());
|
|
36
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
37
|
+
throw new Error(`--port must be a whole number from 0 through 65535, got "${flag}".`);
|
|
38
|
+
}
|
|
39
|
+
return { port, strict: true };
|
|
40
|
+
}
|
|
41
|
+
const SEARCH_DEPTH = 3;
|
|
42
|
+
const SEARCH_MAX_DIRS = 2000;
|
|
43
|
+
/** Folders holding project.godot below `dir`: the shallowest level that has
|
|
44
|
+
* any (depth <= 3, no hidden folders or node_modules, at most 2000 read). */
|
|
45
|
+
function projectsBelow(dir) {
|
|
46
|
+
const found = [];
|
|
47
|
+
let read = 0;
|
|
48
|
+
let level = [dir];
|
|
49
|
+
for (let depth = 1; depth <= SEARCH_DEPTH && level.length > 0 && found.length === 0; depth += 1) {
|
|
50
|
+
const next = [];
|
|
51
|
+
for (const parent of level) {
|
|
52
|
+
if (++read > SEARCH_MAX_DIRS)
|
|
53
|
+
return found;
|
|
54
|
+
let names;
|
|
55
|
+
try {
|
|
56
|
+
names = readdirSync(parent, { withFileTypes: true })
|
|
57
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules")
|
|
58
|
+
.map((entry) => entry.name);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
for (const name of names) {
|
|
64
|
+
const child = join(parent, name);
|
|
65
|
+
if (existsSync(join(child, "project.godot")))
|
|
66
|
+
found.push(child);
|
|
67
|
+
else
|
|
68
|
+
next.push(child);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
level = next;
|
|
72
|
+
}
|
|
73
|
+
return found;
|
|
74
|
+
}
|
|
75
|
+
/** The project `summer preview` is for: at or above `cwd` (the same walk as
|
|
76
|
+
* `summer setup`), else the one project below it. Several below is an error. */
|
|
77
|
+
export function findPreviewProject(cwd) {
|
|
78
|
+
const sessionRoot = realpathSync(resolve(cwd));
|
|
79
|
+
const above = findSetupProjectRoot(sessionRoot);
|
|
80
|
+
if (above)
|
|
81
|
+
return { projectRoot: above, sessionRoot };
|
|
82
|
+
if (sessionRoot === realpathSync(homedir()) || sessionRoot === parse(sessionRoot).root) {
|
|
83
|
+
return { projectRoot: null, sessionRoot };
|
|
84
|
+
}
|
|
85
|
+
const below = projectsBelow(sessionRoot);
|
|
86
|
+
if (below.length > 1) {
|
|
87
|
+
throw new Error(`Several Summer projects below ${tildeify(sessionRoot)} (${below.map((dir) => relative(sessionRoot, dir)).join(", ")}). ` +
|
|
88
|
+
"Run summer preview inside the one to show.");
|
|
89
|
+
}
|
|
90
|
+
if (below.length === 0)
|
|
91
|
+
return { projectRoot: null, sessionRoot };
|
|
92
|
+
return { projectRoot: below[0], sessionRoot, entryCwd: relative(sessionRoot, below[0]).split(sep).join("/") };
|
|
93
|
+
}
|
|
94
|
+
function launchLine(result, project, skipped) {
|
|
95
|
+
if (!project.projectRoot) {
|
|
96
|
+
return `${sym.warn()} No Summer project here, above or below (no project.godot): .claude/launch.json not written.`;
|
|
97
|
+
}
|
|
98
|
+
if (skipped)
|
|
99
|
+
return `${c.dim("·")} .claude/launch.json not written (--no-launch-json).`;
|
|
100
|
+
if (!result)
|
|
101
|
+
return `${sym.warn()} .claude/launch.json not written.`;
|
|
102
|
+
const where = tildeify(result.path);
|
|
103
|
+
const reader = `Claude Code Desktop reads it when the session's folder is ${tildeify(project.sessionRoot)}`;
|
|
104
|
+
switch (result.status) {
|
|
105
|
+
case "created":
|
|
106
|
+
case "added":
|
|
107
|
+
return `${sym.ok()} ${result.status === "created" ? "Wrote" : "Added summer-preview to"} ${where}. ${reader}: its Browser pane then starts this preview and checks it after each edit.`;
|
|
108
|
+
case "kept":
|
|
109
|
+
return `${sym.ok()} ${where} already has summer-preview${result.autoVerify ? "" : " (auto-verify is off in that file)"}. ${reader}.`;
|
|
110
|
+
case "skipped":
|
|
111
|
+
return `${sym.warn()} ${where} left as it is: ${result.reason}. Add this entry to its configurations: ${JSON.stringify(result.entry)}`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Start the preview; resolves with the running server (the command keeps it
|
|
115
|
+
* open until Ctrl-C). Throws on a bad --port, a taken explicit port, or
|
|
116
|
+
* several projects below the folder. */
|
|
117
|
+
export async function runPreview(opts, io) {
|
|
118
|
+
const { port, strict } = resolvePreviewPort(opts.port);
|
|
119
|
+
const project = findPreviewProject(io.cwd);
|
|
120
|
+
// Only this project's editor: never another project's through the global pointer.
|
|
121
|
+
const selection = engineSelectionFromEnv(io.env) ?? (project.projectRoot ? { projectPath: project.projectRoot } : undefined);
|
|
122
|
+
let server;
|
|
123
|
+
try {
|
|
124
|
+
server = await startPreviewServer({ port, strictPort: strict, connect: () => io.connect(selection), log: io.err });
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
if (err instanceof PortInUseError) {
|
|
128
|
+
throw new Error(`Port ${err.port} is in use. Pass another --port (0 picks any free one), or stop what runs there.`);
|
|
129
|
+
}
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
let launch = null;
|
|
133
|
+
const skipped = opts.launchJson === false;
|
|
134
|
+
if (project.projectRoot && !skipped) {
|
|
135
|
+
try {
|
|
136
|
+
launch = writeClaudeLaunchConfig(project.sessionRoot, { entry: previewLaunchEntry(DEFAULT_PREVIEW_PORT, project.entryCwd) });
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
io.err(`${sym.warn()} .claude/launch.json not written: ${err instanceof Error ? err.message : String(err)}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (opts.json) {
|
|
143
|
+
io.out(JSON.stringify({ ok: true, url: server.url, port: server.port, projectRoot: project.projectRoot, launchJson: launch }));
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
io.out(`${sym.arrow()} ${c.bold("Summer preview")} ${server.url}`);
|
|
147
|
+
io.out(` Shows the running game, or the editor viewport when no game runs${project.projectRoot ? ` (${tildeify(project.projectRoot)})` : ""}. Ctrl-C to stop.`);
|
|
148
|
+
io.out(` ${launchLine(launch, project, skipped)}`);
|
|
149
|
+
}
|
|
150
|
+
if (opts.open)
|
|
151
|
+
await io.openUrl(server.url).catch(() => undefined);
|
|
152
|
+
return server;
|
|
153
|
+
}
|
|
154
|
+
function defaultIo() {
|
|
155
|
+
return {
|
|
156
|
+
cwd: process.cwd(),
|
|
157
|
+
env: process.env,
|
|
158
|
+
out: (line) => console.log(line),
|
|
159
|
+
err: (line) => console.error(line),
|
|
160
|
+
connect: (selection) => EngineApiClient.connect(selection),
|
|
161
|
+
openUrl: async (url) => {
|
|
162
|
+
await open(url);
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
export const previewCommand = new Command("preview")
|
|
167
|
+
.description("Show the running game (or the editor viewport) in a local web page an agent's browser can see; registers it in .claude/launch.json")
|
|
168
|
+
.option("--port <n>", `Port to serve on (default ${DEFAULT_PREVIEW_PORT}, or the next free one)`)
|
|
169
|
+
.option("--no-launch-json", "Do not add the summer-preview entry to .claude/launch.json in this folder")
|
|
170
|
+
.option("--open", "Also open the page in the default browser")
|
|
171
|
+
.option("--json", "Print one JSON line with the URL instead of text")
|
|
172
|
+
.action(async (opts) => {
|
|
173
|
+
const server = await runPreview(opts, defaultIo());
|
|
174
|
+
await new Promise((resolve) => {
|
|
175
|
+
const stop = () => resolve();
|
|
176
|
+
process.once("SIGINT", stop);
|
|
177
|
+
process.once("SIGTERM", stop);
|
|
178
|
+
});
|
|
179
|
+
await server.close();
|
|
180
|
+
process.exit(0);
|
|
181
|
+
});
|
|
@@ -1,19 +1,3 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
|
|
3
|
-
export type RunBinaryResolution = {
|
|
4
|
-
binary: string;
|
|
5
|
-
source: "--bin" | typeof ENGINE_BIN_ENV | "installed";
|
|
6
|
-
} | {
|
|
7
|
-
binary: null;
|
|
8
|
-
error: string;
|
|
9
|
-
};
|
|
10
|
-
/**
|
|
11
|
-
* Which engine executable `summer run` launches (and probes with --help):
|
|
12
|
-
* `--bin <path>`, else SUMMER_BIN, else the installed engine
|
|
13
|
-
* (findEngineBinary, which also honours the older SUMMER_ENGINE_BINARY).
|
|
14
|
-
* An explicit override is never silently swapped for the installed engine:
|
|
15
|
-
* a missing path or a bare `.app` bundle is an error, because the caller
|
|
16
|
-
* asked for THAT build.
|
|
17
|
-
*/
|
|
18
|
-
export declare function resolveRunBinary(binFlag: string | undefined, env?: NodeJS.ProcessEnv): RunBinaryResolution;
|
|
2
|
+
export { resolveRunBinary, type RunBinaryResolution } from "../../core/engine-launch.js";
|
|
19
3
|
export declare const runCommand: Command;
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -1,110 +1,12 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { spawn } from "child_process";
|
|
3
2
|
import { existsSync } from "fs";
|
|
4
|
-
import { mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
5
3
|
import { join, resolve } from "path";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const LAUNCH_LOCK_WAIT_MS = 15_000;
|
|
13
|
-
function isProcessAlive(pid) {
|
|
14
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
15
|
-
return false;
|
|
16
|
-
try {
|
|
17
|
-
process.kill(pid, 0);
|
|
18
|
-
return true;
|
|
19
|
-
}
|
|
20
|
-
catch (error) {
|
|
21
|
-
const code = error.code;
|
|
22
|
-
return code === "EPERM";
|
|
23
|
-
}
|
|
24
|
-
}
|
|
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 = !isProcessAlive(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
|
-
async function withLaunchLock(fn) {
|
|
45
|
-
const summerDir = getSummerDir();
|
|
46
|
-
await mkdir(summerDir, { recursive: true, mode: 0o700 });
|
|
47
|
-
const lockDir = join(summerDir, "launch.lock");
|
|
48
|
-
const ownerPath = join(lockDir, "owner.json");
|
|
49
|
-
const startedAt = Date.now();
|
|
50
|
-
while (true) {
|
|
51
|
-
try {
|
|
52
|
-
await mkdir(lockDir, { mode: 0o700 });
|
|
53
|
-
try {
|
|
54
|
-
await writeFile(ownerPath, JSON.stringify({ pid: process.pid, createdAt: Date.now() }, null, 2), { encoding: "utf-8", mode: 0o600 });
|
|
55
|
-
return await fn();
|
|
56
|
-
}
|
|
57
|
-
finally {
|
|
58
|
-
await rm(lockDir, { recursive: true, force: true });
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
catch (error) {
|
|
62
|
-
const code = error.code;
|
|
63
|
-
if (code !== "EEXIST") {
|
|
64
|
-
throw error;
|
|
65
|
-
}
|
|
66
|
-
if (await removeStaleLaunchLock(lockDir)) {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
if (Date.now() - startedAt > LAUNCH_LOCK_WAIT_MS) {
|
|
70
|
-
throw new Error("Another `summer run` launch is still in progress. Try again in a few seconds.");
|
|
71
|
-
}
|
|
72
|
-
await sleep(100);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Which engine executable `summer run` launches (and probes with --help):
|
|
78
|
-
* `--bin <path>`, else SUMMER_BIN, else the installed engine
|
|
79
|
-
* (findEngineBinary, which also honours the older SUMMER_ENGINE_BINARY).
|
|
80
|
-
* An explicit override is never silently swapped for the installed engine:
|
|
81
|
-
* a missing path or a bare `.app` bundle is an error, because the caller
|
|
82
|
-
* asked for THAT build.
|
|
83
|
-
*/
|
|
84
|
-
export function resolveRunBinary(binFlag, env = process.env) {
|
|
85
|
-
const fromEnv = engineBinaryOverride(env);
|
|
86
|
-
const explicit = binFlag?.trim()
|
|
87
|
-
? { source: "--bin", path: binFlag.trim() }
|
|
88
|
-
: fromEnv?.name === ENGINE_BIN_ENV
|
|
89
|
-
? { source: ENGINE_BIN_ENV, path: fromEnv.path }
|
|
90
|
-
: null;
|
|
91
|
-
if (explicit) {
|
|
92
|
-
const path = resolve(explicit.path);
|
|
93
|
-
const problem = describeEngineExecutableProblem(path, explicit.source);
|
|
94
|
-
return problem ? { binary: null, error: problem } : { binary: path, source: explicit.source };
|
|
95
|
-
}
|
|
96
|
-
const installed = findEngineBinary();
|
|
97
|
-
if (!installed) {
|
|
98
|
-
return {
|
|
99
|
-
binary: null,
|
|
100
|
-
error: "Summer Engine not found. Install it first:\n" +
|
|
101
|
-
" summer install\n" +
|
|
102
|
-
" or download from https://summerengine.com/download\n" +
|
|
103
|
-
`To launch a build that is not installed, pass --bin <executable> or set ${ENGINE_BIN_ENV}.`,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
return { binary: installed, source: "installed" };
|
|
107
|
-
}
|
|
4
|
+
import { launchEditor } from "../../core/engine-launch.js";
|
|
5
|
+
import { ENGINE_BIN_ENV } from "../../core/engine-install.js";
|
|
6
|
+
import { resolveLaunchPosture } from "../../core/launch-posture.js";
|
|
7
|
+
// The launch itself (lock, already-open check, posture flags, health wait)
|
|
8
|
+
// lives in core/engine-launch.ts, shared with `summer dev` and the MCP.
|
|
9
|
+
export { resolveRunBinary } from "../../core/engine-launch.js";
|
|
108
10
|
export const runCommand = new Command("run")
|
|
109
11
|
.description("Launch Summer Engine with a project. Without a path it launches a bare " +
|
|
110
12
|
"detached editor (project manager), which requires --no-project.")
|
|
@@ -148,108 +50,17 @@ export const runCommand = new Command("run")
|
|
|
148
50
|
process.exit(1);
|
|
149
51
|
}
|
|
150
52
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (!fullProjectPath || runningProjectPath === fullProjectPath) {
|
|
159
|
-
console.log(`Summer Engine is already running (v${health.version}) on port ${port}`);
|
|
160
|
-
if (health.project_name) {
|
|
161
|
-
console.log(` Project: ${health.project_name}`);
|
|
162
|
-
}
|
|
163
|
-
if (health.project_path) {
|
|
164
|
-
console.log(` Path: ${health.project_path}`);
|
|
165
|
-
}
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
console.log(`Summer Engine is already running (v${health.version}) on port ${port}`);
|
|
169
|
-
console.log(` Current project: ${health.project_path ?? health.project_name ?? "unknown"}`);
|
|
170
|
-
console.error(`Refusing to launch a second editor for: ${fullProjectPath}\n` +
|
|
171
|
-
"Close or switch the current Summer Engine project first, then run this command again.");
|
|
172
|
-
process.exitCode = 1;
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
const resolved = resolveRunBinary(opts.bin);
|
|
176
|
-
if (resolved.binary === null) {
|
|
177
|
-
console.error(resolved.error);
|
|
178
|
-
process.exitCode = 1;
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
const binary = resolved.binary;
|
|
182
|
-
if (resolved.source !== "installed") {
|
|
183
|
-
console.log(`Using engine from ${resolved.source}: ${binary}`);
|
|
184
|
-
}
|
|
185
|
-
// The engine is not running yet, so /api/health cannot tell us whether
|
|
186
|
-
// it honours --summer-background. Ask THAT binary itself (`--help`, which
|
|
187
|
-
// exits headless before any window exists; cached per install) with the
|
|
188
|
-
// installed version as pre-check and fallback; pass the flag only when
|
|
189
|
-
// the engine is known to support it. Focus launches have nothing to decide.
|
|
190
|
-
const support = posture === "background" ? await detectBackgroundLaunchSupport(binary) : backgroundLaunchSupport(null);
|
|
191
|
-
const plan = planLaunch(posture, support);
|
|
192
|
-
const args = ["--editor", ...plan.extraArgs];
|
|
193
|
-
if (fullProjectPath) {
|
|
194
|
-
args.unshift("--path", fullProjectPath);
|
|
195
|
-
}
|
|
196
|
-
if (plan.note)
|
|
197
|
-
console.log(plan.note);
|
|
198
|
-
console.log(plan.background
|
|
199
|
-
? "Launching Summer Engine in the background (the window will not take focus until you click it)..."
|
|
200
|
-
: "Launching Summer Engine...");
|
|
201
|
-
// Direct executable, never `open -a`: LaunchServices activates whatever
|
|
202
|
-
// it opens and would defeat --summer-background regardless of flags.
|
|
203
|
-
const child = spawn(binary, args, { detached: true, stdio: "ignore" });
|
|
204
|
-
// A stale or non-executable binary surfaces as an async "error" event
|
|
205
|
-
// (ENOENT/EACCES). Without a listener Node raises it as an uncaught
|
|
206
|
-
// exception with a raw stack; catch it and report the path instead.
|
|
207
|
-
const spawnState = { error: null };
|
|
208
|
-
child.on("error", (error) => {
|
|
209
|
-
spawnState.error = error;
|
|
210
|
-
});
|
|
211
|
-
child.unref();
|
|
212
|
-
// Wait for engine to start responding
|
|
213
|
-
const startTime = Date.now();
|
|
214
|
-
const timeout = 20000;
|
|
215
|
-
while (Date.now() - startTime < timeout) {
|
|
216
|
-
await sleep(500);
|
|
217
|
-
if (spawnState.error) {
|
|
218
|
-
reportSpawnFailure(spawnState.error, binary);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
const newPort = await getApiPort();
|
|
222
|
-
const h = await checkEngineHealth(newPort);
|
|
223
|
-
if (h) {
|
|
224
|
-
console.log(`Summer Engine running (v${h.version}) on port ${newPort}`);
|
|
225
|
-
if (h.project_name) {
|
|
226
|
-
console.log(` Project: ${h.project_name}`);
|
|
227
|
-
}
|
|
228
|
-
// Background was wanted but the flag was withheld, and the running
|
|
229
|
-
// engine now says (capabilities.launchPostures, else its version)
|
|
230
|
-
// that it would have honoured it: say so, so the next launch can be
|
|
231
|
-
// fixed rather than silently repeating a focus launch.
|
|
232
|
-
if (posture === "background" && !plan.background) {
|
|
233
|
-
const advert = advertisedBackgroundPosture(h.capabilities);
|
|
234
|
-
const wouldHonour = advert ?? backgroundLaunchSupport(h.version).supported;
|
|
235
|
-
if (wouldHonour) {
|
|
236
|
-
console.log(` Note: this engine (v${h.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.`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
if (spawnState.error) {
|
|
243
|
-
reportSpawnFailure(spawnState.error, binary);
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
53
|
+
const result = await launchEditor({
|
|
54
|
+
projectPath: fullProjectPath,
|
|
55
|
+
posture,
|
|
56
|
+
bin: opts.bin,
|
|
57
|
+
out: { log: (line) => console.log(line), error: (line) => console.error(line) },
|
|
58
|
+
});
|
|
59
|
+
if (result.status === "starting") {
|
|
246
60
|
console.log("Summer Engine launched but API not responding yet.\n" +
|
|
247
61
|
"It may still be loading. Run 'summer status' to check.");
|
|
248
|
-
}
|
|
62
|
+
}
|
|
63
|
+
else if (result.status !== "running") {
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
}
|
|
249
66
|
});
|
|
250
|
-
function reportSpawnFailure(error, binary) {
|
|
251
|
-
const code = error.code ?? error.message;
|
|
252
|
-
console.error(`Summer Engine binary failed to start: ${code} (${binary})\n` +
|
|
253
|
-
"Re-run 'summer install' to repair the installation, or check the binary path.");
|
|
254
|
-
process.exitCode = 1;
|
|
255
|
-
}
|