@wangjiehu/sandbox 0.1.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/dist/index.d.ts +33 -0
- package/dist/index.js +98 -0
- package/package.json +15 -0
- package/src/Checkpoint.test.ts +53 -0
- package/src/CheckpointManager.ts +77 -0
- package/src/RollbackManager.ts +36 -0
- package/src/index.ts +3 -0
- package/src/types.ts +13 -0
- package/tsconfig.json +8 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
interface FileBackup {
|
|
2
|
+
path: string;
|
|
3
|
+
originalContent: string | null;
|
|
4
|
+
originalHash?: string;
|
|
5
|
+
}
|
|
6
|
+
interface Checkpoint {
|
|
7
|
+
id: string;
|
|
8
|
+
sessionId: string;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
toolCallId: string;
|
|
11
|
+
backups: FileBackup[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare class CheckpointManager {
|
|
15
|
+
private cwd;
|
|
16
|
+
private sessionId;
|
|
17
|
+
private checkpoints;
|
|
18
|
+
constructor(cwd: string, sessionId: string);
|
|
19
|
+
captureBeforeState(toolCallId: string, filePath: string): Promise<Checkpoint>;
|
|
20
|
+
getCheckpoints(): Checkpoint[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
declare class RollbackManager {
|
|
24
|
+
private cwd;
|
|
25
|
+
constructor(cwd: string);
|
|
26
|
+
rollback(checkpoint: Checkpoint): {
|
|
27
|
+
success: boolean;
|
|
28
|
+
error?: string;
|
|
29
|
+
restored: string[];
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { type Checkpoint, CheckpointManager, type FileBackup, RollbackManager };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// src/CheckpointManager.ts
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { generateId, resolveSafePath } from "@wangjiehu/shared";
|
|
5
|
+
var CheckpointManager = class {
|
|
6
|
+
constructor(cwd, sessionId) {
|
|
7
|
+
this.cwd = cwd;
|
|
8
|
+
this.sessionId = sessionId;
|
|
9
|
+
}
|
|
10
|
+
cwd;
|
|
11
|
+
sessionId;
|
|
12
|
+
checkpoints = [];
|
|
13
|
+
async captureBeforeState(toolCallId, filePath) {
|
|
14
|
+
let originalContent = null;
|
|
15
|
+
try {
|
|
16
|
+
const safePath = resolveSafePath(this.cwd, filePath);
|
|
17
|
+
if (existsSync(safePath)) {
|
|
18
|
+
originalContent = readFileSync(safePath, "utf8");
|
|
19
|
+
}
|
|
20
|
+
} catch (e) {
|
|
21
|
+
}
|
|
22
|
+
const backup = {
|
|
23
|
+
path: filePath,
|
|
24
|
+
originalContent
|
|
25
|
+
};
|
|
26
|
+
const checkpoint = {
|
|
27
|
+
id: generateId("cp"),
|
|
28
|
+
sessionId: this.sessionId,
|
|
29
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
30
|
+
toolCallId,
|
|
31
|
+
backups: [backup]
|
|
32
|
+
};
|
|
33
|
+
this.checkpoints.push(checkpoint);
|
|
34
|
+
const checkpointDir = join(
|
|
35
|
+
this.cwd,
|
|
36
|
+
".orbit",
|
|
37
|
+
"checkpoints",
|
|
38
|
+
this.sessionId,
|
|
39
|
+
checkpoint.id
|
|
40
|
+
);
|
|
41
|
+
mkdirSync(checkpointDir, { recursive: true });
|
|
42
|
+
if (originalContent !== null) {
|
|
43
|
+
writeFileSync(
|
|
44
|
+
join(checkpointDir, "backup_content.txt"),
|
|
45
|
+
originalContent,
|
|
46
|
+
"utf8"
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
writeFileSync(
|
|
50
|
+
join(checkpointDir, "meta.json"),
|
|
51
|
+
JSON.stringify({
|
|
52
|
+
id: checkpoint.id,
|
|
53
|
+
timestamp: checkpoint.timestamp,
|
|
54
|
+
toolCallId,
|
|
55
|
+
filePath,
|
|
56
|
+
exists: originalContent !== null
|
|
57
|
+
}),
|
|
58
|
+
"utf8"
|
|
59
|
+
);
|
|
60
|
+
return checkpoint;
|
|
61
|
+
}
|
|
62
|
+
getCheckpoints() {
|
|
63
|
+
return this.checkpoints;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// src/RollbackManager.ts
|
|
68
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync2, unlinkSync } from "fs";
|
|
69
|
+
import { resolveSafePath as resolveSafePath2 } from "@wangjiehu/shared";
|
|
70
|
+
var RollbackManager = class {
|
|
71
|
+
constructor(cwd) {
|
|
72
|
+
this.cwd = cwd;
|
|
73
|
+
}
|
|
74
|
+
cwd;
|
|
75
|
+
rollback(checkpoint) {
|
|
76
|
+
const restored = [];
|
|
77
|
+
for (const backup of checkpoint.backups) {
|
|
78
|
+
const safePath = resolveSafePath2(this.cwd, backup.path);
|
|
79
|
+
if (backup.originalContent === null) {
|
|
80
|
+
if (existsSync2(safePath)) {
|
|
81
|
+
unlinkSync(safePath);
|
|
82
|
+
restored.push(backup.path);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
writeFileSync2(safePath, backup.originalContent, "utf8");
|
|
86
|
+
restored.push(backup.path);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
success: true,
|
|
91
|
+
restored
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
export {
|
|
96
|
+
CheckpointManager,
|
|
97
|
+
RollbackManager
|
|
98
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wangjiehu/sandbox",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@wangjiehu/config": "0.1.0",
|
|
9
|
+
"@wangjiehu/shared": "0.1.0",
|
|
10
|
+
"@wangjiehu/tools": "0.1.0"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup src/index.ts --format esm --dts"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { existsSync, writeFileSync, readFileSync, rmSync, mkdirSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { CheckpointManager } from "./CheckpointManager.js";
|
|
6
|
+
import { RollbackManager } from "./RollbackManager.js";
|
|
7
|
+
|
|
8
|
+
describe("Sandbox Checkpoints and Rollbacks", () => {
|
|
9
|
+
let tempDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tempDir = join(tmpdir(), `orbit-sandbox-test-${Date.now()}`);
|
|
13
|
+
mkdirSync(tempDir, { recursive: true });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should capture JIT snapshot before edit and rollback successfully", async () => {
|
|
21
|
+
const filePath = "test.txt";
|
|
22
|
+
const absPath = join(tempDir, filePath);
|
|
23
|
+
|
|
24
|
+
writeFileSync(absPath, "initial-content", "utf8");
|
|
25
|
+
|
|
26
|
+
const cpManager = new CheckpointManager(tempDir, "session-123");
|
|
27
|
+
const rbManager = new RollbackManager(tempDir);
|
|
28
|
+
|
|
29
|
+
const checkpoint = await cpManager.captureBeforeState("call-1", filePath);
|
|
30
|
+
|
|
31
|
+
writeFileSync(absPath, "modified-content", "utf8");
|
|
32
|
+
expect(readFileSync(absPath, "utf8")).toBe("modified-content");
|
|
33
|
+
|
|
34
|
+
rbManager.rollback(checkpoint);
|
|
35
|
+
expect(readFileSync(absPath, "utf8")).toBe("initial-content");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should delete newly created files on rollback", async () => {
|
|
39
|
+
const filePath = "new-file.txt";
|
|
40
|
+
const absPath = join(tempDir, filePath);
|
|
41
|
+
|
|
42
|
+
const cpManager = new CheckpointManager(tempDir, "session-123");
|
|
43
|
+
const rbManager = new RollbackManager(tempDir);
|
|
44
|
+
|
|
45
|
+
const checkpoint = await cpManager.captureBeforeState("call-1", filePath);
|
|
46
|
+
|
|
47
|
+
writeFileSync(absPath, "brand-new-file", "utf8");
|
|
48
|
+
expect(existsSync(absPath)).toBe(true);
|
|
49
|
+
|
|
50
|
+
rbManager.rollback(checkpoint);
|
|
51
|
+
expect(existsSync(absPath)).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { generateId, resolveSafePath } from "@wangjiehu/shared";
|
|
4
|
+
import { FileBackup, Checkpoint } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export class CheckpointManager {
|
|
7
|
+
private checkpoints: Checkpoint[] = [];
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
private cwd: string,
|
|
11
|
+
private sessionId: string,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
public async captureBeforeState(
|
|
15
|
+
toolCallId: string,
|
|
16
|
+
filePath: string,
|
|
17
|
+
): Promise<Checkpoint> {
|
|
18
|
+
let originalContent: string | null = null;
|
|
19
|
+
try {
|
|
20
|
+
const safePath = resolveSafePath(this.cwd, filePath);
|
|
21
|
+
if (existsSync(safePath)) {
|
|
22
|
+
originalContent = readFileSync(safePath, "utf8");
|
|
23
|
+
}
|
|
24
|
+
} catch (e) {
|
|
25
|
+
// File does not exist yet or read failed
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const backup: FileBackup = {
|
|
29
|
+
path: filePath,
|
|
30
|
+
originalContent,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const checkpoint: Checkpoint = {
|
|
34
|
+
id: generateId("cp"),
|
|
35
|
+
sessionId: this.sessionId,
|
|
36
|
+
timestamp: new Date().toISOString(),
|
|
37
|
+
toolCallId,
|
|
38
|
+
backups: [backup],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
this.checkpoints.push(checkpoint);
|
|
42
|
+
|
|
43
|
+
const checkpointDir = join(
|
|
44
|
+
this.cwd,
|
|
45
|
+
".orbit",
|
|
46
|
+
"checkpoints",
|
|
47
|
+
this.sessionId,
|
|
48
|
+
checkpoint.id,
|
|
49
|
+
);
|
|
50
|
+
mkdirSync(checkpointDir, { recursive: true });
|
|
51
|
+
|
|
52
|
+
if (originalContent !== null) {
|
|
53
|
+
writeFileSync(
|
|
54
|
+
join(checkpointDir, "backup_content.txt"),
|
|
55
|
+
originalContent,
|
|
56
|
+
"utf8",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
writeFileSync(
|
|
60
|
+
join(checkpointDir, "meta.json"),
|
|
61
|
+
JSON.stringify({
|
|
62
|
+
id: checkpoint.id,
|
|
63
|
+
timestamp: checkpoint.timestamp,
|
|
64
|
+
toolCallId,
|
|
65
|
+
filePath,
|
|
66
|
+
exists: originalContent !== null,
|
|
67
|
+
}),
|
|
68
|
+
"utf8",
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return checkpoint;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public getCheckpoints(): Checkpoint[] {
|
|
75
|
+
return this.checkpoints;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync, writeFileSync, unlinkSync } from "fs";
|
|
2
|
+
import { resolveSafePath } from "@wangjiehu/shared";
|
|
3
|
+
import { Checkpoint } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export class RollbackManager {
|
|
6
|
+
constructor(private cwd: string) {}
|
|
7
|
+
|
|
8
|
+
public rollback(checkpoint: Checkpoint): {
|
|
9
|
+
success: boolean;
|
|
10
|
+
error?: string;
|
|
11
|
+
restored: string[];
|
|
12
|
+
} {
|
|
13
|
+
const restored: string[] = [];
|
|
14
|
+
|
|
15
|
+
for (const backup of checkpoint.backups) {
|
|
16
|
+
const safePath = resolveSafePath(this.cwd, backup.path);
|
|
17
|
+
|
|
18
|
+
if (backup.originalContent === null) {
|
|
19
|
+
// File did not exist before the tool execution, so delete it on rollback
|
|
20
|
+
if (existsSync(safePath)) {
|
|
21
|
+
unlinkSync(safePath);
|
|
22
|
+
restored.push(backup.path);
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
// Restore previous content
|
|
26
|
+
writeFileSync(safePath, backup.originalContent, "utf8");
|
|
27
|
+
restored.push(backup.path);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
success: true,
|
|
33
|
+
restored,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface FileBackup {
|
|
2
|
+
path: string;
|
|
3
|
+
originalContent: string | null; // null if the file did not exist before
|
|
4
|
+
originalHash?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface Checkpoint {
|
|
8
|
+
id: string;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
toolCallId: string;
|
|
12
|
+
backups: FileBackup[];
|
|
13
|
+
}
|