@lasso-ai/cli 1.3.2 → 1.3.3
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/cli/agent.js +18 -1
- package/dist/cli/bridge.d.ts +22 -0
- package/dist/cli/bridge.js +268 -74
- package/dist/cli/oneshot.js +59 -43
- package/dist/cli/snapshot.d.ts +33 -0
- package/dist/cli/snapshot.js +225 -0
- package/dist/overlay.js +443 -2
- package/package.json +2 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type FileSnapshot = {
|
|
2
|
+
filePath: string;
|
|
3
|
+
content: string | null;
|
|
4
|
+
hash: string;
|
|
5
|
+
timestamp: string;
|
|
6
|
+
};
|
|
7
|
+
export type TimelineSnapshot = {
|
|
8
|
+
id: string;
|
|
9
|
+
taskId: string;
|
|
10
|
+
summary: string;
|
|
11
|
+
files: FileSnapshot[];
|
|
12
|
+
createdAt: string;
|
|
13
|
+
parentSnapshotId?: string;
|
|
14
|
+
};
|
|
15
|
+
export type ConflictInfo = {
|
|
16
|
+
filePath: string;
|
|
17
|
+
expectedHash: string;
|
|
18
|
+
actualHash: string;
|
|
19
|
+
originalContent: string;
|
|
20
|
+
currentContent: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function createSnapshot(cwd: string, taskId: string, summary: string, filePaths: string[], parentSnapshotId?: string): TimelineSnapshot;
|
|
23
|
+
export declare function restoreSnapshot(cwd: string, snapshotId: string): boolean;
|
|
24
|
+
export declare function detectConflicts(cwd: string, expectedFiles: Array<{
|
|
25
|
+
filePath: string;
|
|
26
|
+
content: string | null;
|
|
27
|
+
hash?: string;
|
|
28
|
+
}>): ConflictInfo[];
|
|
29
|
+
export declare function getTimeline(cwd: string, limit?: number): TimelineSnapshot[];
|
|
30
|
+
export declare function deleteSnapshot(cwd: string, snapshotId: string): boolean;
|
|
31
|
+
export declare function clearSnapshots(cwd: string): void;
|
|
32
|
+
export declare function getSnapshotByTaskId(cwd: string, taskId: string): TimelineSnapshot | undefined;
|
|
33
|
+
export declare function getLatestSnapshot(cwd: string): TimelineSnapshot | undefined;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createSnapshot = createSnapshot;
|
|
7
|
+
exports.restoreSnapshot = restoreSnapshot;
|
|
8
|
+
exports.detectConflicts = detectConflicts;
|
|
9
|
+
exports.getTimeline = getTimeline;
|
|
10
|
+
exports.deleteSnapshot = deleteSnapshot;
|
|
11
|
+
exports.clearSnapshots = clearSnapshots;
|
|
12
|
+
exports.getSnapshotByTaskId = getSnapshotByTaskId;
|
|
13
|
+
exports.getLatestSnapshot = getLatestSnapshot;
|
|
14
|
+
// src/cli/snapshot.ts
|
|
15
|
+
// Local history snapshot system for safe concurrent edits
|
|
16
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
17
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
18
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
19
|
+
const SNAPSHOTS_DIR = ".lasso/snapshots";
|
|
20
|
+
const TIMELINE_FILE = ".lasso/timeline.json";
|
|
21
|
+
const MAX_SNAPSHOTS = 50;
|
|
22
|
+
// Ensure snapshots directory exists
|
|
23
|
+
function ensureSnapshotsDir(cwd) {
|
|
24
|
+
const dir = node_path_1.default.join(cwd, SNAPSHOTS_DIR);
|
|
25
|
+
if (!node_fs_1.default.existsSync(dir)) {
|
|
26
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Generate content hash
|
|
30
|
+
function hashContent(content) {
|
|
31
|
+
return node_crypto_1.default.createHash("sha256").update(content).digest("hex");
|
|
32
|
+
}
|
|
33
|
+
// Generate unique snapshot ID
|
|
34
|
+
function generateSnapshotId() {
|
|
35
|
+
return `snap-${Date.now()}-${node_crypto_1.default.randomBytes(4).toString("hex")}`;
|
|
36
|
+
}
|
|
37
|
+
// Load timeline from disk
|
|
38
|
+
function loadTimeline(cwd) {
|
|
39
|
+
const timelinePath = node_path_1.default.join(cwd, TIMELINE_FILE);
|
|
40
|
+
try {
|
|
41
|
+
if (node_fs_1.default.existsSync(timelinePath)) {
|
|
42
|
+
const data = node_fs_1.default.readFileSync(timelinePath, "utf8");
|
|
43
|
+
return JSON.parse(data);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// File might be corrupted, start fresh
|
|
48
|
+
}
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
// Save timeline to disk
|
|
52
|
+
function saveTimeline(cwd, timeline) {
|
|
53
|
+
const timelinePath = node_path_1.default.join(cwd, TIMELINE_FILE);
|
|
54
|
+
try {
|
|
55
|
+
node_fs_1.default.writeFileSync(timelinePath, JSON.stringify(timeline, null, 2));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.warn("[lasso] Failed to save timeline:", error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Create a snapshot of current file states
|
|
62
|
+
function createSnapshot(cwd, taskId, summary, filePaths, parentSnapshotId) {
|
|
63
|
+
ensureSnapshotsDir(cwd);
|
|
64
|
+
const snapshotId = generateSnapshotId();
|
|
65
|
+
const files = [];
|
|
66
|
+
const timestamp = new Date().toISOString();
|
|
67
|
+
for (const filePath of filePaths) {
|
|
68
|
+
const fullPath = node_path_1.default.resolve(cwd, filePath);
|
|
69
|
+
let content = null;
|
|
70
|
+
let hash = "";
|
|
71
|
+
try {
|
|
72
|
+
if (node_fs_1.default.existsSync(fullPath)) {
|
|
73
|
+
content = node_fs_1.default.readFileSync(fullPath, "utf8");
|
|
74
|
+
hash = hashContent(content);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.warn(`[lasso] Failed to snapshot ${filePath}:`, error);
|
|
79
|
+
}
|
|
80
|
+
files.push({
|
|
81
|
+
filePath,
|
|
82
|
+
content,
|
|
83
|
+
hash,
|
|
84
|
+
timestamp,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const snapshot = {
|
|
88
|
+
id: snapshotId,
|
|
89
|
+
taskId,
|
|
90
|
+
summary,
|
|
91
|
+
files,
|
|
92
|
+
createdAt: timestamp,
|
|
93
|
+
parentSnapshotId,
|
|
94
|
+
};
|
|
95
|
+
// Update timeline
|
|
96
|
+
const timeline = loadTimeline(cwd);
|
|
97
|
+
timeline.unshift(snapshot);
|
|
98
|
+
// Trim old snapshots
|
|
99
|
+
if (timeline.length > MAX_SNAPSHOTS) {
|
|
100
|
+
const removed = timeline.splice(MAX_SNAPSHOTS);
|
|
101
|
+
// Cleanup old snapshot files
|
|
102
|
+
for (const old of removed) {
|
|
103
|
+
cleanupSnapshotFiles(cwd, old.id);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
saveTimeline(cwd, timeline);
|
|
107
|
+
return snapshot;
|
|
108
|
+
}
|
|
109
|
+
// Restore a snapshot
|
|
110
|
+
function restoreSnapshot(cwd, snapshotId) {
|
|
111
|
+
const timeline = loadTimeline(cwd);
|
|
112
|
+
const snapshot = timeline.find((s) => s.id === snapshotId);
|
|
113
|
+
if (!snapshot) {
|
|
114
|
+
console.warn(`[lasso] Snapshot ${snapshotId} not found`);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
for (const file of snapshot.files) {
|
|
119
|
+
const fullPath = node_path_1.default.resolve(cwd, file.filePath);
|
|
120
|
+
if (file.content === null) {
|
|
121
|
+
// File didn't exist, delete if it exists now
|
|
122
|
+
if (node_fs_1.default.existsSync(fullPath)) {
|
|
123
|
+
node_fs_1.default.unlinkSync(fullPath);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// Restore file content
|
|
128
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(fullPath), { recursive: true });
|
|
129
|
+
node_fs_1.default.writeFileSync(fullPath, file.content);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
console.log(`[lasso] Restored snapshot ${snapshotId}: ${snapshot.summary}`);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
console.error(`[lasso] Failed to restore snapshot ${snapshotId}:`, error);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Check for conflicts between expected and current file states
|
|
141
|
+
function detectConflicts(cwd, expectedFiles) {
|
|
142
|
+
const conflicts = [];
|
|
143
|
+
for (const expected of expectedFiles) {
|
|
144
|
+
const fullPath = node_path_1.default.resolve(cwd, expected.filePath);
|
|
145
|
+
const expectedHash = expected.hash || (expected.content ? hashContent(expected.content) : "");
|
|
146
|
+
try {
|
|
147
|
+
if (!node_fs_1.default.existsSync(fullPath)) {
|
|
148
|
+
// File doesn't exist but was expected
|
|
149
|
+
if (expected.content !== null) {
|
|
150
|
+
conflicts.push({
|
|
151
|
+
filePath: expected.filePath,
|
|
152
|
+
expectedHash,
|
|
153
|
+
actualHash: "file-missing",
|
|
154
|
+
originalContent: expected.content || "",
|
|
155
|
+
currentContent: "",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const currentContent = node_fs_1.default.readFileSync(fullPath, "utf8");
|
|
161
|
+
const actualHash = hashContent(currentContent);
|
|
162
|
+
if (expectedHash !== actualHash) {
|
|
163
|
+
conflicts.push({
|
|
164
|
+
filePath: expected.filePath,
|
|
165
|
+
expectedHash,
|
|
166
|
+
actualHash,
|
|
167
|
+
originalContent: expected.content || "",
|
|
168
|
+
currentContent,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
console.warn(`[lasso] Failed to check conflict for ${expected.filePath}:`, error);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return conflicts;
|
|
177
|
+
}
|
|
178
|
+
// Get timeline history
|
|
179
|
+
function getTimeline(cwd, limit = 20) {
|
|
180
|
+
const timeline = loadTimeline(cwd);
|
|
181
|
+
return timeline.slice(0, limit);
|
|
182
|
+
}
|
|
183
|
+
// Delete a snapshot
|
|
184
|
+
function deleteSnapshot(cwd, snapshotId) {
|
|
185
|
+
const timeline = loadTimeline(cwd);
|
|
186
|
+
const index = timeline.findIndex((s) => s.id === snapshotId);
|
|
187
|
+
if (index === -1) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
timeline.splice(index, 1);
|
|
191
|
+
saveTimeline(cwd, timeline);
|
|
192
|
+
cleanupSnapshotFiles(cwd, snapshotId);
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
// Cleanup snapshot-specific files (if we implement per-snapshot storage)
|
|
196
|
+
function cleanupSnapshotFiles(cwd, snapshotId) {
|
|
197
|
+
// Currently all data is in timeline.json, but if we add per-snapshot files,
|
|
198
|
+
// this is where we'd clean them up
|
|
199
|
+
}
|
|
200
|
+
// Clear all snapshots
|
|
201
|
+
function clearSnapshots(cwd) {
|
|
202
|
+
const timelinePath = node_path_1.default.join(cwd, TIMELINE_FILE);
|
|
203
|
+
try {
|
|
204
|
+
if (node_fs_1.default.existsSync(timelinePath)) {
|
|
205
|
+
node_fs_1.default.unlinkSync(timelinePath);
|
|
206
|
+
}
|
|
207
|
+
const snapshotsDir = node_path_1.default.join(cwd, SNAPSHOTS_DIR);
|
|
208
|
+
if (node_fs_1.default.existsSync(snapshotsDir)) {
|
|
209
|
+
node_fs_1.default.rmSync(snapshotsDir, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
console.warn("[lasso] Failed to clear snapshots:", error);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Get snapshot by task ID
|
|
217
|
+
function getSnapshotByTaskId(cwd, taskId) {
|
|
218
|
+
const timeline = loadTimeline(cwd);
|
|
219
|
+
return timeline.find((s) => s.taskId === taskId);
|
|
220
|
+
}
|
|
221
|
+
// Get latest snapshot
|
|
222
|
+
function getLatestSnapshot(cwd) {
|
|
223
|
+
const timeline = loadTimeline(cwd);
|
|
224
|
+
return timeline[0];
|
|
225
|
+
}
|