trellis 2.0.13 → 2.1.2
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/index.js +1 -1
- package/dist/embeddings/index.js +1 -1
- package/dist/{index-7gvjxt27.js → index-2917tjd8.js} +1 -1
- package/package.json +2 -10
- package/dist/transformers.node-bx3q9d7k.js +0 -33130
- package/src/cli/index.ts +0 -3356
- package/src/core/agents/harness.ts +0 -380
- package/src/core/agents/index.ts +0 -18
- package/src/core/agents/types.ts +0 -90
- package/src/core/index.ts +0 -118
- package/src/core/kernel/middleware.ts +0 -44
- package/src/core/kernel/trellis-kernel.ts +0 -593
- package/src/core/ontology/builtins.ts +0 -248
- package/src/core/ontology/index.ts +0 -34
- package/src/core/ontology/registry.ts +0 -209
- package/src/core/ontology/types.ts +0 -124
- package/src/core/ontology/validator.ts +0 -382
- package/src/core/persist/backend.ts +0 -74
- package/src/core/persist/sqlite-backend.ts +0 -298
- package/src/core/plugins/index.ts +0 -17
- package/src/core/plugins/registry.ts +0 -322
- package/src/core/plugins/types.ts +0 -126
- package/src/core/query/datalog.ts +0 -188
- package/src/core/query/engine.ts +0 -370
- package/src/core/query/index.ts +0 -34
- package/src/core/query/parser.ts +0 -481
- package/src/core/query/types.ts +0 -200
- package/src/core/store/eav-store.ts +0 -467
- package/src/decisions/auto-capture.ts +0 -136
- package/src/decisions/hooks.ts +0 -163
- package/src/decisions/index.ts +0 -261
- package/src/decisions/types.ts +0 -103
- package/src/embeddings/auto-embed.ts +0 -248
- package/src/embeddings/chunker.ts +0 -327
- package/src/embeddings/index.ts +0 -48
- package/src/embeddings/model.ts +0 -112
- package/src/embeddings/search.ts +0 -305
- package/src/embeddings/store.ts +0 -313
- package/src/embeddings/types.ts +0 -92
- package/src/engine.ts +0 -1125
- package/src/garden/cluster.ts +0 -330
- package/src/garden/garden.ts +0 -306
- package/src/garden/index.ts +0 -29
- package/src/git/git-exporter.ts +0 -286
- package/src/git/git-importer.ts +0 -329
- package/src/git/git-reader.ts +0 -189
- package/src/git/index.ts +0 -22
- package/src/identity/governance.ts +0 -211
- package/src/identity/identity.ts +0 -224
- package/src/identity/index.ts +0 -30
- package/src/identity/signing-middleware.ts +0 -97
- package/src/index.ts +0 -29
- package/src/links/index.ts +0 -49
- package/src/links/lifecycle.ts +0 -400
- package/src/links/parser.ts +0 -484
- package/src/links/ref-index.ts +0 -186
- package/src/links/resolver.ts +0 -314
- package/src/links/types.ts +0 -108
- package/src/mcp/index.ts +0 -22
- package/src/mcp/server.ts +0 -1278
- package/src/semantic/csharp-parser.ts +0 -493
- package/src/semantic/go-parser.ts +0 -585
- package/src/semantic/index.ts +0 -34
- package/src/semantic/java-parser.ts +0 -456
- package/src/semantic/python-parser.ts +0 -659
- package/src/semantic/ruby-parser.ts +0 -446
- package/src/semantic/rust-parser.ts +0 -784
- package/src/semantic/semantic-merge.ts +0 -210
- package/src/semantic/ts-parser.ts +0 -681
- package/src/semantic/types.ts +0 -175
- package/src/sync/http-transport.ts +0 -144
- package/src/sync/index.ts +0 -43
- package/src/sync/memory-transport.ts +0 -66
- package/src/sync/multi-repo.ts +0 -200
- package/src/sync/reconciler.ts +0 -237
- package/src/sync/sync-engine.ts +0 -258
- package/src/sync/types.ts +0 -104
- package/src/sync/ws-transport.ts +0 -145
- package/src/ui/client.html +0 -695
- package/src/ui/server.ts +0 -419
- package/src/vcs/blob-store.ts +0 -124
- package/src/vcs/branch.ts +0 -150
- package/src/vcs/checkpoint.ts +0 -64
- package/src/vcs/decompose.ts +0 -469
- package/src/vcs/diff.ts +0 -409
- package/src/vcs/engine-context.ts +0 -26
- package/src/vcs/index.ts +0 -23
- package/src/vcs/issue.ts +0 -800
- package/src/vcs/merge.ts +0 -425
- package/src/vcs/milestone.ts +0 -124
- package/src/vcs/ops.ts +0 -59
- package/src/vcs/types.ts +0 -213
- package/src/vcs/vcs-middleware.ts +0 -81
- package/src/watcher/fs-watcher.ts +0 -255
- package/src/watcher/index.ts +0 -9
- package/src/watcher/ingestion.ts +0 -116
package/src/ui/server.ts
DELETED
|
@@ -1,419 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Trellis UI Server
|
|
3
|
-
*
|
|
4
|
-
* Lightweight Bun HTTP server that exposes the TrellisVCS engine
|
|
5
|
-
* as a JSON API and serves a single-file force-graph visualization.
|
|
6
|
-
*
|
|
7
|
-
* Endpoints:
|
|
8
|
-
* GET / → client.html
|
|
9
|
-
* GET /api/graph → full graph (nodes + edges)
|
|
10
|
-
* GET /api/search → semantic search (?q=...&limit=10&type=...)
|
|
11
|
-
* GET /api/node/:id → node detail
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { readFileSync, existsSync } from 'fs';
|
|
15
|
-
import { join, dirname } from 'path';
|
|
16
|
-
import { TrellisVcsEngine } from '../engine.js';
|
|
17
|
-
import {
|
|
18
|
-
buildRefIndex,
|
|
19
|
-
createResolverContext,
|
|
20
|
-
getOutgoingRefs,
|
|
21
|
-
getReferencedEntities,
|
|
22
|
-
getBacklinks,
|
|
23
|
-
} from '../links/index.js';
|
|
24
|
-
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// Types
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
export interface GraphNode {
|
|
30
|
-
id: string;
|
|
31
|
-
label: string;
|
|
32
|
-
type: 'file' | 'milestone' | 'issue' | 'branch';
|
|
33
|
-
meta: Record<string, unknown>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface GraphEdge {
|
|
37
|
-
source: string;
|
|
38
|
-
target: string;
|
|
39
|
-
type: 'milestone_file' | 'issue_branch' | 'wikilink' | 'causal';
|
|
40
|
-
label?: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface GraphData {
|
|
44
|
-
nodes: GraphNode[];
|
|
45
|
-
edges: GraphEdge[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
// Graph builder
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
|
|
52
|
-
function buildGraph(engine: TrellisVcsEngine): GraphData {
|
|
53
|
-
const nodes: GraphNode[] = [];
|
|
54
|
-
const edges: GraphEdge[] = [];
|
|
55
|
-
const nodeIds = new Set<string>();
|
|
56
|
-
|
|
57
|
-
// --- Files ---
|
|
58
|
-
const files = engine.trackedFiles();
|
|
59
|
-
for (const f of files) {
|
|
60
|
-
const id = `file:${f.path}`;
|
|
61
|
-
nodes.push({
|
|
62
|
-
id,
|
|
63
|
-
label: f.path,
|
|
64
|
-
type: 'file',
|
|
65
|
-
meta: { contentHash: f.contentHash },
|
|
66
|
-
});
|
|
67
|
-
nodeIds.add(id);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// --- Milestones ---
|
|
71
|
-
const milestones = engine.listMilestones();
|
|
72
|
-
for (const m of milestones) {
|
|
73
|
-
const id = `milestone:${m.id}`;
|
|
74
|
-
nodes.push({
|
|
75
|
-
id,
|
|
76
|
-
label: m.message ?? m.id,
|
|
77
|
-
type: 'milestone',
|
|
78
|
-
meta: {
|
|
79
|
-
createdAt: m.createdAt,
|
|
80
|
-
affectedFiles: m.affectedFiles,
|
|
81
|
-
fromOpHash: m.fromOpHash,
|
|
82
|
-
toOpHash: m.toOpHash,
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
nodeIds.add(id);
|
|
86
|
-
|
|
87
|
-
// Edges: milestone → affected files
|
|
88
|
-
for (const fp of m.affectedFiles ?? []) {
|
|
89
|
-
const fileId = `file:${fp}`;
|
|
90
|
-
if (nodeIds.has(fileId)) {
|
|
91
|
-
edges.push({
|
|
92
|
-
source: id,
|
|
93
|
-
target: fileId,
|
|
94
|
-
type: 'milestone_file',
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// --- Issues ---
|
|
101
|
-
const issues = engine.listIssues();
|
|
102
|
-
for (const iss of issues) {
|
|
103
|
-
const id = `issue:${iss.id}`;
|
|
104
|
-
nodes.push({
|
|
105
|
-
id,
|
|
106
|
-
label: iss.title ?? iss.id,
|
|
107
|
-
type: 'issue',
|
|
108
|
-
meta: {
|
|
109
|
-
status: iss.status,
|
|
110
|
-
priority: iss.priority,
|
|
111
|
-
labels: iss.labels,
|
|
112
|
-
assignee: iss.assignee,
|
|
113
|
-
createdAt: iss.createdAt,
|
|
114
|
-
description: iss.description,
|
|
115
|
-
criteria: iss.criteria,
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
nodeIds.add(id);
|
|
119
|
-
|
|
120
|
-
// Edge: issue → its branch
|
|
121
|
-
if (iss.branchName) {
|
|
122
|
-
const branchId = `branch:${iss.branchName}`;
|
|
123
|
-
if (nodeIds.has(branchId)) {
|
|
124
|
-
edges.push({
|
|
125
|
-
source: id,
|
|
126
|
-
target: branchId,
|
|
127
|
-
type: 'issue_branch',
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// --- Branches ---
|
|
134
|
-
const branches = engine.listBranches();
|
|
135
|
-
for (const b of branches) {
|
|
136
|
-
const id = `branch:${b.name}`;
|
|
137
|
-
if (!nodeIds.has(id)) {
|
|
138
|
-
nodes.push({
|
|
139
|
-
id,
|
|
140
|
-
label: b.name,
|
|
141
|
-
type: 'branch',
|
|
142
|
-
meta: {
|
|
143
|
-
isCurrent: b.isCurrent,
|
|
144
|
-
createdAt: b.createdAt,
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
nodeIds.add(id);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Link issues that reference this branch
|
|
151
|
-
for (const iss of issues) {
|
|
152
|
-
if (iss.branchName === b.name) {
|
|
153
|
-
edges.push({
|
|
154
|
-
source: `issue:${iss.id}`,
|
|
155
|
-
target: id,
|
|
156
|
-
type: 'issue_branch',
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// --- Wiki-links (if markdown files exist) ---
|
|
163
|
-
try {
|
|
164
|
-
const mdFiles: Array<{ path: string; content: string }> = [];
|
|
165
|
-
for (const f of files) {
|
|
166
|
-
if (f.path.endsWith('.md')) {
|
|
167
|
-
const absPath = join(engine.getRootPath(), f.path);
|
|
168
|
-
if (existsSync(absPath)) {
|
|
169
|
-
mdFiles.push({
|
|
170
|
-
path: f.path,
|
|
171
|
-
content: readFileSync(absPath, 'utf-8'),
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (mdFiles.length > 0) {
|
|
178
|
-
const ctx = createResolverContext({
|
|
179
|
-
trackedFiles: () => files,
|
|
180
|
-
listIssues: () => issues as any,
|
|
181
|
-
listMilestones: () => milestones as any,
|
|
182
|
-
} as any);
|
|
183
|
-
|
|
184
|
-
const refIndex = buildRefIndex(mdFiles, ctx);
|
|
185
|
-
|
|
186
|
-
for (const [filePath, refs] of refIndex.outgoing) {
|
|
187
|
-
const sourceId = `file:${filePath}`;
|
|
188
|
-
if (!nodeIds.has(sourceId)) continue;
|
|
189
|
-
for (const ref of refs) {
|
|
190
|
-
const targetId = `${ref.namespace}:${ref.target}`;
|
|
191
|
-
// Normalize to our node IDs
|
|
192
|
-
const candidateIds = [
|
|
193
|
-
targetId,
|
|
194
|
-
`file:${ref.target}`,
|
|
195
|
-
`issue:${ref.target}`,
|
|
196
|
-
`milestone:${ref.target}`,
|
|
197
|
-
];
|
|
198
|
-
for (const cid of candidateIds) {
|
|
199
|
-
if (nodeIds.has(cid) && cid !== sourceId) {
|
|
200
|
-
edges.push({
|
|
201
|
-
source: sourceId,
|
|
202
|
-
target: cid,
|
|
203
|
-
type: 'wikilink',
|
|
204
|
-
label: ref.target,
|
|
205
|
-
});
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
} catch {
|
|
213
|
-
// Wiki-link indexing is best-effort
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return { nodes, edges };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// ---------------------------------------------------------------------------
|
|
220
|
-
// Node detail
|
|
221
|
-
// ---------------------------------------------------------------------------
|
|
222
|
-
|
|
223
|
-
function getNodeDetail(
|
|
224
|
-
engine: TrellisVcsEngine,
|
|
225
|
-
nodeId: string,
|
|
226
|
-
): Record<string, unknown> | null {
|
|
227
|
-
const [type, ...rest] = nodeId.split(':');
|
|
228
|
-
const id = rest.join(':');
|
|
229
|
-
|
|
230
|
-
switch (type) {
|
|
231
|
-
case 'file': {
|
|
232
|
-
const files = engine.trackedFiles();
|
|
233
|
-
const file = files.find((f) => f.path === id);
|
|
234
|
-
if (!file) return null;
|
|
235
|
-
// Get recent ops for this file
|
|
236
|
-
const ops = engine.log({ filePath: id, limit: 10 });
|
|
237
|
-
return {
|
|
238
|
-
type: 'file',
|
|
239
|
-
path: id,
|
|
240
|
-
contentHash: file.contentHash,
|
|
241
|
-
recentOps: ops.map((o) => ({
|
|
242
|
-
kind: o.kind,
|
|
243
|
-
timestamp: o.timestamp,
|
|
244
|
-
hash: o.hash.slice(0, 24),
|
|
245
|
-
})),
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
case 'milestone': {
|
|
249
|
-
const milestones = engine.listMilestones();
|
|
250
|
-
const m = milestones.find((ms) => ms.id === id);
|
|
251
|
-
if (!m) return null;
|
|
252
|
-
return { type: 'milestone', ...m };
|
|
253
|
-
}
|
|
254
|
-
case 'issue': {
|
|
255
|
-
const issue = engine.getIssue(id);
|
|
256
|
-
if (!issue) return null;
|
|
257
|
-
return { type: 'issue', ...issue };
|
|
258
|
-
}
|
|
259
|
-
case 'branch': {
|
|
260
|
-
const branches = engine.listBranches();
|
|
261
|
-
const b = branches.find((br) => br.name === id);
|
|
262
|
-
if (!b) return null;
|
|
263
|
-
return { type: 'branch', ...b };
|
|
264
|
-
}
|
|
265
|
-
default:
|
|
266
|
-
return null;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// ---------------------------------------------------------------------------
|
|
271
|
-
// Server
|
|
272
|
-
// ---------------------------------------------------------------------------
|
|
273
|
-
|
|
274
|
-
export interface UIServerOptions {
|
|
275
|
-
rootPath: string;
|
|
276
|
-
port?: number;
|
|
277
|
-
open?: boolean;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
export async function startUIServer(opts: UIServerOptions): Promise<{
|
|
281
|
-
port: number;
|
|
282
|
-
stop: () => void;
|
|
283
|
-
}> {
|
|
284
|
-
const engine = new TrellisVcsEngine({ rootPath: opts.rootPath });
|
|
285
|
-
engine.open();
|
|
286
|
-
|
|
287
|
-
const clientHtml = readFileSync(
|
|
288
|
-
join(dirname(new URL(import.meta.url).pathname), 'client.html'),
|
|
289
|
-
'utf-8',
|
|
290
|
-
);
|
|
291
|
-
|
|
292
|
-
// Lazy-load embedding manager for search
|
|
293
|
-
let embeddingManager: any = null;
|
|
294
|
-
function getEmbeddingManager() {
|
|
295
|
-
if (!embeddingManager) {
|
|
296
|
-
try {
|
|
297
|
-
const { EmbeddingManager } = require('../embeddings/index.js');
|
|
298
|
-
const dbPath = join(opts.rootPath, '.trellis', 'embeddings.db');
|
|
299
|
-
if (existsSync(dbPath)) {
|
|
300
|
-
embeddingManager = new EmbeddingManager(dbPath);
|
|
301
|
-
}
|
|
302
|
-
} catch {
|
|
303
|
-
// Embeddings not available
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return embeddingManager;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const requestedPort = opts.port ?? 3333;
|
|
310
|
-
|
|
311
|
-
const server = Bun.serve({
|
|
312
|
-
port: requestedPort,
|
|
313
|
-
async fetch(req) {
|
|
314
|
-
const url = new URL(req.url);
|
|
315
|
-
const path = url.pathname;
|
|
316
|
-
|
|
317
|
-
// CORS headers
|
|
318
|
-
const headers = {
|
|
319
|
-
'Access-Control-Allow-Origin': '*',
|
|
320
|
-
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
|
321
|
-
'Access-Control-Allow-Headers': 'Content-Type',
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
if (req.method === 'OPTIONS') {
|
|
325
|
-
return new Response(null, { status: 204, headers });
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// --- API Routes ---
|
|
329
|
-
|
|
330
|
-
if (path === '/api/graph') {
|
|
331
|
-
const graph = buildGraph(engine);
|
|
332
|
-
return Response.json(graph, { headers });
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (path === '/api/search') {
|
|
336
|
-
const query = url.searchParams.get('q');
|
|
337
|
-
if (!query) {
|
|
338
|
-
return Response.json(
|
|
339
|
-
{ error: 'Missing ?q= parameter' },
|
|
340
|
-
{ status: 400, headers },
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
const limit = parseInt(url.searchParams.get('limit') ?? '10', 10);
|
|
344
|
-
const typeFilter = url.searchParams.get('type');
|
|
345
|
-
|
|
346
|
-
const mgr = getEmbeddingManager();
|
|
347
|
-
if (!mgr) {
|
|
348
|
-
return Response.json(
|
|
349
|
-
{
|
|
350
|
-
results: [],
|
|
351
|
-
message: 'No embedding index. Run `trellis reindex` first.',
|
|
352
|
-
},
|
|
353
|
-
{ headers },
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
try {
|
|
358
|
-
const searchOpts: any = { limit };
|
|
359
|
-
if (typeFilter) {
|
|
360
|
-
searchOpts.types = typeFilter
|
|
361
|
-
.split(',')
|
|
362
|
-
.map((t: string) => t.trim());
|
|
363
|
-
}
|
|
364
|
-
const results = await mgr.search(query, searchOpts);
|
|
365
|
-
return Response.json(
|
|
366
|
-
{
|
|
367
|
-
results: results.map((r: any) => ({
|
|
368
|
-
score: r.score,
|
|
369
|
-
chunkType: r.chunk.chunkType,
|
|
370
|
-
filePath: r.chunk.filePath,
|
|
371
|
-
entityId: r.chunk.entityId,
|
|
372
|
-
content: r.chunk.content,
|
|
373
|
-
})),
|
|
374
|
-
},
|
|
375
|
-
{ headers },
|
|
376
|
-
);
|
|
377
|
-
} catch (err: any) {
|
|
378
|
-
return Response.json(
|
|
379
|
-
{ error: err.message },
|
|
380
|
-
{ status: 500, headers },
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (path.startsWith('/api/node/')) {
|
|
386
|
-
const nodeId = decodeURIComponent(path.slice('/api/node/'.length));
|
|
387
|
-
const detail = getNodeDetail(engine, nodeId);
|
|
388
|
-
if (!detail) {
|
|
389
|
-
return Response.json(
|
|
390
|
-
{ error: 'Node not found' },
|
|
391
|
-
{ status: 404, headers },
|
|
392
|
-
);
|
|
393
|
-
}
|
|
394
|
-
return Response.json(detail, { headers });
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// --- Static ---
|
|
398
|
-
if (path === '/' || path === '/index.html') {
|
|
399
|
-
return new Response(clientHtml, {
|
|
400
|
-
headers: { ...headers, 'Content-Type': 'text/html; charset=utf-8' },
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
return new Response('Not Found', { status: 404, headers });
|
|
405
|
-
},
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
return {
|
|
409
|
-
port: server.port as number,
|
|
410
|
-
stop: () => {
|
|
411
|
-
server.stop();
|
|
412
|
-
if (embeddingManager) {
|
|
413
|
-
try {
|
|
414
|
-
embeddingManager.close();
|
|
415
|
-
} catch {}
|
|
416
|
-
}
|
|
417
|
-
},
|
|
418
|
-
};
|
|
419
|
-
}
|
package/src/vcs/blob-store.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Content-Addressable Blob Store
|
|
3
|
-
*
|
|
4
|
-
* Stores file content indexed by SHA-256 hash. Provides the source of truth
|
|
5
|
-
* for file reconstruction at any point in history. The EAV graph stores
|
|
6
|
-
* structural metadata; the blob store stores byte-exact content.
|
|
7
|
-
*
|
|
8
|
-
* Storage format: `.trellis/blobs/{hash}` files on disk.
|
|
9
|
-
* Future: migrate to SQLite `blobs(hash TEXT PRIMARY KEY, content BLOB)`.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
13
|
-
import { join } from 'path';
|
|
14
|
-
|
|
15
|
-
export class BlobStore {
|
|
16
|
-
private blobDir: string;
|
|
17
|
-
|
|
18
|
-
constructor(trellisDir: string) {
|
|
19
|
-
this.blobDir = join(trellisDir, 'blobs');
|
|
20
|
-
if (!existsSync(this.blobDir)) {
|
|
21
|
-
mkdirSync(this.blobDir, { recursive: true });
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Store content and return its SHA-256 hash.
|
|
27
|
-
* Idempotent — storing the same content twice is a no-op.
|
|
28
|
-
*/
|
|
29
|
-
async put(content: Buffer | Uint8Array): Promise<string> {
|
|
30
|
-
const hash = await this.hash(content);
|
|
31
|
-
const blobPath = join(this.blobDir, hash);
|
|
32
|
-
if (!existsSync(blobPath)) {
|
|
33
|
-
writeFileSync(blobPath, content);
|
|
34
|
-
}
|
|
35
|
-
return hash;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Synchronous put — uses Bun's sync crypto if available.
|
|
40
|
-
*/
|
|
41
|
-
putSync(content: Buffer | Uint8Array): string {
|
|
42
|
-
const hash = this.hashSync(content);
|
|
43
|
-
const blobPath = join(this.blobDir, hash);
|
|
44
|
-
if (!existsSync(blobPath)) {
|
|
45
|
-
writeFileSync(blobPath, content);
|
|
46
|
-
}
|
|
47
|
-
return hash;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Retrieve content by hash. Returns null if not found.
|
|
52
|
-
*/
|
|
53
|
-
get(hash: string): Buffer | null {
|
|
54
|
-
const blobPath = join(this.blobDir, hash);
|
|
55
|
-
if (!existsSync(blobPath)) {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
return readFileSync(blobPath);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Check if a blob exists.
|
|
63
|
-
*/
|
|
64
|
-
has(hash: string): boolean {
|
|
65
|
-
return existsSync(join(this.blobDir, hash));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Compute SHA-256 hash of content (async).
|
|
70
|
-
*/
|
|
71
|
-
async hash(content: Buffer | Uint8Array): Promise<string> {
|
|
72
|
-
const hashBuffer = await crypto.subtle.digest(
|
|
73
|
-
'SHA-256',
|
|
74
|
-
content as unknown as ArrayBuffer,
|
|
75
|
-
);
|
|
76
|
-
return this.hexFromBuffer(hashBuffer);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Compute SHA-256 hash of content (sync, using Bun's CryptoHasher).
|
|
81
|
-
*/
|
|
82
|
-
hashSync(content: Buffer | Uint8Array): string {
|
|
83
|
-
const hasher = new Bun.CryptoHasher('sha256');
|
|
84
|
-
hasher.update(content);
|
|
85
|
-
return hasher.digest('hex');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Returns the number of blobs stored.
|
|
90
|
-
*/
|
|
91
|
-
count(): number {
|
|
92
|
-
try {
|
|
93
|
-
const { readdirSync } = require('fs');
|
|
94
|
-
return readdirSync(this.blobDir).length;
|
|
95
|
-
} catch {
|
|
96
|
-
return 0;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Returns the total size of all blobs in bytes.
|
|
102
|
-
*/
|
|
103
|
-
totalSize(): number {
|
|
104
|
-
try {
|
|
105
|
-
const { readdirSync, statSync } = require('fs');
|
|
106
|
-
const files: string[] = readdirSync(this.blobDir);
|
|
107
|
-
return files.reduce((sum: number, f: string) => {
|
|
108
|
-
try {
|
|
109
|
-
return sum + statSync(join(this.blobDir, f)).size;
|
|
110
|
-
} catch {
|
|
111
|
-
return sum;
|
|
112
|
-
}
|
|
113
|
-
}, 0);
|
|
114
|
-
} catch {
|
|
115
|
-
return 0;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private hexFromBuffer(buffer: ArrayBuffer): string {
|
|
120
|
-
return Array.from(new Uint8Array(buffer))
|
|
121
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
122
|
-
.join('');
|
|
123
|
-
}
|
|
124
|
-
}
|
package/src/vcs/branch.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Branch Management Module
|
|
3
|
-
*
|
|
4
|
-
* Extracted from engine.ts per DESIGN.md §8.1.
|
|
5
|
-
* Handles create, switch, list, delete branch operations.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
9
|
-
import { join } from 'path';
|
|
10
|
-
import { createVcsOp } from './ops.js';
|
|
11
|
-
import type { VcsOp } from './types.js';
|
|
12
|
-
import type { EngineContext } from './engine-context.js';
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Types
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
export interface BranchInfo {
|
|
19
|
-
name: string;
|
|
20
|
-
isCurrent: boolean;
|
|
21
|
-
createdAt?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface BranchState {
|
|
25
|
-
currentBranch: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
// Operations
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Create a new branch forked from the current branch.
|
|
34
|
-
*/
|
|
35
|
-
export async function createBranch(
|
|
36
|
-
ctx: EngineContext,
|
|
37
|
-
name: string,
|
|
38
|
-
currentBranch: string,
|
|
39
|
-
): Promise<VcsOp> {
|
|
40
|
-
const existing = ctx.store
|
|
41
|
-
.getFactsByAttribute('type')
|
|
42
|
-
.filter((f) => f.v === 'Branch' && f.e === `branch:${name}`);
|
|
43
|
-
if (existing.length > 0) {
|
|
44
|
-
throw new Error(`Branch '${name}' already exists`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const op = await createVcsOp('vcs:branchCreate', {
|
|
48
|
-
agentId: ctx.agentId,
|
|
49
|
-
previousHash: ctx.getLastOp()?.hash,
|
|
50
|
-
vcs: {
|
|
51
|
-
branchName: name,
|
|
52
|
-
baseBranch: currentBranch,
|
|
53
|
-
targetOpHash: ctx.getLastOp()?.hash,
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
ctx.applyOp(op);
|
|
57
|
-
return op;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Switch to an existing branch.
|
|
62
|
-
*/
|
|
63
|
-
export function switchBranch(
|
|
64
|
-
ctx: EngineContext,
|
|
65
|
-
name: string,
|
|
66
|
-
): void {
|
|
67
|
-
const branchFacts = ctx.store
|
|
68
|
-
.getFactsByEntity(`branch:${name}`)
|
|
69
|
-
.filter((f) => f.a === 'type' && f.v === 'Branch');
|
|
70
|
-
if (branchFacts.length === 0) {
|
|
71
|
-
throw new Error(`Branch '${name}' does not exist`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* List all branches.
|
|
77
|
-
*/
|
|
78
|
-
export function listBranches(
|
|
79
|
-
ctx: EngineContext,
|
|
80
|
-
currentBranch: string,
|
|
81
|
-
): BranchInfo[] {
|
|
82
|
-
const branchFacts = ctx.store
|
|
83
|
-
.getFactsByAttribute('type')
|
|
84
|
-
.filter((f) => f.v === 'Branch');
|
|
85
|
-
|
|
86
|
-
return branchFacts.map((f) => {
|
|
87
|
-
const nameFact = ctx.store
|
|
88
|
-
.getFactsByEntity(f.e)
|
|
89
|
-
.find((ef) => ef.a === 'name');
|
|
90
|
-
const createdFact = ctx.store
|
|
91
|
-
.getFactsByEntity(f.e)
|
|
92
|
-
.find((ef) => ef.a === 'createdAt');
|
|
93
|
-
const name = (nameFact?.v as string) ?? f.e.replace('branch:', '');
|
|
94
|
-
return {
|
|
95
|
-
name,
|
|
96
|
-
isCurrent: name === currentBranch,
|
|
97
|
-
createdAt: createdFact?.v as string | undefined,
|
|
98
|
-
};
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Delete a branch (cannot delete the current branch).
|
|
104
|
-
*/
|
|
105
|
-
export async function deleteBranch(
|
|
106
|
-
ctx: EngineContext,
|
|
107
|
-
name: string,
|
|
108
|
-
currentBranch: string,
|
|
109
|
-
): Promise<VcsOp> {
|
|
110
|
-
if (name === currentBranch) {
|
|
111
|
-
throw new Error(`Cannot delete the current branch '${name}'`);
|
|
112
|
-
}
|
|
113
|
-
const branchFacts = ctx.store
|
|
114
|
-
.getFactsByEntity(`branch:${name}`)
|
|
115
|
-
.filter((f) => f.a === 'type' && f.v === 'Branch');
|
|
116
|
-
if (branchFacts.length === 0) {
|
|
117
|
-
throw new Error(`Branch '${name}' does not exist`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const op = await createVcsOp('vcs:branchDelete', {
|
|
121
|
-
agentId: ctx.agentId,
|
|
122
|
-
previousHash: ctx.getLastOp()?.hash,
|
|
123
|
-
vcs: { branchName: name },
|
|
124
|
-
});
|
|
125
|
-
ctx.applyOp(op);
|
|
126
|
-
return op;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
// Persistence
|
|
131
|
-
// ---------------------------------------------------------------------------
|
|
132
|
-
|
|
133
|
-
export function saveBranchState(rootPath: string, state: BranchState): void {
|
|
134
|
-
const statePath = join(rootPath, '.trellis', 'state.json');
|
|
135
|
-
writeFileSync(statePath, JSON.stringify(state));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function loadBranchState(rootPath: string): BranchState {
|
|
139
|
-
const statePath = join(rootPath, '.trellis', 'state.json');
|
|
140
|
-
if (existsSync(statePath)) {
|
|
141
|
-
try {
|
|
142
|
-
const raw = readFileSync(statePath, 'utf-8');
|
|
143
|
-
const state = JSON.parse(raw);
|
|
144
|
-
if (state.currentBranch) {
|
|
145
|
-
return { currentBranch: state.currentBranch };
|
|
146
|
-
}
|
|
147
|
-
} catch {}
|
|
148
|
-
}
|
|
149
|
-
return { currentBranch: 'main' };
|
|
150
|
-
}
|