summer-engine 2.3.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +24 -8
  3. package/.codex-plugin/plugin.json +19 -8
  4. package/.cursor-plugin/plugin.json +18 -7
  5. package/.factory-plugin/plugin.json +3 -3
  6. package/.opencode/INSTALL.md +1 -1
  7. package/AGENTS.md +6 -4
  8. package/CHANGELOG.md +46 -7
  9. package/CLAUDE.md +2 -2
  10. package/GEMINI.md +5 -6
  11. package/README.md +116 -109
  12. package/_persona/summer/SKILL.md +5 -6
  13. package/commands/gameskill.md +103 -0
  14. package/dist/bin/summer.js +17 -4
  15. package/dist/commands/cloud.d.ts +2 -0
  16. package/dist/commands/cloud.js +80 -0
  17. package/dist/commands/create.js +2 -0
  18. package/dist/commands/doctor.js +90 -1
  19. package/dist/commands/install.d.ts +17 -0
  20. package/dist/commands/install.js +63 -4
  21. package/dist/commands/login.js +4 -1
  22. package/dist/commands/logout.js +1 -1
  23. package/dist/commands/memory.d.ts +14 -0
  24. package/dist/commands/memory.js +196 -0
  25. package/dist/commands/open.js +1 -1
  26. package/dist/commands/plan.d.ts +2 -0
  27. package/dist/commands/plan.js +60 -0
  28. package/dist/commands/run.js +132 -35
  29. package/dist/commands/setup.js +8 -2
  30. package/dist/commands/skills.js +90 -6
  31. package/dist/commands/status.js +3 -0
  32. package/dist/lib/agent-config.d.ts +2 -2
  33. package/dist/lib/agent-config.js +134 -7
  34. package/dist/lib/api-client.d.ts +13 -0
  35. package/dist/lib/api-client.js +63 -26
  36. package/dist/lib/async-op-lifecycle.d.ts +60 -0
  37. package/dist/lib/async-op-lifecycle.js +83 -0
  38. package/dist/lib/auth.d.ts +2 -0
  39. package/dist/lib/auth.js +13 -0
  40. package/dist/lib/cloud/api.d.ts +79 -0
  41. package/dist/lib/cloud/api.js +107 -0
  42. package/dist/lib/cloud/apply.d.ts +32 -0
  43. package/dist/lib/cloud/apply.js +294 -0
  44. package/dist/lib/cloud/checkpoint.d.ts +26 -0
  45. package/dist/lib/cloud/checkpoint.js +142 -0
  46. package/dist/lib/cloud/conflicts.d.ts +55 -0
  47. package/dist/lib/cloud/conflicts.js +131 -0
  48. package/dist/lib/cloud/containment.d.ts +8 -0
  49. package/dist/lib/cloud/containment.js +52 -0
  50. package/dist/lib/cloud/diff.d.ts +39 -0
  51. package/dist/lib/cloud/diff.js +138 -0
  52. package/dist/lib/cloud/engine-bridge.d.ts +21 -0
  53. package/dist/lib/cloud/engine-bridge.js +105 -0
  54. package/dist/lib/cloud/hash-cache.d.ts +23 -0
  55. package/dist/lib/cloud/hash-cache.js +44 -0
  56. package/dist/lib/cloud/hash.d.ts +10 -0
  57. package/dist/lib/cloud/hash.js +111 -0
  58. package/dist/lib/cloud/http.d.ts +17 -0
  59. package/dist/lib/cloud/http.js +47 -0
  60. package/dist/lib/cloud/lock-path.d.ts +1 -0
  61. package/dist/lib/cloud/lock-path.js +5 -0
  62. package/dist/lib/cloud/lock.d.ts +14 -0
  63. package/dist/lib/cloud/lock.js +143 -0
  64. package/dist/lib/cloud/manifest.d.ts +6 -0
  65. package/dist/lib/cloud/manifest.js +41 -0
  66. package/dist/lib/cloud/paths.d.ts +20 -0
  67. package/dist/lib/cloud/paths.js +90 -0
  68. package/dist/lib/cloud/project-godot-merge.d.ts +32 -0
  69. package/dist/lib/cloud/project-godot-merge.js +185 -0
  70. package/dist/lib/cloud/rules.d.ts +39 -0
  71. package/dist/lib/cloud/rules.js +159 -0
  72. package/dist/lib/cloud/sync.d.ts +21 -0
  73. package/dist/lib/cloud/sync.js +518 -0
  74. package/dist/lib/cloud/transfer.d.ts +23 -0
  75. package/dist/lib/cloud/transfer.js +184 -0
  76. package/dist/lib/cloud/types.d.ts +62 -0
  77. package/dist/lib/cloud/validate-path.d.ts +23 -0
  78. package/dist/lib/cloud/validate-path.js +103 -0
  79. package/dist/lib/engine.d.ts +6 -0
  80. package/dist/lib/engine.js +36 -1
  81. package/dist/lib/game-task-plan.d.ts +42 -0
  82. package/dist/lib/game-task-plan.js +376 -0
  83. package/dist/lib/project-memory.d.ts +33 -0
  84. package/dist/lib/project-memory.js +220 -0
  85. package/dist/lib/skills-registry.d.ts +134 -61
  86. package/dist/lib/skills-registry.js +102 -4
  87. package/dist/lib/version-check.d.ts +106 -0
  88. package/dist/lib/version-check.js +338 -0
  89. package/dist/mcp/server.d.ts +2 -0
  90. package/dist/mcp/server.js +83 -0
  91. package/dist/mcp/tools/asset-tools.js +265 -45
  92. package/dist/mcp/tools/cloud-tools.d.ts +2 -0
  93. package/dist/mcp/tools/cloud-tools.js +42 -0
  94. package/dist/mcp/tools/debug-tools.js +32 -2
  95. package/dist/mcp/tools/generate-tools.js +101 -13
  96. package/dist/mcp/tools/project-tools.js +76 -21
  97. package/dist/mcp/tools/with-engine.d.ts +14 -0
  98. package/dist/mcp/tools/with-engine.js +52 -7
  99. package/gemini-extension.json +3 -3
  100. package/hooks/pre-commit-doctor.sh +0 -0
  101. package/hooks/run-hook.cmd +0 -0
  102. package/hooks/session-start.sh +0 -0
  103. package/package.json +20 -7
  104. package/skills/2d-assets/character-portrait/SKILL.md +1 -1
  105. package/skills/2d-assets/concept-art/SKILL.md +1 -1
  106. package/skills/2d-assets/create-asset-sheet/SKILL.md +163 -0
  107. package/skills/2d-assets/instantiate-asset-pack/SKILL.md +245 -0
  108. package/skills/2d-assets/pixel-art/SKILL.md +1 -1
  109. package/skills/2d-assets/skybox-panorama/SKILL.md +1 -1
  110. package/skills/2d-assets/sprite-sheet/SKILL.md +1 -1
  111. package/skills/2d-assets/tileable-texture/SKILL.md +1 -1
  112. package/skills/2d-assets/ui-graphics/SKILL.md +1 -1
  113. package/skills/2d-assets/use-widget-asset/SKILL.md +217 -0
  114. package/skills/3d-assets/character-model/SKILL.md +28 -6
  115. package/skills/3d-assets/environment-kit/SKILL.md +7 -2
  116. package/skills/3d-assets/organic-model/SKILL.md +5 -4
  117. package/skills/3d-assets/prop-model/SKILL.md +17 -9
  118. package/skills/3d-assets/vehicle-model/SKILL.md +5 -4
  119. package/skills/README.md +37 -30
  120. package/skills/ai-and-npcs/design-npc/SKILL.md +6 -6
  121. package/skills/animation/animation-tree/SKILL.md +4 -2
  122. package/skills/animation/facial-and-lipsync/SKILL.md +1 -1
  123. package/skills/animation/generate-motion/SKILL.md +26 -5
  124. package/skills/animation/procedural-animation/SKILL.md +2 -2
  125. package/skills/asset-pipeline/asset-strategy/SKILL.md +3 -2
  126. package/skills/audio/adaptive-music/SKILL.md +1 -1
  127. package/skills/audio/audio-direction/SKILL.md +5 -5
  128. package/skills/audio/sound-effect/SKILL.md +2 -2
  129. package/skills/audio/voice-line/SKILL.md +25 -3
  130. package/skills/character-controllers/fps-controller/SKILL.md +5 -5
  131. package/skills/debugging/debug/SKILL.md +39 -1
  132. package/skills/deployment/export-and-ship/SKILL.md +4 -4
  133. package/skills/gameplay-mechanics/auto-fire-targeting/SKILL.md +170 -0
  134. package/skills/gameplay-mechanics/design-mechanic/SKILL.md +8 -8
  135. package/skills/level-design/design-level/SKILL.md +5 -5
  136. package/skills/level-design/scene-to-level/SKILL.md +125 -0
  137. package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +4 -4
  138. package/skills/multiplayer-and-networking/peer-to-peer-multiplayer/SKILL.md +4 -4
  139. package/skills/multiplayer-and-networking/setup-multiplayer/SKILL.md +7 -7
  140. package/skills/performance/tune-performance/SKILL.md +5 -5
  141. package/skills/rendering-and-lighting/3d-lighting/SKILL.md +1 -1
  142. package/skills/rendering-and-lighting/art-direction/SKILL.md +5 -5
  143. package/skills/scene-and-project/brainstorm-game/SKILL.md +4 -4
  144. package/skills/scene-and-project/play/SKILL.md +1 -1
  145. package/skills/scene-and-project/scene-composition/SKILL.md +30 -1
  146. package/skills/scripting-patterns/gdscript-patterns/SKILL.md +53 -0
  147. package/skills/ui-and-ux/ui-basics/SKILL.md +1 -1
  148. package/skills/video/animated-loop/SKILL.md +1 -1
  149. package/skills/video/cinematic-cutscene/SKILL.md +1 -1
  150. package/skills/video/trailer-shot/SKILL.md +1 -1
  151. package/skills/visual-effects/game-feel/SKILL.md +6 -6
  152. package/skills/workflow/brainstorming/SKILL.md +164 -0
  153. package/skills/workflow/brainstorming/scripts/frame-template.html +214 -0
  154. package/skills/workflow/brainstorming/scripts/helper.js +88 -0
  155. package/skills/workflow/brainstorming/scripts/server.cjs +354 -0
  156. package/skills/workflow/brainstorming/scripts/start-server.sh +148 -0
  157. package/skills/workflow/brainstorming/scripts/stop-server.sh +56 -0
  158. package/skills/workflow/brainstorming/spec-document-reviewer-prompt.md +49 -0
  159. package/skills/workflow/brainstorming/visual-companion.md +287 -0
  160. package/skills/workflow/debugging-game-feel/SKILL.md +174 -0
  161. package/skills/workflow/diagnosing-perf-regressions/SKILL.md +190 -0
  162. package/skills/workflow/dispatching-parallel-agents/SKILL.md +182 -0
  163. package/skills/workflow/gameskill/SKILL.md +111 -0
  164. package/skills/workflow/investigating-bugs/CREATION-LOG.md +119 -0
  165. package/skills/workflow/investigating-bugs/SKILL.md +296 -0
  166. package/skills/workflow/investigating-bugs/condition-based-waiting-example.ts +158 -0
  167. package/skills/workflow/investigating-bugs/condition-based-waiting.md +115 -0
  168. package/skills/workflow/investigating-bugs/defense-in-depth.md +122 -0
  169. package/skills/workflow/investigating-bugs/find-polluter.sh +63 -0
  170. package/skills/workflow/investigating-bugs/root-cause-tracing.md +169 -0
  171. package/skills/workflow/investigating-bugs/test-academic.md +14 -0
  172. package/skills/workflow/investigating-bugs/test-pressure-1.md +58 -0
  173. package/skills/workflow/investigating-bugs/test-pressure-2.md +68 -0
  174. package/skills/workflow/investigating-bugs/test-pressure-3.md +69 -0
  175. package/skills/workflow/mcpupdate/SKILL.md +134 -0
  176. package/skills/workflow/playtesting-a-feature/SKILL.md +173 -0
  177. package/skills/{_meta → workflow}/skill-create/SKILL.md +32 -22
  178. package/skills/{_meta → workflow}/skill-improve/SKILL.md +9 -10
  179. package/skills/{_meta → workflow}/skill-test/SKILL.md +12 -12
  180. package/skills/{_meta → workflow}/using-summer/SKILL.md +5 -2
  181. package/skills/workflow/verification-before-completion/SKILL.md +139 -0
  182. package/skills/workflow/writing-plans/SKILL.md +152 -0
  183. package/skills/workflow/writing-plans/plan-document-reviewer-prompt.md +49 -0
  184. package/skills/workflow/writing-skills/SKILL.md +655 -0
  185. package/skills/workflow/writing-skills/anthropic-best-practices.md +1150 -0
  186. package/skills/workflow/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  187. package/skills/workflow/writing-skills/graphviz-conventions.dot +172 -0
  188. package/skills/workflow/writing-skills/persuasion-principles.md +187 -0
  189. package/skills/workflow/writing-skills/render-graphs.js +168 -0
  190. package/skills/workflow/writing-skills/testing-skills-with-subagents.md +384 -0
  191. package/dist/commands/assets.d.ts +0 -2
  192. package/dist/commands/assets.js +0 -189
  193. package/dist/commands/debug.d.ts +0 -2
  194. package/dist/commands/debug.js +0 -127
  195. package/dist/commands/doctor.test.js +0 -29
  196. package/dist/commands/project.d.ts +0 -2
  197. package/dist/commands/project.js +0 -170
  198. package/dist/commands/scene.d.ts +0 -2
  199. package/dist/commands/scene.js +0 -290
  200. package/dist/lib/agent-config.test.d.ts +0 -1
  201. package/dist/lib/agent-config.test.js +0 -80
  202. package/dist/lib/format.test.d.ts +0 -1
  203. package/dist/lib/format.test.js +0 -36
  204. package/dist/lib/plugin-manifests.test.d.ts +0 -1
  205. package/dist/lib/plugin-manifests.test.js +0 -134
  206. package/dist/lib/with-engine.d.ts +0 -14
  207. package/dist/lib/with-engine.js +0 -109
  208. package/skills/_shared/collaborative-protocol.md +0 -50
  209. package/skills/_shared/gd-style.md +0 -121
  210. package/skills/_shared/godot-version.md +0 -32
  211. package/skills/_shared/mcp-tools-reference.md +0 -133
  212. package/skills/_shared/summer-folder.md +0 -44
  213. package/skills/_shared/template-registry.md +0 -40
  214. package/skills/_tests/runner.md +0 -90
  215. package/skills/_tests/specs/3d-lighting.md +0 -19
  216. package/skills/_tests/specs/art-direction.md +0 -115
  217. package/skills/_tests/specs/asset-strategy.md +0 -19
  218. package/skills/_tests/specs/audio-direction.md +0 -123
  219. package/skills/_tests/specs/brainstorm-game.md +0 -99
  220. package/skills/_tests/specs/debug.md +0 -94
  221. package/skills/_tests/specs/design-level.md +0 -113
  222. package/skills/_tests/specs/design-mechanic.md +0 -112
  223. package/skills/_tests/specs/design-npc.md +0 -143
  224. package/skills/_tests/specs/export-and-ship.md +0 -114
  225. package/skills/_tests/specs/fps-controller.md +0 -113
  226. package/skills/_tests/specs/gdscript-patterns.md +0 -19
  227. package/skills/_tests/specs/host-authoritative-state.md +0 -104
  228. package/skills/_tests/specs/make-game.md +0 -19
  229. package/skills/_tests/specs/peer-to-peer-multiplayer.md +0 -99
  230. package/skills/_tests/specs/play.md +0 -88
  231. package/skills/_tests/specs/scene-composition.md +0 -19
  232. package/skills/_tests/specs/setup-multiplayer.md +0 -106
  233. package/skills/_tests/specs/tune-performance.md +0 -104
  234. package/skills/_tests/specs/ui-basics.md +0 -19
  235. package/skills/_tests/specs/vfx.md +0 -116
  236. package/skills/catalog.yaml +0 -991
  237. /package/dist/{commands/doctor.test.d.ts → lib/cloud/types.js} +0 -0
@@ -0,0 +1,90 @@
1
+ import { mkdir, readFile, writeFile } from "fs/promises";
2
+ import { existsSync } from "fs";
3
+ import { basename, dirname, isAbsolute, join, resolve } from "path";
4
+ export function resolveProjectRoot(project) {
5
+ return resolve(project ? (isAbsolute(project) ? project : join(process.cwd(), project)) : process.cwd());
6
+ }
7
+ export function bindingPath(projectRoot) {
8
+ return join(projectRoot, "summer-cloud.json");
9
+ }
10
+ export function localCloudDir(projectRoot) {
11
+ return join(projectRoot, ".summer", "local", "cloud");
12
+ }
13
+ export function basePath(projectRoot) {
14
+ return join(localCloudDir(projectRoot), "base.json");
15
+ }
16
+ export function journalPath(projectRoot) {
17
+ return join(localCloudDir(projectRoot), "journal.json");
18
+ }
19
+ export function hashCachePath(projectRoot) {
20
+ return join(localCloudDir(projectRoot), "hash-cache.json");
21
+ }
22
+ export function stagingDir(projectRoot) {
23
+ return join(localCloudDir(projectRoot), "staging");
24
+ }
25
+ export function conflictsDir(projectRoot) {
26
+ return join(localCloudDir(projectRoot), "conflicts");
27
+ }
28
+ export async function ensureCloudDirs(projectRoot) {
29
+ await mkdir(stagingDir(projectRoot), { recursive: true, mode: 0o700 });
30
+ await mkdir(conflictsDir(projectRoot), { recursive: true, mode: 0o700 });
31
+ }
32
+ export async function readBinding(projectRoot) {
33
+ try {
34
+ return JSON.parse(await readFile(bindingPath(projectRoot), "utf8"));
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ export async function writeBinding(projectRoot, binding) {
41
+ await writeJsonAtomic(bindingPath(projectRoot), binding);
42
+ }
43
+ export async function readBase(projectRoot) {
44
+ try {
45
+ return JSON.parse(await readFile(basePath(projectRoot), "utf8"));
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ export async function writeBase(projectRoot, base) {
52
+ await writeJsonAtomic(basePath(projectRoot), base);
53
+ }
54
+ export async function readJournal(projectRoot) {
55
+ try {
56
+ return JSON.parse(await readFile(journalPath(projectRoot), "utf8"));
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ export async function writeJournal(projectRoot, journal) {
63
+ await writeJsonAtomic(journalPath(projectRoot), journal);
64
+ }
65
+ export async function removeJournal(projectRoot) {
66
+ const { rm } = await import("fs/promises");
67
+ await rm(journalPath(projectRoot), { force: true });
68
+ }
69
+ export async function writeJsonAtomic(path, value) {
70
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
71
+ const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
72
+ await writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
73
+ const { rename } = await import("fs/promises");
74
+ await rename(tmp, path);
75
+ }
76
+ export async function projectDisplayName(projectRoot) {
77
+ try {
78
+ const projectGodot = await readFile(join(projectRoot, "project.godot"), "utf8");
79
+ const match = projectGodot.match(/^\s*config\/name\s*=\s*"([^"]+)"/m);
80
+ if (match?.[1])
81
+ return match[1];
82
+ }
83
+ catch {
84
+ // Fall back to folder name.
85
+ }
86
+ return basename(projectRoot) || "Summer Project";
87
+ }
88
+ export function isCloudEnabled(projectRoot) {
89
+ return existsSync(bindingPath(projectRoot));
90
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Section+key 3-way merge for project.godot (spec 10.2). ConfigFile semantics:
3
+ * keys added on either side are unioned; a key deleted on one side and
4
+ * unchanged on the other stays deleted (a naive union would resurrect deleted
5
+ * autoloads); keys changed on both sides take the CAS winner's (remote) value
6
+ * and the losing value is surfaced in the sync notice. config_version comes
7
+ * from the winner. Values can span multiple lines (Object(InputEventKey, ...))
8
+ * so parsing uses a balanced-delimiter tokenizer, not line splitting.
9
+ */
10
+ export interface ParsedConfig {
11
+ /** Section name ("" for the top section) to ordered key/value text map. */
12
+ sections: Map<string, Map<string, string>>;
13
+ /** Section order as encountered. */
14
+ order: string[];
15
+ }
16
+ export declare class ConfigParseError extends Error {
17
+ }
18
+ export declare function parseGodotConfig(text: string): ParsedConfig;
19
+ export declare function serializeGodotConfig(config: ParsedConfig): string;
20
+ export interface GodotMergeResult {
21
+ ok: boolean;
22
+ merged: string;
23
+ /** Both-changed keys where the remote value won; notices for the sync result. */
24
+ losingValues: Array<{
25
+ section: string;
26
+ key: string;
27
+ localValue: string;
28
+ }>;
29
+ /** Parse failure on any input: whole-file remote-wins fallback. */
30
+ fallback: boolean;
31
+ }
32
+ export declare function mergeProjectGodot(baseText: string, localText: string, remoteText: string): GodotMergeResult;
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Section+key 3-way merge for project.godot (spec 10.2). ConfigFile semantics:
3
+ * keys added on either side are unioned; a key deleted on one side and
4
+ * unchanged on the other stays deleted (a naive union would resurrect deleted
5
+ * autoloads); keys changed on both sides take the CAS winner's (remote) value
6
+ * and the losing value is surfaced in the sync notice. config_version comes
7
+ * from the winner. Values can span multiple lines (Object(InputEventKey, ...))
8
+ * so parsing uses a balanced-delimiter tokenizer, not line splitting.
9
+ */
10
+ export class ConfigParseError extends Error {
11
+ }
12
+ export function parseGodotConfig(text) {
13
+ const sections = new Map();
14
+ const order = [];
15
+ let current = "";
16
+ ensureSection(sections, order, current);
17
+ let i = 0;
18
+ const len = text.length;
19
+ while (i < len) {
20
+ // Skip whitespace and blank lines.
21
+ while (i < len && (text[i] === " " || text[i] === "\t" || text[i] === "\r" || text[i] === "\n"))
22
+ i += 1;
23
+ if (i >= len)
24
+ break;
25
+ const ch = text[i];
26
+ if (ch === ";" || ch === "#") {
27
+ while (i < len && text[i] !== "\n")
28
+ i += 1;
29
+ continue;
30
+ }
31
+ if (ch === "[") {
32
+ const close = text.indexOf("]", i);
33
+ if (close === -1)
34
+ throw new ConfigParseError("Unterminated section header");
35
+ current = text.slice(i + 1, close).trim();
36
+ ensureSection(sections, order, current);
37
+ i = close + 1;
38
+ continue;
39
+ }
40
+ // key=value. Key runs to the first unquoted '='.
41
+ const eq = text.indexOf("=", i);
42
+ const lineEnd = text.indexOf("\n", i);
43
+ if (eq === -1 || (lineEnd !== -1 && lineEnd < eq)) {
44
+ throw new ConfigParseError(`Expected key=value near offset ${i}`);
45
+ }
46
+ const key = text.slice(i, eq).trim();
47
+ if (!key)
48
+ throw new ConfigParseError(`Empty key near offset ${i}`);
49
+ i = eq + 1;
50
+ const { value, next } = readValue(text, i);
51
+ sections.get(current).set(key, value.trim());
52
+ i = next;
53
+ }
54
+ return { sections, order };
55
+ }
56
+ /**
57
+ * Reads a Variant value starting at `start`, consuming across newlines while
58
+ * any parenthesis, bracket, or brace is unbalanced or a string is open.
59
+ */
60
+ function readValue(text, start) {
61
+ let depth = 0;
62
+ let inString = false;
63
+ let i = start;
64
+ const len = text.length;
65
+ while (i < len) {
66
+ const ch = text[i];
67
+ if (inString) {
68
+ if (ch === "\\") {
69
+ i += 2;
70
+ continue;
71
+ }
72
+ if (ch === '"')
73
+ inString = false;
74
+ i += 1;
75
+ continue;
76
+ }
77
+ if (ch === '"') {
78
+ inString = true;
79
+ i += 1;
80
+ continue;
81
+ }
82
+ if (ch === "(" || ch === "[" || ch === "{")
83
+ depth += 1;
84
+ else if (ch === ")" || ch === "]" || ch === "}") {
85
+ depth -= 1;
86
+ if (depth < 0)
87
+ throw new ConfigParseError(`Unbalanced delimiter at offset ${i}`);
88
+ }
89
+ else if (ch === "\n" && depth === 0) {
90
+ return { value: text.slice(start, i), next: i + 1 };
91
+ }
92
+ i += 1;
93
+ }
94
+ if (inString || depth > 0)
95
+ throw new ConfigParseError("Unterminated value");
96
+ return { value: text.slice(start, i), next: i };
97
+ }
98
+ function ensureSection(sections, order, name) {
99
+ if (!sections.has(name)) {
100
+ sections.set(name, new Map());
101
+ order.push(name);
102
+ }
103
+ }
104
+ export function serializeGodotConfig(config) {
105
+ const parts = [];
106
+ for (const name of config.order) {
107
+ const entries = config.sections.get(name);
108
+ if (!entries || entries.size === 0) {
109
+ if (name === "")
110
+ continue;
111
+ }
112
+ if (name !== "") {
113
+ parts.push(`[${name}]`);
114
+ parts.push("");
115
+ }
116
+ for (const [key, value] of entries ?? []) {
117
+ parts.push(`${key}=${value}`);
118
+ }
119
+ parts.push("");
120
+ }
121
+ return `${parts.join("\n").replace(/\n+$/, "")}\n`;
122
+ }
123
+ export function mergeProjectGodot(baseText, localText, remoteText) {
124
+ let base;
125
+ let local;
126
+ let remote;
127
+ try {
128
+ base = parseGodotConfig(baseText);
129
+ local = parseGodotConfig(localText);
130
+ remote = parseGodotConfig(remoteText);
131
+ }
132
+ catch {
133
+ // Parse failure on any of the three inputs: fall back to whole-file
134
+ // remote-wins; the caller writes the local bytes as a conflict copy.
135
+ return { ok: true, merged: remoteText, losingValues: [], fallback: true };
136
+ }
137
+ const merged = { sections: new Map(), order: [] };
138
+ const losingValues = [];
139
+ const sectionNames = [];
140
+ for (const name of [...remote.order, ...local.order, ...base.order]) {
141
+ if (!sectionNames.includes(name))
142
+ sectionNames.push(name);
143
+ }
144
+ for (const section of sectionNames) {
145
+ const b = base.sections.get(section) ?? new Map();
146
+ const l = local.sections.get(section) ?? new Map();
147
+ const r = remote.sections.get(section) ?? new Map();
148
+ const keys = [];
149
+ for (const key of [...r.keys(), ...l.keys(), ...b.keys()]) {
150
+ if (!keys.includes(key))
151
+ keys.push(key);
152
+ }
153
+ const out = new Map();
154
+ for (const key of keys) {
155
+ const bv = b.get(key);
156
+ const lv = l.get(key);
157
+ const rv = r.get(key);
158
+ const resolved = mergeKey(bv, lv, rv);
159
+ if (resolved.losing !== undefined) {
160
+ losingValues.push({ section, key, localValue: resolved.losing });
161
+ }
162
+ if (resolved.value !== undefined)
163
+ out.set(key, resolved.value);
164
+ }
165
+ if (out.size > 0 || section === "") {
166
+ merged.sections.set(section, out);
167
+ merged.order.push(section);
168
+ }
169
+ }
170
+ return { ok: true, merged: serializeGodotConfig(merged), losingValues, fallback: false };
171
+ }
172
+ function mergeKey(base, local, remote) {
173
+ if (local === remote)
174
+ return { value: local };
175
+ const localChanged = local !== base;
176
+ const remoteChanged = remote !== base;
177
+ if (localChanged && !remoteChanged)
178
+ return { value: local };
179
+ if (remoteChanged && !localChanged)
180
+ return { value: remote };
181
+ // Changed on both sides (including delete vs change): the CAS winner's
182
+ // (remote) value wins at key granularity; deletion on the remote side wins
183
+ // too. The losing local value is surfaced.
184
+ return { value: remote, losing: local ?? "(deleted locally)" };
185
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Tracked-set rules (spec section 13). Full-project sync, single mode.
3
+ * Hard excludes cannot be re-included; .summercloudignore can only exclude
4
+ * further. The built-in rule set carries RULES_VERSION; manifests record the
5
+ * rulesVersion that produced them so stale clients can operate restricted.
6
+ */
7
+ export declare const RULES_VERSION = 1;
8
+ export declare function isHardExcludedDir(relPath: string, name: string): boolean;
9
+ /**
10
+ * Hard-excluded files. `siblings` is the set of basenames in the same
11
+ * directory, used to detect Godot safe-save temps (name-XXXXXX with the base
12
+ * file present).
13
+ */
14
+ export declare function isHardExcludedFile(relPath: string, name: string, siblings: ReadonlySet<string>): boolean;
15
+ /** Godot safe-save writes `name-XXXXXX` (six trailing random chars) next to `name`. */
16
+ export declare function isGodotSafeSaveTemp(name: string, siblings: ReadonlySet<string>): boolean;
17
+ /** True when a manifest key falls under the built-in hard excludes. */
18
+ export declare function isHardExcludedPath(path: string): boolean;
19
+ export interface IgnoreRule {
20
+ raw: string;
21
+ /** Trailing slash: matches directories (and everything under them). */
22
+ directory: boolean;
23
+ /** Leading slash: anchored to the project root. */
24
+ anchored: boolean;
25
+ regex: RegExp;
26
+ }
27
+ export interface TrackedRules {
28
+ rulesVersion: number;
29
+ ignore: IgnoreRule[];
30
+ }
31
+ export declare function loadTrackedRules(projectRoot: string): Promise<TrackedRules>;
32
+ export declare function parseIgnoreRules(raw: string): IgnoreRule[];
33
+ export declare function isIgnoredByRules(path: string, isDirectory: boolean, rules: TrackedRules): boolean;
34
+ /**
35
+ * Deletion semantics (spec 13): a path may be marked deleted only if it
36
+ * matches the CURRENT tracked rules AND is absent on disk. Paths that merely
37
+ * stopped matching rules are carried forward unchanged.
38
+ */
39
+ export declare function isTrackedByCurrentRules(path: string, rules: TrackedRules): boolean;
@@ -0,0 +1,159 @@
1
+ import { readFile } from "fs/promises";
2
+ import { join } from "path";
3
+ /**
4
+ * Tracked-set rules (spec section 13). Full-project sync, single mode.
5
+ * Hard excludes cannot be re-included; .summercloudignore can only exclude
6
+ * further. The built-in rule set carries RULES_VERSION; manifests record the
7
+ * rulesVersion that produced them so stale clients can operate restricted.
8
+ */
9
+ export const RULES_VERSION = 1;
10
+ /** Directory names hard-excluded at any depth. */
11
+ const EXCLUDED_DIR_NAMES = new Set([
12
+ ".git",
13
+ ".godot",
14
+ "node_modules",
15
+ ".next",
16
+ ".claude",
17
+ ".specstory",
18
+ ]);
19
+ /** Root-only excluded directories. */
20
+ const EXCLUDED_ROOT_DIRS = new Set(["android"]);
21
+ /** Exact file basenames hard-excluded everywhere. */
22
+ const EXCLUDED_FILE_NAMES = new Set([".DS_Store", "Thumbs.db", "desktop.ini", "project.godot.bak"]);
23
+ export function isHardExcludedDir(relPath, name) {
24
+ if (EXCLUDED_DIR_NAMES.has(name))
25
+ return true;
26
+ if (EXCLUDED_ROOT_DIRS.has(name) && relPath === name)
27
+ return true;
28
+ // .summer/local/ is machine state; the rest of .summer/** is tracked.
29
+ if (relPath === ".summer/local")
30
+ return true;
31
+ return false;
32
+ }
33
+ /**
34
+ * Hard-excluded files. `siblings` is the set of basenames in the same
35
+ * directory, used to detect Godot safe-save temps (name-XXXXXX with the base
36
+ * file present).
37
+ */
38
+ export function isHardExcludedFile(relPath, name, siblings) {
39
+ if (EXCLUDED_FILE_NAMES.has(name))
40
+ return true;
41
+ if (name.startsWith(".env"))
42
+ return true;
43
+ if (name.endsWith(".translation"))
44
+ return true;
45
+ if (name.endsWith(".tmp"))
46
+ return true;
47
+ if (isGodotSafeSaveTemp(name, siblings))
48
+ return true;
49
+ return false;
50
+ }
51
+ /** Godot safe-save writes `name-XXXXXX` (six trailing random chars) next to `name`. */
52
+ export function isGodotSafeSaveTemp(name, siblings) {
53
+ const match = /^(.+)-[A-Za-z0-9]{6}$/.exec(name);
54
+ if (!match)
55
+ return false;
56
+ return siblings.has(match[1]);
57
+ }
58
+ /** True when a manifest key falls under the built-in hard excludes. */
59
+ export function isHardExcludedPath(path) {
60
+ if (path === ".summer/local" || path.startsWith(".summer/local/"))
61
+ return true;
62
+ const segments = path.split("/");
63
+ let prefix = "";
64
+ for (let i = 0; i < segments.length - 1; i += 1) {
65
+ prefix = prefix ? `${prefix}/${segments[i]}` : segments[i];
66
+ if (isHardExcludedDir(prefix, segments[i]))
67
+ return true;
68
+ }
69
+ const name = segments[segments.length - 1];
70
+ if (EXCLUDED_DIR_NAMES.has(name))
71
+ return true;
72
+ return isHardExcludedFile(path, name, new Set());
73
+ }
74
+ export async function loadTrackedRules(projectRoot) {
75
+ let raw = "";
76
+ try {
77
+ raw = await readFile(join(projectRoot, ".summercloudignore"), "utf8");
78
+ }
79
+ catch {
80
+ return { rulesVersion: RULES_VERSION, ignore: [] };
81
+ }
82
+ return { rulesVersion: RULES_VERSION, ignore: parseIgnoreRules(raw) };
83
+ }
84
+ export function parseIgnoreRules(raw) {
85
+ return raw
86
+ .split(/\r?\n/)
87
+ .map((line) => line.trim())
88
+ .filter((line) => line && !line.startsWith("#") && !line.startsWith("!"))
89
+ .map((line) => {
90
+ const directory = line.endsWith("/");
91
+ let cleaned = directory ? line.slice(0, -1) : line;
92
+ const anchored = cleaned.startsWith("/");
93
+ if (anchored)
94
+ cleaned = cleaned.slice(1);
95
+ return { raw: cleaned, directory, anchored, regex: globToRegex(cleaned, anchored) };
96
+ });
97
+ }
98
+ function globToRegex(pattern, anchored) {
99
+ let out = "";
100
+ for (let i = 0; i < pattern.length; i += 1) {
101
+ const ch = pattern[i];
102
+ if (ch === "*") {
103
+ if (pattern[i + 1] === "*") {
104
+ out += ".*";
105
+ i += 1;
106
+ if (pattern[i + 1] === "/")
107
+ i += 1;
108
+ }
109
+ else {
110
+ out += "[^/]*";
111
+ }
112
+ }
113
+ else if (ch === "?") {
114
+ out += "[^/]";
115
+ }
116
+ else {
117
+ out += escapeRegex(ch);
118
+ }
119
+ }
120
+ // Gitignore semantics: an unanchored pattern without a slash matches at any
121
+ // depth; a pattern containing a slash is root-relative.
122
+ const prefix = anchored || pattern.includes("/") ? "^" : "(^|/)";
123
+ return new RegExp(`${prefix}${out}$`);
124
+ }
125
+ function escapeRegex(value) {
126
+ return value.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
127
+ }
128
+ export function isIgnoredByRules(path, isDirectory, rules) {
129
+ for (const rule of rules.ignore) {
130
+ if (rule.regex.test(path)) {
131
+ if (rule.directory && !isDirectory)
132
+ continue;
133
+ return true;
134
+ }
135
+ // A directory rule also excludes everything under a matching directory.
136
+ const segments = path.split("/");
137
+ let prefix = "";
138
+ for (let i = 0; i < segments.length - 1; i += 1) {
139
+ prefix = prefix ? `${prefix}/${segments[i]}` : segments[i];
140
+ if (rule.regex.test(prefix))
141
+ return true;
142
+ }
143
+ }
144
+ return false;
145
+ }
146
+ /**
147
+ * Deletion semantics (spec 13): a path may be marked deleted only if it
148
+ * matches the CURRENT tracked rules AND is absent on disk. Paths that merely
149
+ * stopped matching rules are carried forward unchanged.
150
+ */
151
+ export function isTrackedByCurrentRules(path, rules) {
152
+ if (path === "summer-cloud.json")
153
+ return false;
154
+ if (isHardExcludedPath(path))
155
+ return false;
156
+ if (isIgnoredByRules(path, false, rules))
157
+ return false;
158
+ return true;
159
+ }
@@ -0,0 +1,21 @@
1
+ import type { SyncOptions, SyncResult } from "./types.js";
2
+ /** Jittered 409 backoff schedule. Exported so tests can shrink the delays. */
3
+ export declare const CAS_BACKOFF_MS: number[];
4
+ type Face = "cli" | "mcp" | "engine";
5
+ export interface SyncRunOptions extends SyncOptions {
6
+ face?: Face;
7
+ }
8
+ export declare function cloudInit(options?: SyncRunOptions): Promise<SyncResult>;
9
+ export declare function cloudStatus(options?: SyncRunOptions): Promise<SyncResult>;
10
+ export declare function cloudPush(options?: SyncRunOptions): Promise<SyncResult>;
11
+ export declare function cloudPull(options?: SyncRunOptions): Promise<SyncResult>;
12
+ export declare function cloudConflicts(options?: SyncRunOptions & {
13
+ restorePath?: string;
14
+ set?: string;
15
+ }): Promise<SyncResult>;
16
+ export declare function cloudRestore(options?: SyncRunOptions & {
17
+ version?: number;
18
+ checkpoint?: string;
19
+ }): Promise<SyncResult>;
20
+ export declare function cloudCheckpoints(options?: SyncRunOptions): Promise<SyncResult>;
21
+ export {};