shell-dsl 0.0.31 → 0.0.33
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/README.md +110 -1
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/commands/mv/mv.cjs +7 -1
- package/dist/cjs/src/commands/mv/mv.cjs.map +3 -3
- package/dist/cjs/src/commands/tree/tree.cjs +64 -27
- package/dist/cjs/src/commands/tree/tree.cjs.map +3 -3
- package/dist/cjs/src/fs/memfs-adapter.cjs +26 -3
- package/dist/cjs/src/fs/memfs-adapter.cjs.map +3 -3
- package/dist/cjs/src/fs/real-fs.cjs +32 -1
- package/dist/cjs/src/fs/real-fs.cjs.map +3 -3
- package/dist/cjs/src/fs/special-files.cjs +98 -0
- package/dist/cjs/src/fs/special-files.cjs.map +10 -0
- package/dist/cjs/src/index.cjs +3 -1
- package/dist/cjs/src/index.cjs.map +3 -3
- package/dist/cjs/src/interpreter/interpreter.cjs +9 -8
- package/dist/cjs/src/interpreter/interpreter.cjs.map +3 -3
- package/dist/cjs/src/vcs/diff.cjs +102 -0
- package/dist/cjs/src/vcs/diff.cjs.map +10 -0
- package/dist/cjs/src/vcs/index.cjs +47 -0
- package/dist/cjs/src/vcs/index.cjs.map +10 -0
- package/dist/cjs/src/vcs/match.cjs +106 -0
- package/dist/cjs/src/vcs/match.cjs.map +10 -0
- package/dist/cjs/src/vcs/snapshot.cjs +112 -0
- package/dist/cjs/src/vcs/snapshot.cjs.map +10 -0
- package/dist/cjs/src/vcs/storage.cjs +120 -0
- package/dist/cjs/src/vcs/storage.cjs.map +10 -0
- package/dist/cjs/src/vcs/types.cjs +30 -0
- package/dist/cjs/src/vcs/types.cjs.map +9 -0
- package/dist/cjs/src/vcs/vcs.cjs +305 -0
- package/dist/cjs/src/vcs/vcs.cjs.map +10 -0
- package/dist/cjs/src/vcs/walk.cjs +83 -0
- package/dist/cjs/src/vcs/walk.cjs.map +10 -0
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/src/commands/mv/mv.mjs +7 -1
- package/dist/mjs/src/commands/mv/mv.mjs.map +3 -3
- package/dist/mjs/src/commands/tree/tree.mjs +64 -27
- package/dist/mjs/src/commands/tree/tree.mjs.map +3 -3
- package/dist/mjs/src/fs/memfs-adapter.mjs +32 -3
- package/dist/mjs/src/fs/memfs-adapter.mjs.map +3 -3
- package/dist/mjs/src/fs/real-fs.mjs +38 -1
- package/dist/mjs/src/fs/real-fs.mjs.map +3 -3
- package/dist/mjs/src/fs/special-files.mjs +58 -0
- package/dist/mjs/src/fs/special-files.mjs.map +10 -0
- package/dist/mjs/src/index.mjs +3 -1
- package/dist/mjs/src/index.mjs.map +3 -3
- package/dist/mjs/src/interpreter/interpreter.mjs +9 -8
- package/dist/mjs/src/interpreter/interpreter.mjs.map +3 -3
- package/dist/mjs/src/vcs/diff.mjs +62 -0
- package/dist/mjs/src/vcs/diff.mjs.map +10 -0
- package/dist/mjs/src/vcs/index.mjs +7 -0
- package/dist/mjs/src/vcs/index.mjs.map +10 -0
- package/dist/mjs/src/vcs/match.mjs +66 -0
- package/dist/mjs/src/vcs/match.mjs.map +10 -0
- package/dist/mjs/src/vcs/snapshot.mjs +72 -0
- package/dist/mjs/src/vcs/snapshot.mjs.map +10 -0
- package/dist/mjs/src/vcs/storage.mjs +79 -0
- package/dist/mjs/src/vcs/storage.mjs.map +10 -0
- package/dist/mjs/src/vcs/types.mjs +2 -0
- package/dist/mjs/src/vcs/types.mjs.map +9 -0
- package/dist/mjs/src/vcs/vcs.mjs +265 -0
- package/dist/mjs/src/vcs/vcs.mjs.map +10 -0
- package/dist/mjs/src/vcs/walk.mjs +43 -0
- package/dist/mjs/src/vcs/walk.mjs.map +10 -0
- package/dist/types/src/fs/special-files.d.ts +8 -0
- package/dist/types/src/index.d.ts +2 -0
- package/dist/types/src/vcs/diff.d.ts +10 -0
- package/dist/types/src/vcs/index.d.ts +2 -0
- package/dist/types/src/vcs/match.d.ts +5 -0
- package/dist/types/src/vcs/snapshot.d.ts +20 -0
- package/dist/types/src/vcs/storage.d.ts +22 -0
- package/dist/types/src/vcs/types.d.ts +74 -0
- package/dist/types/src/vcs/vcs.d.ts +35 -0
- package/dist/types/src/vcs/walk.d.ts +7 -0
- package/package.json +1 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/vcs/storage.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type { VirtualFS } from \"../types.mjs\";\nimport type {\n HeadRef,\n BranchRef,\n RevisionCounter,\n VCSConfigFile,\n Revision,\n} from \"./types.mjs\";\n\nexport class VCSStorage {\n constructor(\n private readonly fs: VirtualFS,\n private readonly basePath: string,\n ) {}\n\n private path(...segments: string[]): string {\n return this.fs.resolve(this.basePath, ...segments);\n }\n\n // --- Init ---\n\n async isInitialized(): Promise<boolean> {\n return this.fs.exists(this.path(\"HEAD\"));\n }\n\n async initialize(defaultBranch: string = \"main\"): Promise<void> {\n await this.fs.mkdir(this.basePath, { recursive: true });\n await this.fs.mkdir(this.path(\"refs\", \"heads\"), { recursive: true });\n await this.fs.mkdir(this.path(\"revisions\"), { recursive: true });\n\n await this.writeHead({ ref: `refs/heads/${defaultBranch}` });\n await this.writeJSON([\"counter.json\"], { next: 1 } satisfies RevisionCounter);\n await this.writeJSON([\"config.json\"], { defaultBranch } satisfies VCSConfigFile);\n }\n\n // --- HEAD ---\n\n async readHead(): Promise<HeadRef> {\n return this.readJSON<HeadRef>(\"HEAD\");\n }\n\n async writeHead(head: HeadRef): Promise<void> {\n await this.writeJSON([\"HEAD\"], head);\n }\n\n // --- Branches ---\n\n async readBranch(name: string): Promise<BranchRef | null> {\n const branchPath = this.path(\"refs\", \"heads\", name);\n if (!(await this.fs.exists(branchPath))) return null;\n return this.readJSON<BranchRef>(\"refs\", \"heads\", name);\n }\n\n async writeBranch(name: string, ref: BranchRef): Promise<void> {\n await this.writeJSON([\"refs\", \"heads\", name], ref);\n }\n\n async deleteBranch(name: string): Promise<void> {\n const branchPath = this.path(\"refs\", \"heads\", name);\n await this.fs.rm(branchPath);\n }\n\n async listBranches(): Promise<string[]> {\n const headsPath = this.path(\"refs\", \"heads\");\n try {\n return await this.fs.readdir(headsPath);\n } catch {\n return [];\n }\n }\n\n // --- Revisions ---\n\n async readRevision(id: number): Promise<Revision> {\n return this.readJSON<Revision>(\"revisions\", `${id}.json`);\n }\n\n async writeRevision(rev: Revision): Promise<void> {\n await this.writeJSON([\"revisions\", `${rev.id}.json`], rev);\n }\n\n // --- Counter ---\n\n async nextRevisionId(): Promise<number> {\n const counter = await this.readJSON<RevisionCounter>(\"counter.json\");\n const id = counter.next;\n await this.writeJSON([\"counter.json\"], { next: id + 1 } satisfies RevisionCounter);\n return id;\n }\n\n // --- Config ---\n\n async readConfig(): Promise<VCSConfigFile> {\n return this.readJSON<VCSConfigFile>(\"config.json\");\n }\n\n // --- Helpers ---\n\n private async readJSON<T>(...segments: string[]): Promise<T> {\n const filePath = this.path(...segments);\n const content = await this.fs.readFile(filePath, \"utf8\");\n return JSON.parse(content) as T;\n }\n\n private async writeJSON(segments: string[], data: unknown): Promise<void> {\n const filePath = this.path(...segments);\n await this.fs.writeFile(filePath, JSON.stringify(data, null, 2));\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "63A4490666B1400E64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// src/vcs/vcs.ts
|
|
2
|
+
import { VCSStorage } from "./storage.mjs";
|
|
3
|
+
import { diffManifests, diffWorkingTree } from "./diff.mjs";
|
|
4
|
+
import { buildTreeManifest, restoreTree } from "./snapshot.mjs";
|
|
5
|
+
import { matchGlobPath } from "./match.mjs";
|
|
6
|
+
|
|
7
|
+
class VersionControlSystem {
|
|
8
|
+
workFs;
|
|
9
|
+
workPath;
|
|
10
|
+
storage;
|
|
11
|
+
vcsDirName;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.workFs = config.fs;
|
|
14
|
+
this.workPath = config.fs.resolve(config.path);
|
|
15
|
+
const metaFs = config.vcsPath?.fs ?? config.fs;
|
|
16
|
+
const metaPath = config.vcsPath?.path ?? metaFs.resolve(config.path, ".vcs");
|
|
17
|
+
this.storage = new VCSStorage(metaFs, metaPath);
|
|
18
|
+
const resolvedMeta = metaFs.resolve(metaPath);
|
|
19
|
+
const resolvedWork = this.workPath;
|
|
20
|
+
if (resolvedMeta.startsWith(resolvedWork + "/") || resolvedMeta.startsWith(resolvedWork + "\\")) {
|
|
21
|
+
const rel = resolvedMeta.slice(resolvedWork.length + 1);
|
|
22
|
+
this.vcsDirName = rel.split("/")[0] ?? ".vcs";
|
|
23
|
+
} else {
|
|
24
|
+
this.vcsDirName = "";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
get excludeDirs() {
|
|
28
|
+
return this.vcsDirName ? [this.vcsDirName] : [];
|
|
29
|
+
}
|
|
30
|
+
async init() {
|
|
31
|
+
if (await this.storage.isInitialized())
|
|
32
|
+
return;
|
|
33
|
+
await this.storage.initialize();
|
|
34
|
+
}
|
|
35
|
+
async ensureInit() {
|
|
36
|
+
if (!await this.storage.isInitialized()) {
|
|
37
|
+
await this.init();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async resolveHead() {
|
|
41
|
+
const head = await this.storage.readHead();
|
|
42
|
+
if (head.revision !== undefined) {
|
|
43
|
+
return { branch: null, revision: head.revision };
|
|
44
|
+
}
|
|
45
|
+
if (head.ref) {
|
|
46
|
+
const branchName = head.ref.replace("refs/heads/", "");
|
|
47
|
+
const branchRef = await this.storage.readBranch(branchName);
|
|
48
|
+
return { branch: branchName, revision: branchRef?.revision ?? null };
|
|
49
|
+
}
|
|
50
|
+
return { branch: null, revision: null };
|
|
51
|
+
}
|
|
52
|
+
async headManifest() {
|
|
53
|
+
const { revision } = await this.resolveHead();
|
|
54
|
+
if (revision === null)
|
|
55
|
+
return {};
|
|
56
|
+
const rev = await this.storage.readRevision(revision);
|
|
57
|
+
return rev.tree;
|
|
58
|
+
}
|
|
59
|
+
async commit(message, opts) {
|
|
60
|
+
await this.ensureInit();
|
|
61
|
+
const { branch, revision: parentId } = await this.resolveHead();
|
|
62
|
+
const parentManifest = parentId !== null ? (await this.storage.readRevision(parentId)).tree : {};
|
|
63
|
+
let newTree;
|
|
64
|
+
let changes;
|
|
65
|
+
if (opts?.paths && opts.paths.length > 0) {
|
|
66
|
+
const fullManifest = await buildTreeManifest(this.workFs, this.workPath, this.excludeDirs);
|
|
67
|
+
const matchedPaths = filterPathsByGlobs(Object.keys(fullManifest), opts.paths);
|
|
68
|
+
newTree = { ...parentManifest };
|
|
69
|
+
const parentMatchedPaths = filterPathsByGlobs(Object.keys(parentManifest), opts.paths);
|
|
70
|
+
for (const p of parentMatchedPaths) {
|
|
71
|
+
if (!fullManifest[p]) {
|
|
72
|
+
delete newTree[p];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const p of matchedPaths) {
|
|
76
|
+
newTree[p] = fullManifest[p];
|
|
77
|
+
}
|
|
78
|
+
const relevantBefore = {};
|
|
79
|
+
const relevantAfter = {};
|
|
80
|
+
const allRelevant = new Set([...matchedPaths, ...parentMatchedPaths]);
|
|
81
|
+
for (const p of allRelevant) {
|
|
82
|
+
if (parentManifest[p])
|
|
83
|
+
relevantBefore[p] = parentManifest[p];
|
|
84
|
+
if (newTree[p])
|
|
85
|
+
relevantAfter[p] = newTree[p];
|
|
86
|
+
}
|
|
87
|
+
changes = diffManifests(relevantBefore, relevantAfter);
|
|
88
|
+
} else {
|
|
89
|
+
newTree = await buildTreeManifest(this.workFs, this.workPath, this.excludeDirs);
|
|
90
|
+
changes = diffManifests(parentManifest, newTree);
|
|
91
|
+
}
|
|
92
|
+
if (changes.length === 0) {
|
|
93
|
+
throw new Error("nothing to commit");
|
|
94
|
+
}
|
|
95
|
+
const id = await this.storage.nextRevisionId();
|
|
96
|
+
const rev = {
|
|
97
|
+
id,
|
|
98
|
+
parent: parentId,
|
|
99
|
+
branch: branch ?? "detached",
|
|
100
|
+
message,
|
|
101
|
+
timestamp: new Date().toISOString(),
|
|
102
|
+
changes,
|
|
103
|
+
tree: newTree
|
|
104
|
+
};
|
|
105
|
+
await this.storage.writeRevision(rev);
|
|
106
|
+
if (branch) {
|
|
107
|
+
await this.storage.writeBranch(branch, { revision: id });
|
|
108
|
+
} else {
|
|
109
|
+
await this.storage.writeHead({ revision: id });
|
|
110
|
+
}
|
|
111
|
+
return rev;
|
|
112
|
+
}
|
|
113
|
+
async checkout(target, opts) {
|
|
114
|
+
await this.ensureInit();
|
|
115
|
+
const isPartial = opts?.paths && opts.paths.length > 0;
|
|
116
|
+
let targetRevision;
|
|
117
|
+
let targetBranch = null;
|
|
118
|
+
if (typeof target === "string") {
|
|
119
|
+
const branchRef = await this.storage.readBranch(target);
|
|
120
|
+
if (branchRef) {
|
|
121
|
+
targetBranch = target;
|
|
122
|
+
targetRevision = branchRef.revision;
|
|
123
|
+
} else {
|
|
124
|
+
throw new Error(`unknown branch or revision: "${target}"`);
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
targetRevision = target;
|
|
128
|
+
}
|
|
129
|
+
let rev;
|
|
130
|
+
try {
|
|
131
|
+
rev = await this.storage.readRevision(targetRevision);
|
|
132
|
+
} catch {
|
|
133
|
+
throw new Error(`revision ${targetRevision} not found`);
|
|
134
|
+
}
|
|
135
|
+
if (isPartial) {
|
|
136
|
+
const matchedPaths = filterPathsByGlobs(Object.keys(rev.tree), opts.paths);
|
|
137
|
+
const filteredManifest = {};
|
|
138
|
+
for (const p of matchedPaths) {
|
|
139
|
+
filteredManifest[p] = rev.tree[p];
|
|
140
|
+
}
|
|
141
|
+
await restoreTree(this.workFs, this.workPath, filteredManifest, {
|
|
142
|
+
fullRestore: false,
|
|
143
|
+
paths: matchedPaths
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
if (!opts?.force) {
|
|
147
|
+
const changes = await this.status();
|
|
148
|
+
if (changes.length > 0) {
|
|
149
|
+
throw new Error("working tree has uncommitted changes (use force to discard)");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
await restoreTree(this.workFs, this.workPath, rev.tree, { fullRestore: true });
|
|
153
|
+
if (targetBranch) {
|
|
154
|
+
await this.storage.writeHead({ ref: `refs/heads/${targetBranch}` });
|
|
155
|
+
} else {
|
|
156
|
+
await this.storage.writeHead({ revision: targetRevision });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async branch(name) {
|
|
161
|
+
await this.ensureInit();
|
|
162
|
+
const existing = await this.storage.readBranch(name);
|
|
163
|
+
if (existing) {
|
|
164
|
+
throw new Error(`branch "${name}" already exists`);
|
|
165
|
+
}
|
|
166
|
+
const { revision } = await this.resolveHead();
|
|
167
|
+
if (revision === null) {
|
|
168
|
+
throw new Error("cannot create branch: no commits yet");
|
|
169
|
+
}
|
|
170
|
+
await this.storage.writeBranch(name, { revision });
|
|
171
|
+
}
|
|
172
|
+
async branches() {
|
|
173
|
+
await this.ensureInit();
|
|
174
|
+
const names = await this.storage.listBranches();
|
|
175
|
+
const head = await this.resolveHead();
|
|
176
|
+
const result = [];
|
|
177
|
+
for (const name of names) {
|
|
178
|
+
const ref = await this.storage.readBranch(name);
|
|
179
|
+
if (ref) {
|
|
180
|
+
result.push({
|
|
181
|
+
name,
|
|
182
|
+
revision: ref.revision,
|
|
183
|
+
current: head.branch === name
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return result.sort((a, b) => a.name.localeCompare(b.name));
|
|
188
|
+
}
|
|
189
|
+
async log(opts) {
|
|
190
|
+
await this.ensureInit();
|
|
191
|
+
let startRevision;
|
|
192
|
+
if (opts?.branch) {
|
|
193
|
+
const branchRef = await this.storage.readBranch(opts.branch);
|
|
194
|
+
if (!branchRef)
|
|
195
|
+
throw new Error(`branch "${opts.branch}" not found`);
|
|
196
|
+
startRevision = branchRef.revision;
|
|
197
|
+
} else {
|
|
198
|
+
const { revision } = await this.resolveHead();
|
|
199
|
+
startRevision = revision;
|
|
200
|
+
}
|
|
201
|
+
if (startRevision === null)
|
|
202
|
+
return [];
|
|
203
|
+
const entries = [];
|
|
204
|
+
let currentId = startRevision;
|
|
205
|
+
while (currentId !== null) {
|
|
206
|
+
if (opts?.limit && entries.length >= opts.limit)
|
|
207
|
+
break;
|
|
208
|
+
let rev;
|
|
209
|
+
try {
|
|
210
|
+
rev = await this.storage.readRevision(currentId);
|
|
211
|
+
} catch {
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
const changedPaths = rev.changes.map((c) => c.path);
|
|
215
|
+
if (opts?.path) {
|
|
216
|
+
const matchesPath = changedPaths.some((p) => matchGlobPath(opts.path, p));
|
|
217
|
+
if (matchesPath) {
|
|
218
|
+
entries.push({
|
|
219
|
+
id: rev.id,
|
|
220
|
+
parent: rev.parent,
|
|
221
|
+
branch: rev.branch,
|
|
222
|
+
message: rev.message,
|
|
223
|
+
timestamp: rev.timestamp,
|
|
224
|
+
paths: changedPaths
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
entries.push({
|
|
229
|
+
id: rev.id,
|
|
230
|
+
parent: rev.parent,
|
|
231
|
+
branch: rev.branch,
|
|
232
|
+
message: rev.message,
|
|
233
|
+
timestamp: rev.timestamp,
|
|
234
|
+
paths: changedPaths
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
currentId = rev.parent;
|
|
238
|
+
}
|
|
239
|
+
return entries;
|
|
240
|
+
}
|
|
241
|
+
async status() {
|
|
242
|
+
await this.ensureInit();
|
|
243
|
+
const manifest = await this.headManifest();
|
|
244
|
+
return diffWorkingTree(this.workFs, this.workPath, manifest, this.excludeDirs);
|
|
245
|
+
}
|
|
246
|
+
async diff(revA, revB) {
|
|
247
|
+
await this.ensureInit();
|
|
248
|
+
const a = await this.storage.readRevision(revA);
|
|
249
|
+
const b = await this.storage.readRevision(revB);
|
|
250
|
+
return diffManifests(a.tree, b.tree);
|
|
251
|
+
}
|
|
252
|
+
async head() {
|
|
253
|
+
await this.ensureInit();
|
|
254
|
+
return this.resolveHead();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function filterPathsByGlobs(paths, patterns) {
|
|
258
|
+
const normalizedPatterns = patterns.map((p) => p.startsWith("/") ? p.slice(1) : p);
|
|
259
|
+
return paths.filter((filePath) => normalizedPatterns.some((pattern) => matchGlobPath(pattern, filePath)));
|
|
260
|
+
}
|
|
261
|
+
export {
|
|
262
|
+
VersionControlSystem
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
//# debugId=09DEFDF82D3AF06864756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/vcs/vcs.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type { VirtualFS } from \"../types.mjs\";\nimport type {\n VCSConfig,\n Revision,\n DiffEntry,\n TreeManifest,\n CommitOptions,\n CheckoutOptions,\n LogOptions,\n LogEntry,\n BranchInfo,\n} from \"./types.mjs\";\nimport { VCSStorage } from \"./storage.mjs\";\nimport { diffManifests, diffWorkingTree } from \"./diff.mjs\";\nimport { buildTreeManifest, restoreTree } from \"./snapshot.mjs\";\nimport { matchGlobPath } from \"./match.mjs\";\n\nexport class VersionControlSystem {\n private readonly workFs: VirtualFS;\n private readonly workPath: string;\n private readonly storage: VCSStorage;\n private readonly vcsDirName: string;\n\n constructor(config: VCSConfig) {\n this.workFs = config.fs;\n this.workPath = config.fs.resolve(config.path);\n\n const metaFs = config.vcsPath?.fs ?? config.fs;\n const metaPath = config.vcsPath?.path ?? metaFs.resolve(config.path, \".vcs\");\n this.storage = new VCSStorage(metaFs, metaPath);\n\n // Determine the vcs directory name relative to workPath for exclusion\n const resolvedMeta = metaFs.resolve(metaPath);\n const resolvedWork = this.workPath;\n if (resolvedMeta.startsWith(resolvedWork + \"/\") || resolvedMeta.startsWith(resolvedWork + \"\\\\\")) {\n const rel = resolvedMeta.slice(resolvedWork.length + 1);\n this.vcsDirName = rel.split(\"/\")[0] ?? \".vcs\";\n } else {\n // VCS dir is outside the work tree, no exclusion needed\n this.vcsDirName = \"\";\n }\n }\n\n private get excludeDirs(): string[] {\n return this.vcsDirName ? [this.vcsDirName] : [];\n }\n\n /** Initialize the .vcs directory. Called automatically on first operation if needed. */\n async init(): Promise<void> {\n if (await this.storage.isInitialized()) return;\n await this.storage.initialize();\n }\n\n private async ensureInit(): Promise<void> {\n if (!(await this.storage.isInitialized())) {\n await this.init();\n }\n }\n\n /** Get the current HEAD revision number, or null if no commits yet. */\n private async resolveHead(): Promise<{ branch: string | null; revision: number | null }> {\n const head = await this.storage.readHead();\n if (head.revision !== undefined) {\n return { branch: null, revision: head.revision };\n }\n if (head.ref) {\n const branchName = head.ref.replace(\"refs/heads/\", \"\");\n const branchRef = await this.storage.readBranch(branchName);\n return { branch: branchName, revision: branchRef?.revision ?? null };\n }\n return { branch: null, revision: null };\n }\n\n /** Get current HEAD manifest, or empty if no commits. */\n private async headManifest(): Promise<TreeManifest> {\n const { revision } = await this.resolveHead();\n if (revision === null) return {};\n const rev = await this.storage.readRevision(revision);\n return rev.tree;\n }\n\n /** Commit all pending changes, or selective changes if paths are provided. */\n async commit(message: string, opts?: CommitOptions): Promise<Revision> {\n await this.ensureInit();\n\n const { branch, revision: parentId } = await this.resolveHead();\n const parentManifest = parentId !== null\n ? (await this.storage.readRevision(parentId)).tree\n : {};\n\n let newTree: TreeManifest;\n let changes: DiffEntry[];\n\n if (opts?.paths && opts.paths.length > 0) {\n // Selective commit: only include matching files\n const fullManifest = await buildTreeManifest(this.workFs, this.workPath, this.excludeDirs);\n const matchedPaths = filterPathsByGlobs(Object.keys(fullManifest), opts.paths);\n\n // Start with parent manifest, overlay matched files from working tree\n newTree = { ...parentManifest };\n\n // Also check for deletions: files in parent that match patterns but are gone from working tree\n const parentMatchedPaths = filterPathsByGlobs(Object.keys(parentManifest), opts.paths);\n for (const p of parentMatchedPaths) {\n if (!fullManifest[p]) {\n delete newTree[p]; // file was deleted\n }\n }\n\n for (const p of matchedPaths) {\n newTree[p] = fullManifest[p]!;\n }\n\n // Compute changes only for matched paths\n const relevantBefore: TreeManifest = {};\n const relevantAfter: TreeManifest = {};\n const allRelevant = new Set([...matchedPaths, ...parentMatchedPaths]);\n for (const p of allRelevant) {\n if (parentManifest[p]) relevantBefore[p] = parentManifest[p]!;\n if (newTree[p]) relevantAfter[p] = newTree[p]!;\n }\n changes = diffManifests(relevantBefore, relevantAfter);\n } else {\n // Full commit\n newTree = await buildTreeManifest(this.workFs, this.workPath, this.excludeDirs);\n changes = diffManifests(parentManifest, newTree);\n }\n\n if (changes.length === 0) {\n throw new Error(\"nothing to commit\");\n }\n\n const id = await this.storage.nextRevisionId();\n const rev: Revision = {\n id,\n parent: parentId,\n branch: branch ?? \"detached\",\n message,\n timestamp: new Date().toISOString(),\n changes,\n tree: newTree,\n };\n\n await this.storage.writeRevision(rev);\n\n // Update branch ref or HEAD\n if (branch) {\n await this.storage.writeBranch(branch, { revision: id });\n } else {\n await this.storage.writeHead({ revision: id });\n }\n\n return rev;\n }\n\n /** Checkout a revision number or branch name. */\n async checkout(target: string | number, opts?: CheckoutOptions): Promise<void> {\n await this.ensureInit();\n\n const isPartial = opts?.paths && opts.paths.length > 0;\n\n let targetRevision: number;\n let targetBranch: string | null = null;\n\n if (typeof target === \"string\") {\n // Check if it's a branch name\n const branchRef = await this.storage.readBranch(target);\n if (branchRef) {\n targetBranch = target;\n targetRevision = branchRef.revision;\n } else {\n throw new Error(`unknown branch or revision: \"${target}\"`);\n }\n } else {\n targetRevision = target;\n }\n\n // Verify revision exists\n let rev: Revision;\n try {\n rev = await this.storage.readRevision(targetRevision);\n } catch {\n throw new Error(`revision ${targetRevision} not found`);\n }\n\n if (isPartial) {\n // Partial checkout: restore specific files, don't update HEAD\n const matchedPaths = filterPathsByGlobs(Object.keys(rev.tree), opts!.paths!);\n const filteredManifest: TreeManifest = {};\n for (const p of matchedPaths) {\n filteredManifest[p] = rev.tree[p]!;\n }\n await restoreTree(this.workFs, this.workPath, filteredManifest, {\n fullRestore: false,\n paths: matchedPaths,\n });\n } else {\n // Full checkout\n if (!opts?.force) {\n const changes = await this.status();\n if (changes.length > 0) {\n throw new Error(\"working tree has uncommitted changes (use force to discard)\");\n }\n }\n\n await restoreTree(this.workFs, this.workPath, rev.tree, { fullRestore: true });\n\n // Update HEAD\n if (targetBranch) {\n await this.storage.writeHead({ ref: `refs/heads/${targetBranch}` });\n } else {\n await this.storage.writeHead({ revision: targetRevision });\n }\n }\n }\n\n /** Create a new branch at HEAD. */\n async branch(name: string): Promise<void> {\n await this.ensureInit();\n\n const existing = await this.storage.readBranch(name);\n if (existing) {\n throw new Error(`branch \"${name}\" already exists`);\n }\n\n const { revision } = await this.resolveHead();\n if (revision === null) {\n throw new Error(\"cannot create branch: no commits yet\");\n }\n\n await this.storage.writeBranch(name, { revision });\n }\n\n /** List all branches. */\n async branches(): Promise<BranchInfo[]> {\n await this.ensureInit();\n\n const names = await this.storage.listBranches();\n const head = await this.resolveHead();\n const result: BranchInfo[] = [];\n\n for (const name of names) {\n const ref = await this.storage.readBranch(name);\n if (ref) {\n result.push({\n name,\n revision: ref.revision,\n current: head.branch === name,\n });\n }\n }\n\n return result.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n /** Get revision history. */\n async log(opts?: LogOptions): Promise<LogEntry[]> {\n await this.ensureInit();\n\n let startRevision: number | null;\n\n if (opts?.branch) {\n const branchRef = await this.storage.readBranch(opts.branch);\n if (!branchRef) throw new Error(`branch \"${opts.branch}\" not found`);\n startRevision = branchRef.revision;\n } else {\n const { revision } = await this.resolveHead();\n startRevision = revision;\n }\n\n if (startRevision === null) return [];\n\n const entries: LogEntry[] = [];\n let currentId: number | null = startRevision;\n\n while (currentId !== null) {\n if (opts?.limit && entries.length >= opts.limit) break;\n\n let rev: Revision;\n try {\n rev = await this.storage.readRevision(currentId);\n } catch {\n break;\n }\n\n const changedPaths = rev.changes.map((c) => c.path);\n\n if (opts?.path) {\n // Filter: only include if this revision touches the specified path\n const matchesPath = changedPaths.some((p) => matchGlobPath(opts.path!, p));\n if (matchesPath) {\n entries.push({\n id: rev.id,\n parent: rev.parent,\n branch: rev.branch,\n message: rev.message,\n timestamp: rev.timestamp,\n paths: changedPaths,\n });\n }\n } else {\n entries.push({\n id: rev.id,\n parent: rev.parent,\n branch: rev.branch,\n message: rev.message,\n timestamp: rev.timestamp,\n paths: changedPaths,\n });\n }\n\n currentId = rev.parent;\n }\n\n return entries;\n }\n\n /** Get uncommitted changes as DiffEntry[]. */\n async status(): Promise<DiffEntry[]> {\n await this.ensureInit();\n const manifest = await this.headManifest();\n return diffWorkingTree(this.workFs, this.workPath, manifest, this.excludeDirs);\n }\n\n /** Diff between two revisions. */\n async diff(revA: number, revB: number): Promise<DiffEntry[]> {\n await this.ensureInit();\n const a = await this.storage.readRevision(revA);\n const b = await this.storage.readRevision(revB);\n return diffManifests(a.tree, b.tree);\n }\n\n /** Get current HEAD info. */\n async head(): Promise<{ branch: string | null; revision: number | null }> {\n await this.ensureInit();\n return this.resolveHead();\n }\n}\n\n/**\n * Filter a list of paths to only those matching any of the given glob patterns.\n * Patterns may start with `/` which is stripped before matching.\n */\nfunction filterPathsByGlobs(paths: string[], patterns: string[]): string[] {\n const normalizedPatterns = patterns.map((p) => (p.startsWith(\"/\") ? p.slice(1) : p));\n return paths.filter((filePath) =>\n normalizedPatterns.some((pattern) => matchGlobPath(pattern, filePath)),\n );\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "09DEFDF82D3AF06864756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/vcs/walk.ts
|
|
2
|
+
async function walkTree(fs, root, exclude = []) {
|
|
3
|
+
const results = [];
|
|
4
|
+
await walkDir(fs, root, root, exclude, results);
|
|
5
|
+
return results.sort();
|
|
6
|
+
}
|
|
7
|
+
async function walkDir(fs, base, dir, exclude, results) {
|
|
8
|
+
let entries;
|
|
9
|
+
try {
|
|
10
|
+
entries = await fs.readdir(dir);
|
|
11
|
+
} catch {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
if (exclude.includes(entry))
|
|
16
|
+
continue;
|
|
17
|
+
const fullPath = fs.resolve(dir, entry);
|
|
18
|
+
let stat;
|
|
19
|
+
try {
|
|
20
|
+
stat = await fs.stat(fullPath);
|
|
21
|
+
} catch {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (stat.isDirectory()) {
|
|
25
|
+
await walkDir(fs, base, fullPath, exclude, results);
|
|
26
|
+
} else if (stat.isFile()) {
|
|
27
|
+
const relative = relativePath(base, fullPath);
|
|
28
|
+
results.push(relative);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function relativePath(base, full) {
|
|
33
|
+
const normalizedBase = base.endsWith("/") ? base : base + "/";
|
|
34
|
+
if (full.startsWith(normalizedBase)) {
|
|
35
|
+
return full.slice(normalizedBase.length);
|
|
36
|
+
}
|
|
37
|
+
return full;
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
walkTree
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
//# debugId=BC9373798114D03264756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/vcs/walk.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type { VirtualFS } from \"../types.mjs\";\n\n/**\n * Recursively walk a directory tree and return all file paths\n * relative to the given root. Excludes directories whose names\n * match the exclude list.\n */\nexport async function walkTree(\n fs: VirtualFS,\n root: string,\n exclude: string[] = [],\n): Promise<string[]> {\n const results: string[] = [];\n await walkDir(fs, root, root, exclude, results);\n return results.sort();\n}\n\nasync function walkDir(\n fs: VirtualFS,\n base: string,\n dir: string,\n exclude: string[],\n results: string[],\n): Promise<void> {\n let entries: string[];\n try {\n entries = await fs.readdir(dir);\n } catch {\n return;\n }\n\n for (const entry of entries) {\n if (exclude.includes(entry)) continue;\n\n const fullPath = fs.resolve(dir, entry);\n let stat;\n try {\n stat = await fs.stat(fullPath);\n } catch {\n continue;\n }\n\n if (stat.isDirectory()) {\n await walkDir(fs, base, fullPath, exclude, results);\n } else if (stat.isFile()) {\n // Compute relative path from base\n const relative = relativePath(base, fullPath);\n results.push(relative);\n }\n }\n}\n\nfunction relativePath(base: string, full: string): string {\n const normalizedBase = base.endsWith(\"/\") ? base : base + \"/\";\n if (full.startsWith(normalizedBase)) {\n return full.slice(normalizedBase.length);\n }\n return full;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAOA,eAAsB,QAAQ,CAC5B,IACA,MACA,UAAoB,CAAC,GACF;AAAA,EACnB,MAAM,UAAoB,CAAC;AAAA,EAC3B,MAAM,QAAQ,IAAI,MAAM,MAAM,SAAS,OAAO;AAAA,EAC9C,OAAO,QAAQ,KAAK;AAAA;AAGtB,eAAe,OAAO,CACpB,IACA,MACA,KACA,SACA,SACe;AAAA,EACf,IAAI;AAAA,EACJ,IAAI;AAAA,IACF,UAAU,MAAM,GAAG,QAAQ,GAAG;AAAA,IAC9B,MAAM;AAAA,IACN;AAAA;AAAA,EAGF,WAAW,SAAS,SAAS;AAAA,IAC3B,IAAI,QAAQ,SAAS,KAAK;AAAA,MAAG;AAAA,IAE7B,MAAM,WAAW,GAAG,QAAQ,KAAK,KAAK;AAAA,IACtC,IAAI;AAAA,IACJ,IAAI;AAAA,MACF,OAAO,MAAM,GAAG,KAAK,QAAQ;AAAA,MAC7B,MAAM;AAAA,MACN;AAAA;AAAA,IAGF,IAAI,KAAK,YAAY,GAAG;AAAA,MACtB,MAAM,QAAQ,IAAI,MAAM,UAAU,SAAS,OAAO;AAAA,IACpD,EAAO,SAAI,KAAK,OAAO,GAAG;AAAA,MAExB,MAAM,WAAW,aAAa,MAAM,QAAQ;AAAA,MAC5C,QAAQ,KAAK,QAAQ;AAAA,IACvB;AAAA,EACF;AAAA;AAGF,SAAS,YAAY,CAAC,MAAc,MAAsB;AAAA,EACxD,MAAM,iBAAiB,KAAK,SAAS,GAAG,IAAI,OAAO,OAAO;AAAA,EAC1D,IAAI,KAAK,WAAW,cAAc,GAAG;AAAA,IACnC,OAAO,KAAK,MAAM,eAAe,MAAM;AAAA,EACzC;AAAA,EACA,OAAO;AAAA;",
|
|
8
|
+
"debugId": "BC9373798114D03264756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FileStat } from "../types.ts";
|
|
2
|
+
export declare const DEV_NULL_PATH = "/dev/null";
|
|
3
|
+
export declare function isDevNullPath(path: string): boolean;
|
|
4
|
+
export declare function readSpecialFile(path: string, encoding?: BufferEncoding): Buffer | string | undefined;
|
|
5
|
+
export declare function statSpecialFile(path: string): FileStat | undefined;
|
|
6
|
+
export declare function existsSpecialFile(path: string): boolean | undefined;
|
|
7
|
+
export declare function discardsSpecialFileWrites(path: string): boolean;
|
|
8
|
+
export declare function getSpecialPathError(path: string, operation: "mkdir" | "readdir" | "rm"): Error | undefined;
|
|
@@ -15,3 +15,5 @@ export { createStdin, StdinImpl } from "./io/index.ts";
|
|
|
15
15
|
export { createStdout, createStderr, createPipe, OutputCollectorImpl, PipeBuffer } from "./io/index.ts";
|
|
16
16
|
export { escape, escapeForInterpolation, globVirtualFS } from "./utils/index.ts";
|
|
17
17
|
export type { GlobVirtualFS, GlobOptions } from "./utils/index.ts";
|
|
18
|
+
export { VersionControlSystem } from "./vcs/index.ts";
|
|
19
|
+
export type { VCSConfig, Revision, DiffEntry, TreeManifest, FileEntry, CommitOptions, CheckoutOptions, LogOptions, LogEntry, BranchInfo, } from "./vcs/index.ts";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { VirtualFS } from "../types.ts";
|
|
2
|
+
import type { TreeManifest, DiffEntry } from "./types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Compute diff entries between two tree manifests.
|
|
5
|
+
*/
|
|
6
|
+
export declare function diffManifests(before: TreeManifest, after: TreeManifest): DiffEntry[];
|
|
7
|
+
/**
|
|
8
|
+
* Compute diff between a tree manifest and the current working tree.
|
|
9
|
+
*/
|
|
10
|
+
export declare function diffWorkingTree(fs: VirtualFS, rootPath: string, manifest: TreeManifest, exclude?: string[]): Promise<DiffEntry[]>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { VirtualFS } from "../types.ts";
|
|
2
|
+
import type { TreeManifest } from "./types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Build a TreeManifest from the current working tree.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildTreeManifest(fs: VirtualFS, rootPath: string, exclude?: string[]): Promise<TreeManifest>;
|
|
7
|
+
/**
|
|
8
|
+
* Build a TreeManifest for only the specified relative paths.
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildPartialManifest(fs: VirtualFS, rootPath: string, paths: string[]): Promise<TreeManifest>;
|
|
11
|
+
/**
|
|
12
|
+
* Restore a working tree from a TreeManifest.
|
|
13
|
+
*
|
|
14
|
+
* If `fullRestore` is true, deletes working tree files not in the manifest.
|
|
15
|
+
* If `paths` is provided, only restores matching files.
|
|
16
|
+
*/
|
|
17
|
+
export declare function restoreTree(fs: VirtualFS, rootPath: string, manifest: TreeManifest, options?: {
|
|
18
|
+
fullRestore?: boolean;
|
|
19
|
+
paths?: string[];
|
|
20
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { VirtualFS } from "../types.ts";
|
|
2
|
+
import type { HeadRef, BranchRef, VCSConfigFile, Revision } from "./types.ts";
|
|
3
|
+
export declare class VCSStorage {
|
|
4
|
+
private readonly fs;
|
|
5
|
+
private readonly basePath;
|
|
6
|
+
constructor(fs: VirtualFS, basePath: string);
|
|
7
|
+
private path;
|
|
8
|
+
isInitialized(): Promise<boolean>;
|
|
9
|
+
initialize(defaultBranch?: string): Promise<void>;
|
|
10
|
+
readHead(): Promise<HeadRef>;
|
|
11
|
+
writeHead(head: HeadRef): Promise<void>;
|
|
12
|
+
readBranch(name: string): Promise<BranchRef | null>;
|
|
13
|
+
writeBranch(name: string, ref: BranchRef): Promise<void>;
|
|
14
|
+
deleteBranch(name: string): Promise<void>;
|
|
15
|
+
listBranches(): Promise<string[]>;
|
|
16
|
+
readRevision(id: number): Promise<Revision>;
|
|
17
|
+
writeRevision(rev: Revision): Promise<void>;
|
|
18
|
+
nextRevisionId(): Promise<number>;
|
|
19
|
+
readConfig(): Promise<VCSConfigFile>;
|
|
20
|
+
private readJSON;
|
|
21
|
+
private writeJSON;
|
|
22
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { VirtualFS } from "../types.ts";
|
|
2
|
+
export interface VCSConfig {
|
|
3
|
+
/** The working tree filesystem */
|
|
4
|
+
fs: VirtualFS;
|
|
5
|
+
/** Root path of the working tree */
|
|
6
|
+
path: string;
|
|
7
|
+
/** Optional separate storage for VCS metadata */
|
|
8
|
+
vcsPath?: {
|
|
9
|
+
/** Filesystem for metadata storage (defaults to config.fs) */
|
|
10
|
+
fs?: VirtualFS;
|
|
11
|
+
/** Path for metadata storage (defaults to `${config.path}/.vcs`) */
|
|
12
|
+
path?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface HeadRef {
|
|
16
|
+
ref?: string;
|
|
17
|
+
revision?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface BranchRef {
|
|
20
|
+
revision: number;
|
|
21
|
+
}
|
|
22
|
+
export interface RevisionCounter {
|
|
23
|
+
next: number;
|
|
24
|
+
}
|
|
25
|
+
export interface VCSConfigFile {
|
|
26
|
+
defaultBranch: string;
|
|
27
|
+
}
|
|
28
|
+
export interface FileEntry {
|
|
29
|
+
content: string;
|
|
30
|
+
size: number;
|
|
31
|
+
}
|
|
32
|
+
export interface TreeManifest {
|
|
33
|
+
[path: string]: FileEntry;
|
|
34
|
+
}
|
|
35
|
+
export interface DiffEntry {
|
|
36
|
+
type: "add" | "modify" | "delete";
|
|
37
|
+
path: string;
|
|
38
|
+
content?: string;
|
|
39
|
+
previousContent?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface Revision {
|
|
42
|
+
id: number;
|
|
43
|
+
parent: number | null;
|
|
44
|
+
branch: string;
|
|
45
|
+
message: string;
|
|
46
|
+
timestamp: string;
|
|
47
|
+
changes: DiffEntry[];
|
|
48
|
+
tree: TreeManifest;
|
|
49
|
+
}
|
|
50
|
+
export interface CommitOptions {
|
|
51
|
+
paths?: string[];
|
|
52
|
+
}
|
|
53
|
+
export interface CheckoutOptions {
|
|
54
|
+
force?: boolean;
|
|
55
|
+
paths?: string[];
|
|
56
|
+
}
|
|
57
|
+
export interface LogOptions {
|
|
58
|
+
path?: string;
|
|
59
|
+
branch?: string;
|
|
60
|
+
limit?: number;
|
|
61
|
+
}
|
|
62
|
+
export interface LogEntry {
|
|
63
|
+
id: number;
|
|
64
|
+
parent: number | null;
|
|
65
|
+
branch: string;
|
|
66
|
+
message: string;
|
|
67
|
+
timestamp: string;
|
|
68
|
+
paths: string[];
|
|
69
|
+
}
|
|
70
|
+
export interface BranchInfo {
|
|
71
|
+
name: string;
|
|
72
|
+
revision: number;
|
|
73
|
+
current: boolean;
|
|
74
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { VCSConfig, Revision, DiffEntry, CommitOptions, CheckoutOptions, LogOptions, LogEntry, BranchInfo } from "./types.ts";
|
|
2
|
+
export declare class VersionControlSystem {
|
|
3
|
+
private readonly workFs;
|
|
4
|
+
private readonly workPath;
|
|
5
|
+
private readonly storage;
|
|
6
|
+
private readonly vcsDirName;
|
|
7
|
+
constructor(config: VCSConfig);
|
|
8
|
+
private get excludeDirs();
|
|
9
|
+
/** Initialize the .vcs directory. Called automatically on first operation if needed. */
|
|
10
|
+
init(): Promise<void>;
|
|
11
|
+
private ensureInit;
|
|
12
|
+
/** Get the current HEAD revision number, or null if no commits yet. */
|
|
13
|
+
private resolveHead;
|
|
14
|
+
/** Get current HEAD manifest, or empty if no commits. */
|
|
15
|
+
private headManifest;
|
|
16
|
+
/** Commit all pending changes, or selective changes if paths are provided. */
|
|
17
|
+
commit(message: string, opts?: CommitOptions): Promise<Revision>;
|
|
18
|
+
/** Checkout a revision number or branch name. */
|
|
19
|
+
checkout(target: string | number, opts?: CheckoutOptions): Promise<void>;
|
|
20
|
+
/** Create a new branch at HEAD. */
|
|
21
|
+
branch(name: string): Promise<void>;
|
|
22
|
+
/** List all branches. */
|
|
23
|
+
branches(): Promise<BranchInfo[]>;
|
|
24
|
+
/** Get revision history. */
|
|
25
|
+
log(opts?: LogOptions): Promise<LogEntry[]>;
|
|
26
|
+
/** Get uncommitted changes as DiffEntry[]. */
|
|
27
|
+
status(): Promise<DiffEntry[]>;
|
|
28
|
+
/** Diff between two revisions. */
|
|
29
|
+
diff(revA: number, revB: number): Promise<DiffEntry[]>;
|
|
30
|
+
/** Get current HEAD info. */
|
|
31
|
+
head(): Promise<{
|
|
32
|
+
branch: string | null;
|
|
33
|
+
revision: number | null;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { VirtualFS } from "../types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Recursively walk a directory tree and return all file paths
|
|
4
|
+
* relative to the given root. Excludes directories whose names
|
|
5
|
+
* match the exclude list.
|
|
6
|
+
*/
|
|
7
|
+
export declare function walkTree(fs: VirtualFS, root: string, exclude?: string[]): Promise<string[]>;
|
package/package.json
CHANGED