@variantree/watcher 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/__tests__/git-snapshot.test.d.ts +2 -0
- package/dist/__tests__/git-snapshot.test.d.ts.map +1 -0
- package/dist/__tests__/git-snapshot.test.js +437 -0
- package/dist/__tests__/git-snapshot.test.js.map +1 -0
- package/dist/__tests__/integration.test.d.ts +10 -0
- package/dist/__tests__/integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration.test.js +316 -0
- package/dist/__tests__/integration.test.js.map +1 -0
- package/dist/__tests__/watcher.test.d.ts +2 -0
- package/dist/__tests__/watcher.test.d.ts.map +1 -0
- package/dist/__tests__/watcher.test.js +375 -0
- package/dist/__tests__/watcher.test.js.map +1 -0
- package/dist/adapters/aider.d.ts +21 -0
- package/dist/adapters/aider.d.ts.map +1 -0
- package/dist/adapters/aider.js +42 -0
- package/dist/adapters/aider.js.map +1 -0
- package/dist/adapters/base.d.ts +3 -0
- package/dist/adapters/base.d.ts.map +1 -0
- package/dist/adapters/base.js +2 -0
- package/dist/adapters/base.js.map +1 -0
- package/dist/adapters/claude-code.d.ts +17 -0
- package/dist/adapters/claude-code.d.ts.map +1 -0
- package/dist/adapters/claude-code.js +58 -0
- package/dist/adapters/claude-code.js.map +1 -0
- package/dist/adapters/opencode.d.ts +3 -0
- package/dist/adapters/opencode.d.ts.map +1 -0
- package/dist/adapters/opencode.js +3 -0
- package/dist/adapters/opencode.js.map +1 -0
- package/dist/agents-md.d.ts +18 -0
- package/dist/agents-md.d.ts.map +1 -0
- package/dist/agents-md.js +93 -0
- package/dist/agents-md.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +511 -0
- package/dist/cli.js.map +1 -0
- package/dist/differ.d.ts +18 -0
- package/dist/differ.d.ts.map +1 -0
- package/dist/differ.js +23 -0
- package/dist/differ.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/node/blob-store.d.ts +16 -0
- package/dist/node/blob-store.d.ts.map +1 -0
- package/dist/node/blob-store.js +47 -0
- package/dist/node/blob-store.js.map +1 -0
- package/dist/node/fs-adapter.d.ts +17 -0
- package/dist/node/fs-adapter.d.ts.map +1 -0
- package/dist/node/fs-adapter.js +110 -0
- package/dist/node/fs-adapter.js.map +1 -0
- package/dist/node/git-snapshot.d.ts +46 -0
- package/dist/node/git-snapshot.d.ts.map +1 -0
- package/dist/node/git-snapshot.js +282 -0
- package/dist/node/git-snapshot.js.map +1 -0
- package/dist/node/session-launcher.d.ts +14 -0
- package/dist/node/session-launcher.d.ts.map +1 -0
- package/dist/node/session-launcher.js +28 -0
- package/dist/node/session-launcher.js.map +1 -0
- package/dist/node/storage.d.ts +17 -0
- package/dist/node/storage.d.ts.map +1 -0
- package/dist/node/storage.js +56 -0
- package/dist/node/storage.js.map +1 -0
- package/dist/sync.d.ts +16 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +58 -0
- package/dist/sync.js.map +1 -0
- package/dist/tools/base.d.ts +52 -0
- package/dist/tools/base.d.ts.map +1 -0
- package/dist/tools/base.js +10 -0
- package/dist/tools/base.js.map +1 -0
- package/dist/tools/claudecode/adapter.d.ts +26 -0
- package/dist/tools/claudecode/adapter.d.ts.map +1 -0
- package/dist/tools/claudecode/adapter.js +111 -0
- package/dist/tools/claudecode/adapter.js.map +1 -0
- package/dist/tools/claudecode/config.d.ts +8 -0
- package/dist/tools/claudecode/config.d.ts.map +1 -0
- package/dist/tools/claudecode/config.js +34 -0
- package/dist/tools/claudecode/config.js.map +1 -0
- package/dist/tools/claudecode/index.d.ts +9 -0
- package/dist/tools/claudecode/index.d.ts.map +1 -0
- package/dist/tools/claudecode/index.js +16 -0
- package/dist/tools/claudecode/index.js.map +1 -0
- package/dist/tools/claudecode/instructions.d.ts +7 -0
- package/dist/tools/claudecode/instructions.d.ts.map +1 -0
- package/dist/tools/claudecode/instructions.js +21 -0
- package/dist/tools/claudecode/instructions.js.map +1 -0
- package/dist/tools/index.d.ts +24 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +42 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/instructions.d.ts +19 -0
- package/dist/tools/instructions.d.ts.map +1 -0
- package/dist/tools/instructions.js +93 -0
- package/dist/tools/instructions.js.map +1 -0
- package/dist/tools/opencode/adapter.d.ts +46 -0
- package/dist/tools/opencode/adapter.d.ts.map +1 -0
- package/dist/tools/opencode/adapter.js +166 -0
- package/dist/tools/opencode/adapter.js.map +1 -0
- package/dist/tools/opencode/config.d.ts +8 -0
- package/dist/tools/opencode/config.d.ts.map +1 -0
- package/dist/tools/opencode/config.js +37 -0
- package/dist/tools/opencode/config.js.map +1 -0
- package/dist/tools/opencode/index.d.ts +9 -0
- package/dist/tools/opencode/index.d.ts.map +1 -0
- package/dist/tools/opencode/index.js +16 -0
- package/dist/tools/opencode/index.js.map +1 -0
- package/dist/tools/opencode/instructions.d.ts +10 -0
- package/dist/tools/opencode/instructions.d.ts.map +1 -0
- package/dist/tools/opencode/instructions.js +26 -0
- package/dist/tools/opencode/instructions.js.map +1 -0
- package/dist/watcher.d.ts +40 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +87 -0
- package/dist/watcher.js.map +1 -0
- package/package.json +46 -0
package/dist/differ.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @variantree/watcher — Message Differ
|
|
3
|
+
*
|
|
4
|
+
* Detects new messages between consecutive reads of a session file.
|
|
5
|
+
* Keeps a simple count per session file — only emits messages beyond
|
|
6
|
+
* what was seen last time.
|
|
7
|
+
*/
|
|
8
|
+
import type { Message } from '@variantree/core';
|
|
9
|
+
export declare class MessageDiffer {
|
|
10
|
+
private lastCount;
|
|
11
|
+
/**
|
|
12
|
+
* Given the current full list of messages, return only the ones
|
|
13
|
+
* that are new since the last call to diff().
|
|
14
|
+
*/
|
|
15
|
+
diff(messages: Message[]): Message[];
|
|
16
|
+
reset(): void;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=differ.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"differ.d.ts","sourceRoot":"","sources":["../src/differ.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,qBAAa,aAAa;IACxB,OAAO,CAAC,SAAS,CAAK;IAEtB;;;OAGG;IACH,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE;IAMpC,KAAK,IAAI,IAAI;CAGd"}
|
package/dist/differ.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @variantree/watcher — Message Differ
|
|
3
|
+
*
|
|
4
|
+
* Detects new messages between consecutive reads of a session file.
|
|
5
|
+
* Keeps a simple count per session file — only emits messages beyond
|
|
6
|
+
* what was seen last time.
|
|
7
|
+
*/
|
|
8
|
+
export class MessageDiffer {
|
|
9
|
+
lastCount = 0;
|
|
10
|
+
/**
|
|
11
|
+
* Given the current full list of messages, return only the ones
|
|
12
|
+
* that are new since the last call to diff().
|
|
13
|
+
*/
|
|
14
|
+
diff(messages) {
|
|
15
|
+
const newMessages = messages.slice(this.lastCount);
|
|
16
|
+
this.lastCount = messages.length;
|
|
17
|
+
return newMessages;
|
|
18
|
+
}
|
|
19
|
+
reset() {
|
|
20
|
+
this.lastCount = 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=differ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"differ.js","sourceRoot":"","sources":["../src/differ.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,OAAO,aAAa;IAChB,SAAS,GAAG,CAAC,CAAC;IAEtB;;;OAGG;IACH,IAAI,CAAC,QAAmB;QACtB,MAAM,WAAW,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACnD,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC;QACjC,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;IACrB,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @variantree/watcher — Public API
|
|
3
|
+
*/
|
|
4
|
+
export { VariantreeWatcher } from './watcher.js';
|
|
5
|
+
export type { WatcherOptions } from './watcher.js';
|
|
6
|
+
export { MessageDiffer } from './differ.js';
|
|
7
|
+
export { NodeStorage } from './node/storage.js';
|
|
8
|
+
export { GitSnapshotProvider } from './node/git-snapshot.js';
|
|
9
|
+
export { launchOpenCodeSession } from './node/session-launcher.js';
|
|
10
|
+
export type { LaunchResult } from './node/session-launcher.js';
|
|
11
|
+
export { ALL_TOOLS, ensureProjectInstructions, registerAllMcp, } from './tools/index.js';
|
|
12
|
+
export type { ToolIntegration, SessionAdapter } from './tools/index.js';
|
|
13
|
+
export { OpenCodeAdapter, mergeAgentsMd } from './tools/opencode/index.js';
|
|
14
|
+
export { ClaudeCodeAdapter } from './tools/claudecode/index.js';
|
|
15
|
+
export { syncConversation } from './sync.js';
|
|
16
|
+
export { VARIANTREE_MARKER, mergeInstructions } from './tools/index.js';
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGnD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAG7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,YAAY,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAG/D,OAAO,EACL,SAAS,EACT,yBAAyB,EACzB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAGxE,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAG3E,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAGhE,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAG7C,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @variantree/watcher — Public API
|
|
3
|
+
*/
|
|
4
|
+
// Core watcher
|
|
5
|
+
export { VariantreeWatcher } from './watcher.js';
|
|
6
|
+
// Message differ
|
|
7
|
+
export { MessageDiffer } from './differ.js';
|
|
8
|
+
// Node.js storage + snapshot
|
|
9
|
+
export { NodeStorage } from './node/storage.js';
|
|
10
|
+
export { GitSnapshotProvider } from './node/git-snapshot.js';
|
|
11
|
+
// Session launcher
|
|
12
|
+
export { launchOpenCodeSession } from './node/session-launcher.js';
|
|
13
|
+
// Tool integrations
|
|
14
|
+
export { ALL_TOOLS, ensureProjectInstructions, registerAllMcp, } from './tools/index.js';
|
|
15
|
+
// OpenCode (primary adapter — used by MCP server and CLI directly)
|
|
16
|
+
export { OpenCodeAdapter, mergeAgentsMd } from './tools/opencode/index.js';
|
|
17
|
+
// Claude Code
|
|
18
|
+
export { ClaudeCodeAdapter } from './tools/claudecode/index.js';
|
|
19
|
+
// Shared sync (adapter-agnostic — works with OpenCode, Claude Code, etc.)
|
|
20
|
+
export { syncConversation } from './sync.js';
|
|
21
|
+
// Backwards-compat re-export (old import path: adapters/opencode)
|
|
22
|
+
export { VARIANTREE_MARKER, mergeInstructions } from './tools/index.js';
|
|
23
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,eAAe;AACf,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAGjD,iBAAiB;AACjB,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,6BAA6B;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7D,mBAAmB;AACnB,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAGnE,oBAAoB;AACpB,OAAO,EACL,SAAS,EACT,yBAAyB,EACzB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAG1B,mEAAmE;AACnE,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE3E,cAAc;AACd,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAEhE,0EAA0E;AAC1E,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,kEAAkE;AAClE,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @variantree/watcher — Node.js BlobStore
|
|
3
|
+
*
|
|
4
|
+
* Stores content-addressed blobs in .variantree/blobs/<hash>
|
|
5
|
+
*/
|
|
6
|
+
import type { SnapshotStorage } from '@variantree/core';
|
|
7
|
+
export declare class NodeBlobStore implements SnapshotStorage {
|
|
8
|
+
private readonly blobsDir;
|
|
9
|
+
constructor(workspacePath: string);
|
|
10
|
+
private blobPath;
|
|
11
|
+
writeBlob(hash: string, content: Buffer | Uint8Array): Promise<void>;
|
|
12
|
+
readBlob(hash: string): Promise<Uint8Array | null>;
|
|
13
|
+
hasBlob(hash: string): Promise<boolean>;
|
|
14
|
+
deleteBlob(hash: string): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=blob-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blob-store.d.ts","sourceRoot":"","sources":["../../src/node/blob-store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,qBAAa,aAAc,YAAW,eAAe;IACnD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAEtB,aAAa,EAAE,MAAM;IAIjC,OAAO,CAAC,QAAQ;IAKV,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAMpE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IASlD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASvC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAK9C"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @variantree/watcher — Node.js BlobStore
|
|
3
|
+
*
|
|
4
|
+
* Stores content-addressed blobs in .variantree/blobs/<hash>
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
export class NodeBlobStore {
|
|
9
|
+
blobsDir;
|
|
10
|
+
constructor(workspacePath) {
|
|
11
|
+
this.blobsDir = path.join(workspacePath, '.variantree', 'blobs');
|
|
12
|
+
}
|
|
13
|
+
blobPath(hash) {
|
|
14
|
+
// Split into 2-char prefix dir for better filesystem performance
|
|
15
|
+
return path.join(this.blobsDir, hash.slice(0, 2), hash.slice(2));
|
|
16
|
+
}
|
|
17
|
+
async writeBlob(hash, content) {
|
|
18
|
+
const p = this.blobPath(hash);
|
|
19
|
+
await fs.mkdir(path.dirname(p), { recursive: true });
|
|
20
|
+
await fs.writeFile(p, content);
|
|
21
|
+
}
|
|
22
|
+
async readBlob(hash) {
|
|
23
|
+
try {
|
|
24
|
+
const buf = await fs.readFile(this.blobPath(hash));
|
|
25
|
+
return new Uint8Array(buf);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async hasBlob(hash) {
|
|
32
|
+
try {
|
|
33
|
+
await fs.access(this.blobPath(hash));
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async deleteBlob(hash) {
|
|
41
|
+
try {
|
|
42
|
+
await fs.unlink(this.blobPath(hash));
|
|
43
|
+
}
|
|
44
|
+
catch { /* already gone */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=blob-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blob-store.js","sourceRoot":"","sources":["../../src/node/blob-store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,MAAM,OAAO,aAAa;IACP,QAAQ,CAAS;IAElC,YAAY,aAAqB;QAC/B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,aAAa,EAAE,OAAO,CAAC,CAAC;IACnE,CAAC;IAEO,QAAQ,CAAC,IAAY;QAC3B,iEAAiE;QACjE,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,IAAY,EAAE,OAA4B;QACxD,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,MAAM,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACjC,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,IAAY;QACzB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;YACnD,OAAO,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,IAAY;QACxB,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;YACrC,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,IAAY;QAC3B,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;QACvC,CAAC;QAAC,MAAM,CAAC,CAAC,kBAAkB,CAAC,CAAC;IAChC,CAAC;CACF"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @variantree/watcher — Node.js FileSystemAdapter
|
|
3
|
+
*
|
|
4
|
+
* Implements FileSystemAdapter using Node.js fs module.
|
|
5
|
+
* Respects .gitignore and always skips .variantree/, node_modules/, .git/.
|
|
6
|
+
*/
|
|
7
|
+
import type { FileSystemAdapter } from '@variantree/core';
|
|
8
|
+
export declare class NodeFileSystem implements FileSystemAdapter {
|
|
9
|
+
private gitignorePatterns;
|
|
10
|
+
private loadGitignore;
|
|
11
|
+
private shouldIgnore;
|
|
12
|
+
listFiles(rootPath: string): Promise<string[]>;
|
|
13
|
+
readFile(filePath: string): Promise<Uint8Array>;
|
|
14
|
+
writeFile(filePath: string, content: Buffer | Uint8Array): Promise<void>;
|
|
15
|
+
deleteFile(filePath: string): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=fs-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs-adapter.d.ts","sourceRoot":"","sources":["../../src/node/fs-adapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAgB1D,qBAAa,cAAe,YAAW,iBAAiB;IACtD,OAAO,CAAC,iBAAiB,CAAyB;YAEpC,aAAa;IAc3B,OAAO,CAAC,YAAY;IA4Bd,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAsB9C,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAK/C,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAKxE,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAKlD"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @variantree/watcher — Node.js FileSystemAdapter
|
|
3
|
+
*
|
|
4
|
+
* Implements FileSystemAdapter using Node.js fs module.
|
|
5
|
+
* Respects .gitignore and always skips .variantree/, node_modules/, .git/.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
const ALWAYS_SKIP = new Set([
|
|
10
|
+
'node_modules',
|
|
11
|
+
'.variantree',
|
|
12
|
+
'.venv',
|
|
13
|
+
'venv',
|
|
14
|
+
'.git',
|
|
15
|
+
'.DS_Store',
|
|
16
|
+
'dist',
|
|
17
|
+
'opencode.json',
|
|
18
|
+
'.opencode',
|
|
19
|
+
'.vscode',
|
|
20
|
+
'.idea',
|
|
21
|
+
]);
|
|
22
|
+
export class NodeFileSystem {
|
|
23
|
+
gitignorePatterns = null;
|
|
24
|
+
async loadGitignore(rootPath) {
|
|
25
|
+
if (this.gitignorePatterns)
|
|
26
|
+
return this.gitignorePatterns;
|
|
27
|
+
try {
|
|
28
|
+
const raw = await fs.readFile(path.join(rootPath, '.gitignore'), 'utf8');
|
|
29
|
+
this.gitignorePatterns = raw
|
|
30
|
+
.split('\n')
|
|
31
|
+
.map((l) => l.trim())
|
|
32
|
+
.filter((l) => l && !l.startsWith('#'));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
this.gitignorePatterns = [];
|
|
36
|
+
}
|
|
37
|
+
return this.gitignorePatterns;
|
|
38
|
+
}
|
|
39
|
+
shouldIgnore(relativePath, patterns) {
|
|
40
|
+
const parts = relativePath.split(path.sep);
|
|
41
|
+
// Skip if any path segment is in ALWAYS_SKIP
|
|
42
|
+
for (const part of parts) {
|
|
43
|
+
if (ALWAYS_SKIP.has(part))
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
// Simple gitignore matching: check if filename or relative path matches a pattern
|
|
47
|
+
const basename = parts[parts.length - 1];
|
|
48
|
+
for (const pattern of patterns) {
|
|
49
|
+
if (pattern.endsWith('/')) {
|
|
50
|
+
// Directory pattern
|
|
51
|
+
const dirName = pattern.slice(0, -1);
|
|
52
|
+
if (parts.includes(dirName))
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
else if (pattern.startsWith('*.')) {
|
|
56
|
+
// Extension glob
|
|
57
|
+
const ext = pattern.slice(1); // e.g. ".log"
|
|
58
|
+
if (basename.endsWith(ext))
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
else if (pattern.includes('/')) {
|
|
62
|
+
// Path pattern
|
|
63
|
+
if (relativePath.startsWith(pattern))
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Exact name match
|
|
68
|
+
if (basename === pattern || parts.includes(pattern))
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
async listFiles(rootPath) {
|
|
75
|
+
const patterns = await this.loadGitignore(rootPath);
|
|
76
|
+
const results = [];
|
|
77
|
+
const walk = async (dir) => {
|
|
78
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
const full = path.join(dir, entry.name);
|
|
81
|
+
const rel = path.relative(rootPath, full);
|
|
82
|
+
if (this.shouldIgnore(rel, patterns))
|
|
83
|
+
continue;
|
|
84
|
+
if (entry.isDirectory()) {
|
|
85
|
+
await walk(full);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
results.push(rel);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
await walk(rootPath);
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
async readFile(filePath) {
|
|
96
|
+
const buf = await fs.readFile(filePath);
|
|
97
|
+
return new Uint8Array(buf);
|
|
98
|
+
}
|
|
99
|
+
async writeFile(filePath, content) {
|
|
100
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
101
|
+
await fs.writeFile(filePath, content);
|
|
102
|
+
}
|
|
103
|
+
async deleteFile(filePath) {
|
|
104
|
+
try {
|
|
105
|
+
await fs.unlink(filePath);
|
|
106
|
+
}
|
|
107
|
+
catch { /* already gone */ }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=fs-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs-adapter.js","sourceRoot":"","sources":["../../src/node/fs-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @variantree/watcher — Git-backed Snapshot Provider
|
|
3
|
+
*
|
|
4
|
+
* Uses git as a hidden storage engine for workspace snapshots.
|
|
5
|
+
* Zero pollution of the user's git history:
|
|
6
|
+
*
|
|
7
|
+
* - For git repos: stores snapshots as orphan commits under refs/variantree/,
|
|
8
|
+
* invisible to git log, git branch, git push, and git stash list.
|
|
9
|
+
* - For non-git repos: initializes a private repo inside .variantree/git/,
|
|
10
|
+
* completely transparent to the user.
|
|
11
|
+
*
|
|
12
|
+
* Key trick: GIT_INDEX_FILE env var creates a temporary staging area so we
|
|
13
|
+
* never touch the user's actual index (staged changes are preserved).
|
|
14
|
+
*/
|
|
15
|
+
import type { SnapshotProvider, RestoreSummary, SnapshotDiff } from '@variantree/core';
|
|
16
|
+
export declare class GitSnapshotProvider implements SnapshotProvider {
|
|
17
|
+
private gitDir;
|
|
18
|
+
private useInternalRepo;
|
|
19
|
+
private initPromise;
|
|
20
|
+
private cachedWorkspacePath;
|
|
21
|
+
constructor(workspacePath?: string);
|
|
22
|
+
capture(workspacePath: string, label: string): Promise<string>;
|
|
23
|
+
restore(workspacePath: string, ref: string): Promise<RestoreSummary>;
|
|
24
|
+
diff(workspacePath: string, ref: string): Promise<SnapshotDiff>;
|
|
25
|
+
diffRefs(refA: string, refB: string): Promise<SnapshotDiff>;
|
|
26
|
+
drop(ref: string): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Get the number of files in a snapshot. Not part of the SnapshotProvider
|
|
29
|
+
* interface — convenience method for status display.
|
|
30
|
+
*/
|
|
31
|
+
getSnapshotFileCount(ref: string): Promise<number>;
|
|
32
|
+
private ensureInit;
|
|
33
|
+
private _init;
|
|
34
|
+
/** Add a pattern to .git/info/exclude if not already present. */
|
|
35
|
+
private ensureExclude;
|
|
36
|
+
private buildEnv;
|
|
37
|
+
private git;
|
|
38
|
+
/** Read binary content from git (e.g., file blobs via `git show`). */
|
|
39
|
+
private gitBinary;
|
|
40
|
+
/** Parse the output of `git diff-tree -r --name-status`. */
|
|
41
|
+
private diffTreeParsed;
|
|
42
|
+
/** List all file paths in a snapshot tree. */
|
|
43
|
+
private listTreeFiles;
|
|
44
|
+
private requireWorkspacePath;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=git-snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-snapshot.d.ts","sourceRoot":"","sources":["../../src/node/git-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAoCvF,qBAAa,mBAAoB,YAAW,gBAAgB;IAC1D,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,mBAAmB,CAAuB;gBAEtC,aAAa,CAAC,EAAE,MAAM;IAQ5B,OAAO,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAyB9D,OAAO,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAiCpE,IAAI,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAY/D,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAqB3D,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUtC;;;OAGG;IACG,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAU1C,UAAU;YASV,KAAK;IAsCnB,iEAAiE;YACnD,aAAa;IAc3B,OAAO,CAAC,QAAQ;YAYF,GAAG;IAajB,sEAAsE;IACtE,OAAO,CAAC,SAAS;IAqBjB,4DAA4D;YAC9C,cAAc;IAsB5B,8CAA8C;YAChC,aAAa;IAO3B,OAAO,CAAC,oBAAoB;CAM7B"}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @variantree/watcher — Git-backed Snapshot Provider
|
|
3
|
+
*
|
|
4
|
+
* Uses git as a hidden storage engine for workspace snapshots.
|
|
5
|
+
* Zero pollution of the user's git history:
|
|
6
|
+
*
|
|
7
|
+
* - For git repos: stores snapshots as orphan commits under refs/variantree/,
|
|
8
|
+
* invisible to git log, git branch, git push, and git stash list.
|
|
9
|
+
* - For non-git repos: initializes a private repo inside .variantree/git/,
|
|
10
|
+
* completely transparent to the user.
|
|
11
|
+
*
|
|
12
|
+
* Key trick: GIT_INDEX_FILE env var creates a temporary staging area so we
|
|
13
|
+
* never touch the user's actual index (staged changes are preserved).
|
|
14
|
+
*/
|
|
15
|
+
import { execFile, spawn } from 'node:child_process';
|
|
16
|
+
import { promisify } from 'node:util';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import fs from 'node:fs/promises';
|
|
19
|
+
const execAsync = promisify(execFile);
|
|
20
|
+
/** Fallback excludes used only when a non-git project has no .gitignore. */
|
|
21
|
+
const FALLBACK_EXCLUDES = [
|
|
22
|
+
'node_modules',
|
|
23
|
+
'dist',
|
|
24
|
+
'.venv',
|
|
25
|
+
'venv',
|
|
26
|
+
'.DS_Store',
|
|
27
|
+
'.idea',
|
|
28
|
+
'.vscode',
|
|
29
|
+
'__pycache__',
|
|
30
|
+
].join('\n') + '\n';
|
|
31
|
+
/** Always excluded from snapshots regardless of .gitignore. */
|
|
32
|
+
const MANDATORY_EXCLUDES = '.variantree\n.git\n';
|
|
33
|
+
/**
|
|
34
|
+
* Build the content for the internal repo's info/exclude file.
|
|
35
|
+
* If the workspace has a .gitignore, its patterns are used directly.
|
|
36
|
+
* Otherwise, fall back to a sensible default list.
|
|
37
|
+
*/
|
|
38
|
+
async function buildExcludeContent(workspacePath) {
|
|
39
|
+
try {
|
|
40
|
+
const gitignore = await fs.readFile(path.join(workspacePath, '.gitignore'), 'utf8');
|
|
41
|
+
return MANDATORY_EXCLUDES + '# from project .gitignore\n' + gitignore;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return MANDATORY_EXCLUDES + FALLBACK_EXCLUDES;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export class GitSnapshotProvider {
|
|
48
|
+
gitDir = null;
|
|
49
|
+
useInternalRepo = false;
|
|
50
|
+
initPromise = null;
|
|
51
|
+
cachedWorkspacePath = null;
|
|
52
|
+
constructor(workspacePath) {
|
|
53
|
+
if (workspacePath) {
|
|
54
|
+
this.cachedWorkspacePath = workspacePath;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// ─── Public API ─────────────────────────────────────────────────────────
|
|
58
|
+
async capture(workspacePath, label) {
|
|
59
|
+
await this.ensureInit(workspacePath);
|
|
60
|
+
const tmpIndex = path.join(this.gitDir, 'variantree-tmp-index');
|
|
61
|
+
const indexEnv = { GIT_INDEX_FILE: tmpIndex };
|
|
62
|
+
try {
|
|
63
|
+
await this.git(workspacePath, ['add', '-A'], indexEnv);
|
|
64
|
+
const tree = (await this.git(workspacePath, ['write-tree'], indexEnv)).trim();
|
|
65
|
+
const commit = (await this.git(workspacePath, [
|
|
66
|
+
'commit-tree', tree, '-m', `variantree: ${label}`,
|
|
67
|
+
])).trim();
|
|
68
|
+
await this.git(workspacePath, [
|
|
69
|
+
'update-ref', `refs/variantree/${commit}`, commit,
|
|
70
|
+
]);
|
|
71
|
+
return commit;
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
try {
|
|
75
|
+
await fs.unlink(tmpIndex);
|
|
76
|
+
}
|
|
77
|
+
catch { }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async restore(workspacePath, ref) {
|
|
81
|
+
await this.ensureInit(workspacePath);
|
|
82
|
+
const currentRef = await this.capture(workspacePath, '__restore_tmp__');
|
|
83
|
+
const summary = { written: [], deleted: [], skipped: [] };
|
|
84
|
+
try {
|
|
85
|
+
const changes = await this.diffTreeParsed(workspacePath, currentRef, ref);
|
|
86
|
+
for (const { status, filePath } of changes) {
|
|
87
|
+
const fullPath = path.join(workspacePath, filePath);
|
|
88
|
+
if (status === 'A' || status === 'M') {
|
|
89
|
+
const content = await this.gitBinary(workspacePath, ['show', `${ref}:${filePath}`]);
|
|
90
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
91
|
+
await fs.writeFile(fullPath, content);
|
|
92
|
+
summary.written.push(filePath);
|
|
93
|
+
}
|
|
94
|
+
else if (status === 'D') {
|
|
95
|
+
try {
|
|
96
|
+
await fs.unlink(fullPath);
|
|
97
|
+
}
|
|
98
|
+
catch { }
|
|
99
|
+
summary.deleted.push(filePath);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const snapshotFiles = await this.listTreeFiles(workspacePath, ref);
|
|
103
|
+
const changedSet = new Set([...summary.written, ...summary.deleted]);
|
|
104
|
+
summary.skipped = snapshotFiles.filter(f => !changedSet.has(f));
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await this.drop(currentRef);
|
|
108
|
+
}
|
|
109
|
+
return summary;
|
|
110
|
+
}
|
|
111
|
+
async diff(workspacePath, ref) {
|
|
112
|
+
await this.ensureInit(workspacePath);
|
|
113
|
+
const currentRef = await this.capture(workspacePath, '__diff_tmp__');
|
|
114
|
+
try {
|
|
115
|
+
return await this.diffRefs(ref, currentRef);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
await this.drop(currentRef);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async diffRefs(refA, refB) {
|
|
122
|
+
const cwd = this.requireWorkspacePath();
|
|
123
|
+
await this.ensureInit(cwd);
|
|
124
|
+
const result = { added: [], modified: [], deleted: [], unchanged: [] };
|
|
125
|
+
const changes = await this.diffTreeParsed(cwd, refA, refB);
|
|
126
|
+
for (const { status, filePath } of changes) {
|
|
127
|
+
if (status === 'A')
|
|
128
|
+
result.added.push(filePath);
|
|
129
|
+
else if (status === 'M')
|
|
130
|
+
result.modified.push(filePath);
|
|
131
|
+
else if (status === 'D')
|
|
132
|
+
result.deleted.push(filePath);
|
|
133
|
+
}
|
|
134
|
+
const filesB = await this.listTreeFiles(cwd, refB);
|
|
135
|
+
const changedSet = new Set([...result.added, ...result.modified, ...result.deleted]);
|
|
136
|
+
result.unchanged = filesB.filter(f => !changedSet.has(f));
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
async drop(ref) {
|
|
140
|
+
const cwd = this.requireWorkspacePath();
|
|
141
|
+
try {
|
|
142
|
+
await this.git(cwd, ['update-ref', '-d', `refs/variantree/${ref}`]);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Ref may not exist — safe to ignore
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get the number of files in a snapshot. Not part of the SnapshotProvider
|
|
150
|
+
* interface — convenience method for status display.
|
|
151
|
+
*/
|
|
152
|
+
async getSnapshotFileCount(ref) {
|
|
153
|
+
const cwd = this.requireWorkspacePath();
|
|
154
|
+
await this.ensureInit(cwd);
|
|
155
|
+
const files = await this.listTreeFiles(cwd, ref);
|
|
156
|
+
return files.length;
|
|
157
|
+
}
|
|
158
|
+
// ─── Initialization ─────────────────────────────────────────────────────
|
|
159
|
+
async ensureInit(workspacePath) {
|
|
160
|
+
if (this.gitDir !== null)
|
|
161
|
+
return;
|
|
162
|
+
if (this.initPromise)
|
|
163
|
+
return this.initPromise;
|
|
164
|
+
this.cachedWorkspacePath = workspacePath;
|
|
165
|
+
this.initPromise = this._init(workspacePath);
|
|
166
|
+
return this.initPromise;
|
|
167
|
+
}
|
|
168
|
+
async _init(workspacePath) {
|
|
169
|
+
try {
|
|
170
|
+
const { stdout } = await execAsync('git', ['rev-parse', '--git-dir'], {
|
|
171
|
+
cwd: workspacePath,
|
|
172
|
+
});
|
|
173
|
+
this.gitDir = path.resolve(workspacePath, stdout.trim());
|
|
174
|
+
this.useInternalRepo = false;
|
|
175
|
+
// Ensure .variantree is excluded even if user hasn't added it to .gitignore
|
|
176
|
+
await this.ensureExclude(this.gitDir, '.variantree');
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Not inside a git repo — create a private one
|
|
180
|
+
const repoDir = path.join(workspacePath, '.variantree', 'git');
|
|
181
|
+
const dotGitDir = path.join(repoDir, '.git');
|
|
182
|
+
let needsInit = true;
|
|
183
|
+
try {
|
|
184
|
+
await fs.access(path.join(dotGitDir, 'HEAD'));
|
|
185
|
+
needsInit = false;
|
|
186
|
+
}
|
|
187
|
+
catch { }
|
|
188
|
+
if (needsInit) {
|
|
189
|
+
await fs.mkdir(repoDir, { recursive: true });
|
|
190
|
+
await execAsync('git', ['init', repoDir]);
|
|
191
|
+
const excludeDir = path.join(dotGitDir, 'info');
|
|
192
|
+
await fs.mkdir(excludeDir, { recursive: true });
|
|
193
|
+
await fs.writeFile(path.join(excludeDir, 'exclude'), await buildExcludeContent(workspacePath));
|
|
194
|
+
}
|
|
195
|
+
this.gitDir = dotGitDir;
|
|
196
|
+
this.useInternalRepo = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/** Add a pattern to .git/info/exclude if not already present. */
|
|
200
|
+
async ensureExclude(gitDir, pattern) {
|
|
201
|
+
const excludePath = path.join(gitDir, 'info', 'exclude');
|
|
202
|
+
try {
|
|
203
|
+
const content = await fs.readFile(excludePath, 'utf8');
|
|
204
|
+
if (content.includes(pattern))
|
|
205
|
+
return;
|
|
206
|
+
await fs.appendFile(excludePath, `\n${pattern}\n`);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
await fs.mkdir(path.join(gitDir, 'info'), { recursive: true });
|
|
210
|
+
await fs.writeFile(excludePath, `${pattern}\n`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ─── Git Helpers ────────────────────────────────────────────────────────
|
|
214
|
+
buildEnv(workspacePath, extraEnv) {
|
|
215
|
+
const env = { ...process.env };
|
|
216
|
+
if (this.useInternalRepo) {
|
|
217
|
+
env.GIT_DIR = this.gitDir;
|
|
218
|
+
env.GIT_WORK_TREE = workspacePath;
|
|
219
|
+
}
|
|
220
|
+
if (extraEnv)
|
|
221
|
+
Object.assign(env, extraEnv);
|
|
222
|
+
return env;
|
|
223
|
+
}
|
|
224
|
+
async git(workspacePath, args, extraEnv) {
|
|
225
|
+
const { stdout } = await execAsync('git', args, {
|
|
226
|
+
cwd: workspacePath,
|
|
227
|
+
env: this.buildEnv(workspacePath, extraEnv),
|
|
228
|
+
maxBuffer: 100 * 1024 * 1024,
|
|
229
|
+
});
|
|
230
|
+
return stdout;
|
|
231
|
+
}
|
|
232
|
+
/** Read binary content from git (e.g., file blobs via `git show`). */
|
|
233
|
+
gitBinary(workspacePath, args, extraEnv) {
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
const chunks = [];
|
|
236
|
+
const proc = spawn('git', args, {
|
|
237
|
+
cwd: workspacePath,
|
|
238
|
+
env: this.buildEnv(workspacePath, extraEnv),
|
|
239
|
+
});
|
|
240
|
+
proc.stdout.on('data', (chunk) => chunks.push(chunk));
|
|
241
|
+
proc.stderr.on('data', () => { });
|
|
242
|
+
proc.on('close', (code) => {
|
|
243
|
+
if (code === 0)
|
|
244
|
+
resolve(Buffer.concat(chunks));
|
|
245
|
+
else
|
|
246
|
+
reject(new Error(`git ${args[0]} exited with code ${code}`));
|
|
247
|
+
});
|
|
248
|
+
proc.on('error', reject);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/** Parse the output of `git diff-tree -r --name-status`. */
|
|
252
|
+
async diffTreeParsed(workspacePath, refA, refB) {
|
|
253
|
+
const output = await this.git(workspacePath, [
|
|
254
|
+
'diff-tree', '-r', '--name-status', refA, refB,
|
|
255
|
+
]);
|
|
256
|
+
return output
|
|
257
|
+
.trim()
|
|
258
|
+
.split('\n')
|
|
259
|
+
.filter(Boolean)
|
|
260
|
+
.map(line => {
|
|
261
|
+
const tab = line.indexOf('\t');
|
|
262
|
+
return {
|
|
263
|
+
status: line.charAt(0),
|
|
264
|
+
filePath: line.slice(tab + 1),
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
/** List all file paths in a snapshot tree. */
|
|
269
|
+
async listTreeFiles(workspacePath, ref) {
|
|
270
|
+
const output = (await this.git(workspacePath, [
|
|
271
|
+
'ls-tree', '-r', '--name-only', ref,
|
|
272
|
+
])).trim();
|
|
273
|
+
return output ? output.split('\n') : [];
|
|
274
|
+
}
|
|
275
|
+
requireWorkspacePath() {
|
|
276
|
+
if (!this.cachedWorkspacePath) {
|
|
277
|
+
throw new Error('GitSnapshotProvider: no workspace path configured. Call capture() first or pass workspacePath to the constructor.');
|
|
278
|
+
}
|
|
279
|
+
return this.cachedWorkspacePath;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
//# sourceMappingURL=git-snapshot.js.map
|