summer-engine 2.5.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +5 -4
- package/.codex-plugin/plugin.json +5 -4
- package/.cursor-plugin/plugin.json +4 -3
- package/.factory-plugin/plugin.json +3 -3
- package/AGENTS.md +6 -4
- package/CHANGELOG.md +20 -2
- package/GEMINI.md +3 -3
- package/README.md +83 -97
- package/dist/bin/summer.js +6 -1
- package/dist/commands/cloud.d.ts +2 -0
- package/dist/commands/cloud.js +80 -0
- package/dist/commands/doctor.js +19 -0
- package/dist/commands/login.js +4 -1
- package/dist/commands/logout.js +1 -1
- package/dist/commands/open.js +1 -1
- package/dist/commands/plan.d.ts +2 -0
- package/dist/commands/plan.js +60 -0
- package/dist/commands/run.js +132 -35
- package/dist/lib/auth.d.ts +2 -0
- package/dist/lib/auth.js +13 -0
- package/dist/lib/cloud/api.d.ts +79 -0
- package/dist/lib/cloud/api.js +107 -0
- package/dist/lib/cloud/apply.d.ts +32 -0
- package/dist/lib/cloud/apply.js +294 -0
- package/dist/lib/cloud/checkpoint.d.ts +26 -0
- package/dist/lib/cloud/checkpoint.js +142 -0
- package/dist/lib/cloud/conflicts.d.ts +55 -0
- package/dist/lib/cloud/conflicts.js +131 -0
- package/dist/lib/cloud/containment.d.ts +8 -0
- package/dist/lib/cloud/containment.js +52 -0
- package/dist/lib/cloud/diff.d.ts +39 -0
- package/dist/lib/cloud/diff.js +138 -0
- package/dist/lib/cloud/engine-bridge.d.ts +21 -0
- package/dist/lib/cloud/engine-bridge.js +105 -0
- package/dist/lib/cloud/hash-cache.d.ts +23 -0
- package/dist/lib/cloud/hash-cache.js +44 -0
- package/dist/lib/cloud/hash.d.ts +10 -0
- package/dist/lib/cloud/hash.js +111 -0
- package/dist/lib/cloud/http.d.ts +17 -0
- package/dist/lib/cloud/http.js +47 -0
- package/dist/lib/cloud/lock-path.d.ts +1 -0
- package/dist/lib/cloud/lock-path.js +5 -0
- package/dist/lib/cloud/lock.d.ts +14 -0
- package/dist/lib/cloud/lock.js +143 -0
- package/dist/lib/cloud/manifest.d.ts +6 -0
- package/dist/lib/cloud/manifest.js +41 -0
- package/dist/lib/cloud/paths.d.ts +20 -0
- package/dist/lib/cloud/paths.js +90 -0
- package/dist/lib/cloud/project-godot-merge.d.ts +32 -0
- package/dist/lib/cloud/project-godot-merge.js +185 -0
- package/dist/lib/cloud/rules.d.ts +39 -0
- package/dist/lib/cloud/rules.js +159 -0
- package/dist/lib/cloud/sync.d.ts +21 -0
- package/dist/lib/cloud/sync.js +518 -0
- package/dist/lib/cloud/transfer.d.ts +23 -0
- package/dist/lib/cloud/transfer.js +184 -0
- package/dist/lib/cloud/types.d.ts +62 -0
- package/dist/lib/cloud/types.js +1 -0
- package/dist/lib/cloud/validate-path.d.ts +23 -0
- package/dist/lib/cloud/validate-path.js +103 -0
- package/dist/lib/engine.d.ts +6 -0
- package/dist/lib/engine.js +36 -1
- package/dist/lib/game-task-plan.d.ts +42 -0
- package/dist/lib/game-task-plan.js +376 -0
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/tools/asset-tools.js +265 -45
- package/dist/mcp/tools/cloud-tools.d.ts +2 -0
- package/dist/mcp/tools/cloud-tools.js +42 -0
- package/dist/mcp/tools/generate-tools.js +101 -13
- package/dist/mcp/tools/project-tools.js +59 -16
- package/gemini-extension.json +3 -3
- package/package.json +16 -5
- package/skills/3d-assets/character-model/SKILL.md +27 -5
- package/skills/3d-assets/environment-kit/SKILL.md +7 -2
- package/skills/3d-assets/organic-model/SKILL.md +4 -3
- package/skills/3d-assets/prop-model/SKILL.md +16 -8
- package/skills/3d-assets/vehicle-model/SKILL.md +4 -3
- package/skills/animation/animation-tree/SKILL.md +2 -0
- package/skills/animation/generate-motion/SKILL.md +25 -4
- package/skills/asset-pipeline/asset-strategy/SKILL.md +3 -2
- package/skills/workflow/mcpupdate/SKILL.md +134 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ASSET_POLICIES, GAME_TASK_MODES, GAME_TASK_TARGETS, VERIFICATION_LEVELS, buildGameTaskPlan, } from "../lib/game-task-plan.js";
|
|
3
|
+
import { c } from "../lib/format.js";
|
|
4
|
+
function parseChoice(value, choices, label) {
|
|
5
|
+
if (!value)
|
|
6
|
+
return undefined;
|
|
7
|
+
if (choices.includes(value))
|
|
8
|
+
return value;
|
|
9
|
+
throw new Error(`Invalid ${label}: ${value}. Use one of: ${choices.join(", ")}`);
|
|
10
|
+
}
|
|
11
|
+
function printList(title, items) {
|
|
12
|
+
if (items.length === 0)
|
|
13
|
+
return;
|
|
14
|
+
console.log(c.bold(title));
|
|
15
|
+
for (const item of items) {
|
|
16
|
+
console.log(` - ${item}`);
|
|
17
|
+
}
|
|
18
|
+
console.log("");
|
|
19
|
+
}
|
|
20
|
+
export const planCommand = new Command("plan")
|
|
21
|
+
.description("Plan the right Summer CLI/MCP workflow for an AI game-building task")
|
|
22
|
+
.argument("<goal...>", "What the user wants to build, fix, polish, or create")
|
|
23
|
+
.option("--mode <mode>", `Task mode: ${GAME_TASK_MODES.join("|")}`)
|
|
24
|
+
.option("--target <target>", `Task target: ${GAME_TASK_TARGETS.join("|")}`)
|
|
25
|
+
.option("--asset-policy <policy>", `Asset policy: ${ASSET_POLICIES.join("|")}`)
|
|
26
|
+
.option("--verification <level>", `Verification level: ${VERIFICATION_LEVELS.join("|")}`)
|
|
27
|
+
.option("--json", "Print the plan as JSON")
|
|
28
|
+
.action((goalParts, opts) => {
|
|
29
|
+
const plan = buildGameTaskPlan({
|
|
30
|
+
goal: goalParts.join(" "),
|
|
31
|
+
mode: parseChoice(opts.mode, GAME_TASK_MODES, "mode"),
|
|
32
|
+
target: parseChoice(opts.target, GAME_TASK_TARGETS, "target"),
|
|
33
|
+
assetPolicy: parseChoice(opts.assetPolicy, ASSET_POLICIES, "asset policy"),
|
|
34
|
+
verification: parseChoice(opts.verification, VERIFICATION_LEVELS, "verification"),
|
|
35
|
+
});
|
|
36
|
+
if (opts.json) {
|
|
37
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
console.log("");
|
|
41
|
+
console.log(c.bold("Summer Game Task Plan"));
|
|
42
|
+
console.log(`Goal: ${plan.goal}`);
|
|
43
|
+
console.log(`Mode: ${plan.mode}`);
|
|
44
|
+
console.log(`Target: ${plan.target}`);
|
|
45
|
+
console.log(`Asset policy: ${plan.assetPolicy}`);
|
|
46
|
+
console.log(`Verification: ${plan.verification}`);
|
|
47
|
+
console.log("");
|
|
48
|
+
console.log(c.bold("Recommended Skills"));
|
|
49
|
+
for (const skill of plan.recommendedSkills) {
|
|
50
|
+
console.log(` - ${skill.id}: ${skill.why}`);
|
|
51
|
+
}
|
|
52
|
+
console.log("");
|
|
53
|
+
printList("MCP Start", plan.mcpToolPlan.start);
|
|
54
|
+
printList("MCP Mutations", plan.mcpToolPlan.mutate);
|
|
55
|
+
printList("MCP Assets", plan.mcpToolPlan.assets);
|
|
56
|
+
printList("MCP Verify", plan.mcpToolPlan.verify);
|
|
57
|
+
printList("Host File Work", plan.hostFileWork);
|
|
58
|
+
printList("User Gates", plan.userGates);
|
|
59
|
+
printList("Next Agent Steps", plan.nextAgentSteps);
|
|
60
|
+
});
|
package/dist/commands/run.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
import { existsSync } from "fs";
|
|
4
|
+
import { mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
4
5
|
import { platform } from "os";
|
|
6
|
+
import { join, resolve } from "path";
|
|
7
|
+
import { getSummerDir } from "../lib/auth.js";
|
|
5
8
|
import { getApiPort, checkEngineHealth } from "../lib/engine.js";
|
|
6
9
|
const MAC_PATHS = [
|
|
7
10
|
"/Applications/Summer.app/Contents/MacOS/Summer",
|
|
@@ -12,6 +15,8 @@ const WIN_PATHS = [
|
|
|
12
15
|
`${process.env.LOCALAPPDATA}\\Programs\\Summer Engine\\Summer.exe`,
|
|
13
16
|
`${process.env.PROGRAMFILES}\\Summer Engine\\Summer.exe`,
|
|
14
17
|
];
|
|
18
|
+
const LAUNCH_LOCK_STALE_MS = 60_000;
|
|
19
|
+
const LAUNCH_LOCK_WAIT_MS = 15_000;
|
|
15
20
|
function findEngineBinary() {
|
|
16
21
|
const paths = platform() === "darwin" ? MAC_PATHS : WIN_PATHS;
|
|
17
22
|
for (const p of paths) {
|
|
@@ -20,50 +25,142 @@ function findEngineBinary() {
|
|
|
20
25
|
}
|
|
21
26
|
return null;
|
|
22
27
|
}
|
|
28
|
+
function isProcessAlive(pid) {
|
|
29
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
30
|
+
return false;
|
|
31
|
+
try {
|
|
32
|
+
process.kill(pid, 0);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
const code = error.code;
|
|
37
|
+
return code === "EPERM";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function removeStaleLaunchLock(lockDir) {
|
|
41
|
+
const ownerPath = join(lockDir, "owner.json");
|
|
42
|
+
try {
|
|
43
|
+
const raw = await readFile(ownerPath, "utf-8");
|
|
44
|
+
const owner = JSON.parse(raw);
|
|
45
|
+
const pid = typeof owner.pid === "number" ? owner.pid : 0;
|
|
46
|
+
const createdAt = typeof owner.createdAt === "number" ? owner.createdAt : 0;
|
|
47
|
+
const staleByTime = Date.now() - createdAt > LAUNCH_LOCK_STALE_MS;
|
|
48
|
+
const staleByPid = !isProcessAlive(pid);
|
|
49
|
+
if (!staleByTime && !staleByPid) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Broken owner metadata should not permanently block launches.
|
|
55
|
+
}
|
|
56
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
async function withLaunchLock(fn) {
|
|
60
|
+
const summerDir = getSummerDir();
|
|
61
|
+
await mkdir(summerDir, { recursive: true, mode: 0o700 });
|
|
62
|
+
const lockDir = join(summerDir, "launch.lock");
|
|
63
|
+
const ownerPath = join(lockDir, "owner.json");
|
|
64
|
+
const startedAt = Date.now();
|
|
65
|
+
while (true) {
|
|
66
|
+
try {
|
|
67
|
+
await mkdir(lockDir, { mode: 0o700 });
|
|
68
|
+
try {
|
|
69
|
+
await writeFile(ownerPath, JSON.stringify({ pid: process.pid, createdAt: Date.now() }, null, 2), { encoding: "utf-8", mode: 0o600 });
|
|
70
|
+
return await fn();
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const code = error.code;
|
|
78
|
+
if (code !== "EEXIST") {
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
if (await removeStaleLaunchLock(lockDir)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (Date.now() - startedAt > LAUNCH_LOCK_WAIT_MS) {
|
|
85
|
+
throw new Error("Another `summer run` launch is still in progress. Try again in a few seconds.");
|
|
86
|
+
}
|
|
87
|
+
await sleep(100);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
23
91
|
export const runCommand = new Command("run")
|
|
24
92
|
.description("Launch Summer Engine, optionally opening a project")
|
|
25
93
|
.argument("[path]", "Path to a project directory (must contain project.godot)")
|
|
26
94
|
.action(async (projectPath) => {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
95
|
+
const fullProjectPath = projectPath ? resolve(projectPath) : null;
|
|
96
|
+
if (fullProjectPath) {
|
|
97
|
+
if (!existsSync(fullProjectPath)) {
|
|
98
|
+
console.error(`Directory not found: ${fullProjectPath}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
if (!existsSync(join(fullProjectPath, "project.godot"))) {
|
|
102
|
+
console.error(`No project.godot found in ${fullProjectPath}\n` +
|
|
103
|
+
"This doesn't look like a Godot/Summer Engine project.");
|
|
104
|
+
process.exit(1);
|
|
33
105
|
}
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
const binary = findEngineBinary();
|
|
37
|
-
if (!binary) {
|
|
38
|
-
console.error("Summer Engine not found. Install it first:\n" +
|
|
39
|
-
" summer install\n" +
|
|
40
|
-
" or download from https://summerengine.com/download");
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
const args = ["--editor"];
|
|
44
|
-
if (projectPath) {
|
|
45
|
-
args.unshift("--path", projectPath);
|
|
46
106
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
107
|
+
await withLaunchLock(async () => {
|
|
108
|
+
const port = await getApiPort();
|
|
109
|
+
const health = await checkEngineHealth(port);
|
|
110
|
+
if (health) {
|
|
111
|
+
const runningProjectPath = health.project_path
|
|
112
|
+
? resolve(health.project_path)
|
|
113
|
+
: null;
|
|
114
|
+
if (!fullProjectPath || runningProjectPath === fullProjectPath) {
|
|
115
|
+
console.log(`Summer Engine is already running (v${health.version}) on port ${port}`);
|
|
116
|
+
if (health.project_name) {
|
|
117
|
+
console.log(` Project: ${health.project_name}`);
|
|
118
|
+
}
|
|
119
|
+
if (health.project_path) {
|
|
120
|
+
console.log(` Path: ${health.project_path}`);
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
61
123
|
}
|
|
124
|
+
console.log(`Summer Engine is already running (v${health.version}) on port ${port}`);
|
|
125
|
+
console.log(` Current project: ${health.project_path ?? health.project_name ?? "unknown"}`);
|
|
126
|
+
console.error(`Refusing to launch a second editor for: ${fullProjectPath}\n` +
|
|
127
|
+
"Close or switch the current Summer Engine project first, then run this command again.");
|
|
128
|
+
process.exitCode = 1;
|
|
62
129
|
return;
|
|
63
130
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
131
|
+
const binary = findEngineBinary();
|
|
132
|
+
if (!binary) {
|
|
133
|
+
console.error("Summer Engine not found. Install it first:\n" +
|
|
134
|
+
" summer install\n" +
|
|
135
|
+
" or download from https://summerengine.com/download");
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const args = ["--editor"];
|
|
140
|
+
if (fullProjectPath) {
|
|
141
|
+
args.unshift("--path", fullProjectPath);
|
|
142
|
+
}
|
|
143
|
+
console.log("Launching Summer Engine...");
|
|
144
|
+
const child = spawn(binary, args, { detached: true, stdio: "ignore" });
|
|
145
|
+
child.unref();
|
|
146
|
+
// Wait for engine to start responding
|
|
147
|
+
const startTime = Date.now();
|
|
148
|
+
const timeout = 20000;
|
|
149
|
+
while (Date.now() - startTime < timeout) {
|
|
150
|
+
await sleep(500);
|
|
151
|
+
const newPort = await getApiPort();
|
|
152
|
+
const h = await checkEngineHealth(newPort);
|
|
153
|
+
if (h) {
|
|
154
|
+
console.log(`Summer Engine running (v${h.version}) on port ${newPort}`);
|
|
155
|
+
if (h.project_name) {
|
|
156
|
+
console.log(` Project: ${h.project_name}`);
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
console.log("Summer Engine launched but API not responding yet.\n" +
|
|
162
|
+
"It may still be loading. Run 'summer status' to check.");
|
|
163
|
+
});
|
|
67
164
|
});
|
|
68
165
|
function sleep(ms) {
|
|
69
166
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export declare function getSummerDir(): string;
|
|
2
2
|
export declare function getAuthToken(): Promise<string | null>;
|
|
3
3
|
export declare function saveAuthToken(token: string): Promise<void>;
|
|
4
|
+
export declare function getCloudToken(): Promise<string | null>;
|
|
5
|
+
export declare function saveCloudToken(token: string): Promise<void>;
|
|
4
6
|
export declare function getUserInfo(): Promise<{
|
|
5
7
|
id: string;
|
|
6
8
|
email: string;
|
package/dist/lib/auth.js
CHANGED
|
@@ -24,6 +24,19 @@ export async function saveAuthToken(token) {
|
|
|
24
24
|
await ensureSummerDir();
|
|
25
25
|
await writeFile(join(SUMMER_DIR, "auth-token"), token, { encoding: "utf-8", mode: 0o600 });
|
|
26
26
|
}
|
|
27
|
+
export async function getCloudToken() {
|
|
28
|
+
try {
|
|
29
|
+
const token = await readFile(join(SUMMER_DIR, "cloud-token"), "utf-8");
|
|
30
|
+
return token.trim() || null;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function saveCloudToken(token) {
|
|
37
|
+
await ensureSummerDir();
|
|
38
|
+
await writeFile(join(SUMMER_DIR, "cloud-token"), token, { encoding: "utf-8", mode: 0o600 });
|
|
39
|
+
}
|
|
27
40
|
export async function getUserInfo() {
|
|
28
41
|
try {
|
|
29
42
|
const data = await readFile(join(SUMMER_DIR, "user.json"), "utf-8");
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { CloudManifest } from "./types.js";
|
|
2
|
+
export declare class CloudApiError extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
readonly code: string;
|
|
5
|
+
readonly details: Record<string, unknown>;
|
|
6
|
+
constructor(status: number, code: string, message: string, details?: Record<string, unknown>);
|
|
7
|
+
}
|
|
8
|
+
export interface PresignedBlob {
|
|
9
|
+
sha256: string;
|
|
10
|
+
status: "exists" | "upload" | "quota_exceeded" | "too_large";
|
|
11
|
+
uploadId?: string;
|
|
12
|
+
url?: string;
|
|
13
|
+
headers?: Record<string, string>;
|
|
14
|
+
message?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface RemoteManifest {
|
|
17
|
+
version: number;
|
|
18
|
+
rulesVersion: number;
|
|
19
|
+
manifest: CloudManifest;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Every server interaction goes through this interface so tests can run the
|
|
23
|
+
* full sync pipeline against an in-memory double (no network, no server).
|
|
24
|
+
*/
|
|
25
|
+
export interface CloudApi {
|
|
26
|
+
createCloudProject(body: {
|
|
27
|
+
projectId: string;
|
|
28
|
+
} | {
|
|
29
|
+
name: string;
|
|
30
|
+
}): Promise<{
|
|
31
|
+
projectId: string;
|
|
32
|
+
headVersion: number;
|
|
33
|
+
}>;
|
|
34
|
+
getCloudProject(projectId: string): Promise<{
|
|
35
|
+
projectId: string;
|
|
36
|
+
headVersion: number;
|
|
37
|
+
rulesVersion: number;
|
|
38
|
+
}>;
|
|
39
|
+
checkBlobs(projectId: string, hashes: string[]): Promise<{
|
|
40
|
+
missing: string[];
|
|
41
|
+
}>;
|
|
42
|
+
presignBlobs(projectId: string, blobs: Array<{
|
|
43
|
+
sha256: string;
|
|
44
|
+
size: number;
|
|
45
|
+
}>): Promise<{
|
|
46
|
+
blobs: PresignedBlob[];
|
|
47
|
+
}>;
|
|
48
|
+
completeBlobs(projectId: string, uploads: Array<{
|
|
49
|
+
sha256: string;
|
|
50
|
+
uploadId: string;
|
|
51
|
+
}>): Promise<{
|
|
52
|
+
uploads: Array<{
|
|
53
|
+
sha256: string;
|
|
54
|
+
status: string;
|
|
55
|
+
}>;
|
|
56
|
+
}>;
|
|
57
|
+
presignGet(projectId: string, hashes: string[]): Promise<{
|
|
58
|
+
urls: Record<string, string>;
|
|
59
|
+
missing: string[];
|
|
60
|
+
}>;
|
|
61
|
+
getManifest(projectId: string, version?: number): Promise<RemoteManifest>;
|
|
62
|
+
commitManifest(projectId: string, body: {
|
|
63
|
+
baseVersion: number;
|
|
64
|
+
manifestSha256: string;
|
|
65
|
+
syncId: string;
|
|
66
|
+
rulesVersion: number;
|
|
67
|
+
}): Promise<{
|
|
68
|
+
version: number;
|
|
69
|
+
}>;
|
|
70
|
+
restoreVersion(projectId: string, body: {
|
|
71
|
+
toVersion: number;
|
|
72
|
+
baseVersion: number;
|
|
73
|
+
syncId: string;
|
|
74
|
+
}): Promise<{
|
|
75
|
+
version: number;
|
|
76
|
+
}>;
|
|
77
|
+
}
|
|
78
|
+
export declare function getCloudApi(): CloudApi;
|
|
79
|
+
export declare function setCloudApiForTests(api: CloudApi | null): void;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { getCloudToken } from "../auth.js";
|
|
2
|
+
import { parseManifest } from "./manifest.js";
|
|
3
|
+
const GATEWAY_URL = process.env.SUMMER_GATEWAY_URL || "https://www.summerengine.com";
|
|
4
|
+
export class CloudApiError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
code;
|
|
7
|
+
details;
|
|
8
|
+
constructor(status, code, message, details = {}) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.details = details;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async function cloudFetch(path, init = {}) {
|
|
16
|
+
const token = await getCloudToken();
|
|
17
|
+
if (!token) {
|
|
18
|
+
throw new CloudApiError(401, "unauthorized", "Run: summer login");
|
|
19
|
+
}
|
|
20
|
+
const res = await fetch(`${GATEWAY_URL}${path}`, {
|
|
21
|
+
...init,
|
|
22
|
+
headers: {
|
|
23
|
+
...(init.body ? { "content-type": "application/json" } : {}),
|
|
24
|
+
...(init.headers ?? {}),
|
|
25
|
+
authorization: `Bearer ${token}`,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
let body = null;
|
|
30
|
+
try {
|
|
31
|
+
body = await res.json();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// ignore
|
|
35
|
+
}
|
|
36
|
+
const error = body?.error ?? {};
|
|
37
|
+
throw new CloudApiError(res.status, error.code ?? "request_failed", error.message ?? `Cloud request failed (${res.status})`, error);
|
|
38
|
+
}
|
|
39
|
+
return (await res.json());
|
|
40
|
+
}
|
|
41
|
+
const realCloudApi = {
|
|
42
|
+
async createCloudProject(body) {
|
|
43
|
+
return cloudFetch("/api/cloud/projects", { method: "POST", body: JSON.stringify(body) });
|
|
44
|
+
},
|
|
45
|
+
async getCloudProject(projectId) {
|
|
46
|
+
return cloudFetch(`/api/cloud/projects/${projectId}`);
|
|
47
|
+
},
|
|
48
|
+
async checkBlobs(projectId, hashes) {
|
|
49
|
+
return cloudFetch("/api/cloud/blobs/check", {
|
|
50
|
+
method: "POST",
|
|
51
|
+
body: JSON.stringify({ projectId, hashes }),
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
async presignBlobs(projectId, blobs) {
|
|
55
|
+
return cloudFetch("/api/cloud/blobs/presign", {
|
|
56
|
+
method: "POST",
|
|
57
|
+
body: JSON.stringify({ projectId, blobs }),
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
async completeBlobs(projectId, uploads) {
|
|
61
|
+
return cloudFetch("/api/cloud/blobs/complete", {
|
|
62
|
+
method: "POST",
|
|
63
|
+
body: JSON.stringify({ projectId, uploads }),
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
async presignGet(projectId, hashes) {
|
|
67
|
+
return cloudFetch("/api/cloud/blobs/presign-get", {
|
|
68
|
+
method: "POST",
|
|
69
|
+
body: JSON.stringify({ projectId, hashes }),
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
async getManifest(projectId, version) {
|
|
73
|
+
const suffix = version !== undefined ? `?version=${version}` : "";
|
|
74
|
+
const meta = await cloudFetch(`/api/cloud/projects/${projectId}/manifest${suffix}`);
|
|
75
|
+
if (meta.version === 0) {
|
|
76
|
+
return {
|
|
77
|
+
version: 0,
|
|
78
|
+
rulesVersion: meta.rulesVersion ?? 1,
|
|
79
|
+
manifest: { schemaVersion: 1, projectId, rulesVersion: meta.rulesVersion ?? 1, files: {} },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const res = await fetch(meta.url);
|
|
83
|
+
if (!res.ok)
|
|
84
|
+
throw new CloudApiError(res.status, "manifest_download_failed", "Could not download manifest");
|
|
85
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
86
|
+
return { version: meta.version, rulesVersion: meta.rulesVersion, manifest: parseManifest(bytes, projectId) };
|
|
87
|
+
},
|
|
88
|
+
async commitManifest(projectId, body) {
|
|
89
|
+
return cloudFetch(`/api/cloud/projects/${projectId}/commit`, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
body: JSON.stringify(body),
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
async restoreVersion(projectId, body) {
|
|
95
|
+
return cloudFetch(`/api/cloud/projects/${projectId}/restore-version`, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
body: JSON.stringify(body),
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
let activeApi = realCloudApi;
|
|
102
|
+
export function getCloudApi() {
|
|
103
|
+
return activeApi;
|
|
104
|
+
}
|
|
105
|
+
export function setCloudApiForTests(api) {
|
|
106
|
+
activeApi = api ?? realCloudApi;
|
|
107
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ConflictSetWriter } from "./conflicts.js";
|
|
2
|
+
import type { DiffPlan } from "./diff.js";
|
|
3
|
+
import type { CloudManifest, LocalFileStat } from "./types.js";
|
|
4
|
+
export interface ApplyContext {
|
|
5
|
+
/** Staged blob path per sha256 (downloaded and hash-verified). */
|
|
6
|
+
staged: Map<string, string>;
|
|
7
|
+
/** Actual on-disk byte-name path per NFC manifest key (from the walk). */
|
|
8
|
+
diskPathByKey: Map<string, string>;
|
|
9
|
+
/** Pre-image stats captured at hash time for the apply-time stat check. */
|
|
10
|
+
statByKey: Map<string, LocalFileStat>;
|
|
11
|
+
/** Conflict set writer; losing bytes are preserved through it. */
|
|
12
|
+
conflicts: ConflictSetWriter;
|
|
13
|
+
/** Base manifest (for the project.godot 3-way merge). */
|
|
14
|
+
base: CloudManifest | null;
|
|
15
|
+
/** Called once before the first destructive apply (modify or delete). */
|
|
16
|
+
onBeforeDestructive: () => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export interface ApplyResult {
|
|
19
|
+
/** Paths written or deleted on disk (for the engine rescan). */
|
|
20
|
+
appliedPaths: string[];
|
|
21
|
+
notices: string[];
|
|
22
|
+
/** Local hashes preserved as conflict losers (pushed to R2 by the caller). */
|
|
23
|
+
preservedLocalHashes: Map<string, string>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Two-phase pull, phase 2 (spec 8.7). Fixed order: renames first (case-only
|
|
27
|
+
* renames as two-step), then writes (project.godot and *.tscn last), then
|
|
28
|
+
* deletes. Every write is an atomic rename from staging; every modify or
|
|
29
|
+
* delete is preceded by exactly one full-tree checkpoint and a pre-image
|
|
30
|
+
* stat check that routes drifted files to conflicts instead of overwriting.
|
|
31
|
+
*/
|
|
32
|
+
export declare function applyRemotePlan(projectRoot: string, plan: DiffPlan, remote: CloudManifest, context: ApplyContext): Promise<ApplyResult>;
|