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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The extra fields a usage line (telemetry.ts, cli-usage.ts) can carry. Each
|
|
3
|
+
* is a closed shape checked here, so nothing a person typed, no path and no
|
|
4
|
+
* project or file name can reach one:
|
|
5
|
+
*
|
|
6
|
+
* - `projectIdHash`: the engine's own project id hash (sha256 of the random
|
|
7
|
+
* id Summer Engine keeps in `.summer/local/.project_id`), the same value
|
|
8
|
+
* Summer already uses to route requests to a project. Not the name or path.
|
|
9
|
+
* - `templateId`, `templateTags`: the library template a project started
|
|
10
|
+
* from (`.summer/project.json`) and that template's library domains
|
|
11
|
+
* (`facets.domains`, e.g. `2d`, `platformer`). Only ids from the shipped
|
|
12
|
+
* catalog; anything else is left out.
|
|
13
|
+
* - `instanceCount`: how many game clients `engine.play` started.
|
|
14
|
+
* - `engineState`: four yes/no answers when a call failed with
|
|
15
|
+
* `engine_not_running` (is the app installed, is an editor running, is one
|
|
16
|
+
* holding a project, is the person signed in), from local files only.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { currentToolContext } from "./agent-host.js";
|
|
21
|
+
import { hasEnvAuthToken } from "./auth.js";
|
|
22
|
+
import { listEngineInstances } from "./engine.js";
|
|
23
|
+
import { findEngineBinary } from "./engine-install.js";
|
|
24
|
+
import { loadLibraryIndex } from "./library-search.js";
|
|
25
|
+
import { getSummerDir } from "./store.js";
|
|
26
|
+
import { getTemplateRegistry } from "./templates.js";
|
|
27
|
+
import { readProjectManifest } from "../project-memory/project-manifest.js";
|
|
28
|
+
const HASH = /^[0-9a-f]{16,128}$/;
|
|
29
|
+
const SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
30
|
+
const MAX_TAGS = 8;
|
|
31
|
+
/** Only the closed shapes above survive; everything else is dropped. */
|
|
32
|
+
export function cleanUsageFields(fields) {
|
|
33
|
+
const out = {};
|
|
34
|
+
if (typeof fields.projectIdHash === "string" && HASH.test(fields.projectIdHash))
|
|
35
|
+
out.projectIdHash = fields.projectIdHash;
|
|
36
|
+
if (typeof fields.templateId === "string" && SLUG.test(fields.templateId))
|
|
37
|
+
out.templateId = fields.templateId;
|
|
38
|
+
if (Array.isArray(fields.templateTags)) {
|
|
39
|
+
const tags = fields.templateTags.filter((tag) => typeof tag === "string" && SLUG.test(tag)).slice(0, MAX_TAGS);
|
|
40
|
+
if (tags.length)
|
|
41
|
+
out.templateTags = tags;
|
|
42
|
+
}
|
|
43
|
+
if (typeof fields.instanceCount === "number" && Number.isInteger(fields.instanceCount) && fields.instanceCount >= 0 && fields.instanceCount <= 64) {
|
|
44
|
+
out.instanceCount = fields.instanceCount;
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
// One command per CLI process: its line reads these at exit (cli-usage.ts).
|
|
49
|
+
let processNotes = {};
|
|
50
|
+
let boundProjectIdHash;
|
|
51
|
+
/** A tool or command noted fields for its usage line; never throws. */
|
|
52
|
+
export function noteToolUsage(fields) {
|
|
53
|
+
try {
|
|
54
|
+
const clean = cleanUsageFields(fields);
|
|
55
|
+
const context = currentToolContext();
|
|
56
|
+
if (context)
|
|
57
|
+
Object.assign(context.usage, clean);
|
|
58
|
+
Object.assign(processNotes, clean);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// usage is best-effort
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** The fields noted in this process (the CLI line), with the bound project. */
|
|
65
|
+
export function processUsageFields() {
|
|
66
|
+
return { ...(boundProjectIdHash ? { projectIdHash: boundProjectIdHash } : {}), ...processNotes };
|
|
67
|
+
}
|
|
68
|
+
/** The engine connection bound this project (api-client.ts connect and rebind). */
|
|
69
|
+
export function noteBoundProject(projectIdHash) {
|
|
70
|
+
boundProjectIdHash = typeof projectIdHash === "string" && HASH.test(projectIdHash) ? projectIdHash : undefined;
|
|
71
|
+
}
|
|
72
|
+
export function boundProject() {
|
|
73
|
+
return boundProjectIdHash;
|
|
74
|
+
}
|
|
75
|
+
/** The library slug for a catalog template id or slug, else null. */
|
|
76
|
+
export function catalogTemplateSlug(idOrSlug) {
|
|
77
|
+
try {
|
|
78
|
+
const entry = getTemplateRegistry().find((template) => template.id === idOrSlug || template.slug === idOrSlug);
|
|
79
|
+
return entry ? entry.slug : null;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** A catalog template's library domains (`facets.domains`), e.g. ["2d", "platformer"]. */
|
|
86
|
+
export function templateTags(slug) {
|
|
87
|
+
try {
|
|
88
|
+
const entry = loadLibraryIndex().find((resource) => resource.id === `template/${slug}`);
|
|
89
|
+
return (entry?.facets?.domains ?? []).filter((tag) => SLUG.test(tag)).slice(0, MAX_TAGS);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const templateByHash = new Map();
|
|
96
|
+
/**
|
|
97
|
+
* The catalog template the project with this hash started from: the running
|
|
98
|
+
* editor's registry entry gives its folder, `.summer/project.json` the
|
|
99
|
+
* template. Cached per hash; null when unknown or not a catalog template.
|
|
100
|
+
*/
|
|
101
|
+
export async function templateForProject(projectIdHash) {
|
|
102
|
+
if (templateByHash.has(projectIdHash))
|
|
103
|
+
return templateByHash.get(projectIdHash) ?? null;
|
|
104
|
+
let slug = null;
|
|
105
|
+
try {
|
|
106
|
+
const instance = (await listEngineInstances()).find((entry) => entry.projectIdHash === projectIdHash);
|
|
107
|
+
const id = instance ? readProjectManifest(instance.resourceRoot)?.template?.id : undefined;
|
|
108
|
+
slug = typeof id === "string" ? catalogTemplateSlug(id) : null;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
slug = null;
|
|
112
|
+
}
|
|
113
|
+
templateByHash.set(projectIdHash, slug);
|
|
114
|
+
return slug;
|
|
115
|
+
}
|
|
116
|
+
/** What is true on this machine when the engine could not be reached. Local files only. */
|
|
117
|
+
export async function engineState() {
|
|
118
|
+
const instances = await listEngineInstances(Date.now(), getSummerDir(), { anyHeartbeat: true }).catch(() => []);
|
|
119
|
+
return {
|
|
120
|
+
desktopInstalled: findEngineBinary() !== null,
|
|
121
|
+
engineRunning: instances.length > 0,
|
|
122
|
+
projectOpen: instances.some((instance) => !!instance.projectIdHash),
|
|
123
|
+
signedIn: hasEnvAuthToken() || existsSync(join(getSummerDir(), "auth-token")),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The extra fields of one usage line: what the call noted, else the bound
|
|
128
|
+
* project and its template, and the engine state for engine_not_running.
|
|
129
|
+
* Never throws.
|
|
130
|
+
*/
|
|
131
|
+
export async function usageLineFields(noted, error) {
|
|
132
|
+
try {
|
|
133
|
+
const fields = cleanUsageFields(noted);
|
|
134
|
+
if (fields.projectIdHash && !fields.templateId) {
|
|
135
|
+
const template = await templateForProject(fields.projectIdHash);
|
|
136
|
+
if (template)
|
|
137
|
+
fields.templateId = template;
|
|
138
|
+
}
|
|
139
|
+
return { ...fields, ...(error === "engine_not_running" ? { engineState: await engineState() } : {}) };
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Test-only: forget notes, the bound project and the template cache. */
|
|
146
|
+
export function _resetUsageFieldsForTests() {
|
|
147
|
+
processNotes = {};
|
|
148
|
+
boundProjectIdHash = undefined;
|
|
149
|
+
templateByHash.clear();
|
|
150
|
+
}
|
|
@@ -1 +1,4 @@
|
|
|
1
1
|
export declare function sleep(ms: number): Promise<void>;
|
|
2
|
+
/** Whether `promise` settles within `ms`. The timer is cleared when it does,
|
|
3
|
+
* so a quick answer never keeps a CLI process alive for the full wait. */
|
|
4
|
+
export declare function settlesWithin(promise: Promise<unknown>, ms: number): Promise<boolean>;
|
package/dist/core/util/sleep.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
export function sleep(ms) {
|
|
2
2
|
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
3
3
|
}
|
|
4
|
+
/** Whether `promise` settles within `ms`. The timer is cleared when it does,
|
|
5
|
+
* so a quick answer never keeps a CLI process alive for the full wait. */
|
|
6
|
+
export function settlesWithin(promise, ms) {
|
|
7
|
+
return new Promise((resolveWithin) => {
|
|
8
|
+
const timer = setTimeout(() => resolveWithin(false), ms);
|
|
9
|
+
const done = () => {
|
|
10
|
+
clearTimeout(timer);
|
|
11
|
+
resolveWithin(true);
|
|
12
|
+
};
|
|
13
|
+
promise.then(done, done);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
550
|
+
throw new SetupError("config_invalid", `Could not parse JSON in ${path}: ${formatError(error)}`);
|
|
487
551
|
}
|
|
488
552
|
if (!isJsonObject(parsed)) {
|
|
489
|
-
throw new
|
|
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
|
|
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
|
|
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[];
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `summer setup --detect`: which agents from agent-table.ts are on this
|
|
3
|
+
* computer, where each one's MCP config file is, and whether it already lists
|
|
4
|
+
* summer-engine. Reads only; never writes.
|
|
5
|
+
*
|
|
6
|
+
* How an agent counts as installed (any one hit; `detectedBy` names each):
|
|
7
|
+
* - bin: one of its commands is a file on PATH, or in a usual install folder
|
|
8
|
+
* (~/.local/bin, /opt/homebrew/bin, %APPDATA%\npm, ...). A desktop app's
|
|
9
|
+
* installer runs with a short PATH that leaves those out, so they are
|
|
10
|
+
* checked by name. On Windows each name is tried with every PATHEXT ending.
|
|
11
|
+
* - app: a macOS app bundle in /Applications or ~/Applications, or a Windows
|
|
12
|
+
* executable under %LOCALAPPDATA%, %ProgramFiles% or %ProgramFiles(x86)%.
|
|
13
|
+
* - folder: a config, data or install folder the agent makes on first run.
|
|
14
|
+
* - extension: a VS Code extension folder in ~/.vscode/extensions.
|
|
15
|
+
* The per-agent names live in agent-table.ts (`detect`).
|
|
16
|
+
*/
|
|
17
|
+
import { accessSync, constants, existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
import { allAgentSpecs, defaultPathContext, resolveMcpPath, } from "./agent-table.js";
|
|
20
|
+
import { listsSummerEngine, readRecordedMcpServer } from "./version-check.js";
|
|
21
|
+
import { toStepError } from "./setup-error.js";
|
|
22
|
+
export function detectAgents(options = {}) {
|
|
23
|
+
const ctx = options.ctx ?? defaultPathContext();
|
|
24
|
+
const binDirs = searchedBinDirs(ctx, options.systemBinDirs);
|
|
25
|
+
const macAppDirs = options.macAppDirs ?? ["/Applications", join(ctx.home, "Applications")];
|
|
26
|
+
return allAgentSpecs().map((spec) => detectAgent(spec, ctx, binDirs, macAppDirs));
|
|
27
|
+
}
|
|
28
|
+
function detectAgent(spec, ctx, binDirs, macAppDirs) {
|
|
29
|
+
const detectedBy = [
|
|
30
|
+
...(spec.detect.bins ?? []).flatMap((name) => findBin(name, binDirs, ctx)).map((path) => ({ kind: "bin", path })),
|
|
31
|
+
...findApps(spec, ctx, macAppDirs).map((path) => ({ kind: "app", path })),
|
|
32
|
+
...(spec.detect.dirs?.(ctx) ?? []).filter((path) => existsSync(path)).map((path) => ({ kind: "folder", path })),
|
|
33
|
+
...findVsCodeExtensions(spec.detect.vscodeExtensions ?? [], ctx).map((path) => ({ kind: "extension", path })),
|
|
34
|
+
];
|
|
35
|
+
const unique = detectedBy.filter((hit, index) => detectedBy.findIndex((other) => other.path === hit.path) === index);
|
|
36
|
+
const resolved = resolveMcpPath(spec, "user", ctx);
|
|
37
|
+
const override = ctx.env[spec.envOverride];
|
|
38
|
+
const configPath = override || resolved.path;
|
|
39
|
+
const configExists = existsSync(configPath);
|
|
40
|
+
let mcpEntry = false;
|
|
41
|
+
let mcpServer = null;
|
|
42
|
+
let configError;
|
|
43
|
+
if (configExists) {
|
|
44
|
+
try {
|
|
45
|
+
const text = readFileSync(configPath, "utf-8");
|
|
46
|
+
mcpEntry = listsSummerEngine(text, spec.format);
|
|
47
|
+
mcpServer = readRecordedMcpServer(text, spec.format);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
configError = toStepError(error, "config_unreadable");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
id: spec.id,
|
|
55
|
+
label: spec.label,
|
|
56
|
+
status: spec.status,
|
|
57
|
+
installed: unique.length > 0,
|
|
58
|
+
programFound: unique.some((hit) => hit.kind !== "folder"),
|
|
59
|
+
detectedBy: unique,
|
|
60
|
+
configPath,
|
|
61
|
+
configScope: resolved.scope,
|
|
62
|
+
configFormat: spec.format,
|
|
63
|
+
configExists,
|
|
64
|
+
mcpEntry,
|
|
65
|
+
mcpServer,
|
|
66
|
+
summerGames: Boolean(spec.remoteMcp),
|
|
67
|
+
skills: spec.skills !== null,
|
|
68
|
+
...(configError ? { configError } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** PATH first, then the install folders a GUI-launched process's PATH leaves out. */
|
|
72
|
+
export function searchedBinDirs(ctx, systemBinDirs = ["/opt/homebrew/bin", "/usr/local/bin"]) {
|
|
73
|
+
const path = ctx.env.PATH ?? ctx.env.Path ?? "";
|
|
74
|
+
const sep = ctx.os === "win32" ? ";" : ":";
|
|
75
|
+
const fromPath = path.split(sep).filter(Boolean);
|
|
76
|
+
const home = ctx.home;
|
|
77
|
+
const usual = ctx.os === "win32"
|
|
78
|
+
? [
|
|
79
|
+
join(home, ".local", "bin"),
|
|
80
|
+
join(ctx.env.APPDATA ?? join(home, "AppData", "Roaming"), "npm"),
|
|
81
|
+
join(localAppData(ctx), "Microsoft", "WinGet", "Links"),
|
|
82
|
+
join(localAppData(ctx), "pnpm"),
|
|
83
|
+
join(home, ".bun", "bin"),
|
|
84
|
+
join(home, ".volta", "bin"),
|
|
85
|
+
join(home, ".lmstudio", "bin"),
|
|
86
|
+
]
|
|
87
|
+
: [
|
|
88
|
+
join(home, ".local", "bin"),
|
|
89
|
+
join(home, "bin"),
|
|
90
|
+
...systemBinDirs,
|
|
91
|
+
join(home, ".npm-global", "bin"),
|
|
92
|
+
join(home, ".bun", "bin"),
|
|
93
|
+
join(home, ".volta", "bin"),
|
|
94
|
+
join(home, "Library", "pnpm"),
|
|
95
|
+
join(home, ".local", "share", "pnpm"),
|
|
96
|
+
join(home, ".claude", "local"),
|
|
97
|
+
join(home, ".opencode", "bin"),
|
|
98
|
+
join(home, ".lmstudio", "bin"),
|
|
99
|
+
...nvmBinDirs(home),
|
|
100
|
+
];
|
|
101
|
+
return [...new Set([...fromPath, ...usual])];
|
|
102
|
+
}
|
|
103
|
+
/** ~/.nvm/versions/node/<version>/bin for each installed Node (npm -g installs land there). */
|
|
104
|
+
function nvmBinDirs(home) {
|
|
105
|
+
const root = join(home, ".nvm", "versions", "node");
|
|
106
|
+
try {
|
|
107
|
+
return readdirSync(root).map((version) => join(root, version, "bin"));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function localAppData(ctx) {
|
|
114
|
+
return ctx.env.LOCALAPPDATA ?? join(ctx.home, "AppData", "Local");
|
|
115
|
+
}
|
|
116
|
+
function findBin(name, dirs, ctx) {
|
|
117
|
+
const names = ctx.os === "win32"
|
|
118
|
+
? [name, ...(ctx.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean).map((ext) => `${name}${ext.toLowerCase()}`)]
|
|
119
|
+
: [name];
|
|
120
|
+
const hits = [];
|
|
121
|
+
for (const dir of dirs) {
|
|
122
|
+
for (const candidate of names) {
|
|
123
|
+
const path = join(dir, candidate);
|
|
124
|
+
if (isRunnable(path, ctx)) {
|
|
125
|
+
hits.push(path);
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return hits;
|
|
131
|
+
}
|
|
132
|
+
function isRunnable(path, ctx) {
|
|
133
|
+
try {
|
|
134
|
+
if (!statSync(path).isFile())
|
|
135
|
+
return false;
|
|
136
|
+
if (ctx.os !== "win32")
|
|
137
|
+
accessSync(path, constants.X_OK);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function findApps(spec, ctx, macAppDirs) {
|
|
145
|
+
if (ctx.os === "darwin") {
|
|
146
|
+
return (spec.detect.macApps ?? []).flatMap((app) => macAppDirs.map((dir) => join(dir, app))).filter((path) => existsSync(path));
|
|
147
|
+
}
|
|
148
|
+
if (ctx.os === "win32") {
|
|
149
|
+
const roots = [localAppData(ctx), ctx.env.ProgramFiles, ctx.env["ProgramFiles(x86)"]].filter((root) => Boolean(root));
|
|
150
|
+
return (spec.detect.winApps ?? []).flatMap((exe) => roots.map((root) => join(root, exe))).filter((path) => existsSync(path));
|
|
151
|
+
}
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
function findVsCodeExtensions(prefixes, ctx) {
|
|
155
|
+
if (prefixes.length === 0)
|
|
156
|
+
return [];
|
|
157
|
+
const dir = join(ctx.home, ".vscode", "extensions");
|
|
158
|
+
let entries;
|
|
159
|
+
try {
|
|
160
|
+
entries = readdirSync(dir);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
return entries.filter((entry) => prefixes.some((prefix) => entry.toLowerCase().startsWith(prefix))).map((entry) => join(dir, entry));
|
|
166
|
+
}
|
|
@@ -101,6 +101,25 @@ export interface AgentSpec {
|
|
|
101
101
|
/** null: no skills/rules mechanism — MCP only. */
|
|
102
102
|
skills: SkillHome | null;
|
|
103
103
|
noSkillsNote?: string;
|
|
104
|
+
/** How `summer setup --detect` tells the agent is on this computer (agent-detect.ts). */
|
|
105
|
+
detect: AgentDetect;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Where an agent shows up once it is installed, on macOS and Windows (Linux
|
|
109
|
+
* uses the bins and folders). Any one hit counts as installed; `--detect`
|
|
110
|
+
* names each hit, so a caller can tell a live binary from a leftover folder.
|
|
111
|
+
*/
|
|
112
|
+
export interface AgentDetect {
|
|
113
|
+
/** Commands: looked up on PATH and in the usual install folders a GUI app's short PATH misses (agent-detect.ts). */
|
|
114
|
+
bins?: readonly string[];
|
|
115
|
+
/** macOS app bundles, looked up in /Applications and ~/Applications. */
|
|
116
|
+
macApps?: readonly string[];
|
|
117
|
+
/** Windows executables, relative to %LOCALAPPDATA%, %ProgramFiles% and %ProgramFiles(x86)%. */
|
|
118
|
+
winApps?: readonly string[];
|
|
119
|
+
/** Folders the agent creates on install or first run (config, data or install folder). */
|
|
120
|
+
dirs?: (ctx: PathContext) => string[];
|
|
121
|
+
/** VS Code extension id prefixes, looked up in ~/.vscode/extensions (folders named <id>-<version>). */
|
|
122
|
+
vscodeExtensions?: readonly string[];
|
|
104
123
|
}
|
|
105
124
|
/** True when this location is the shared agentskills.io folder. */
|
|
106
125
|
export declare function isSharedSkillsPath(path: string): boolean;
|