@amoghvj/txr 0.1.1 → 0.2.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/README.md +4 -11
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-6SSBMHMQ.js +185 -0
- package/dist/chunk-KXCWVEEG.js +47 -0
- package/dist/{chunk-XKFFIQXW.js → chunk-MCJNEIJH.js} +0 -8
- package/dist/external-detector-5TT634UP.js +7 -0
- package/dist/git-store-MUETZYUT.js +7 -0
- package/dist/history-store-2H73O75H.js +7 -0
- package/dist/index.js +670 -188
- package/package.json +2 -1
- package/dist/git-store-XSOIM4KZ.js +0 -6
package/README.md
CHANGED
|
@@ -13,21 +13,13 @@ It works independently of any user Git repository, maintaining its own internal
|
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
`txr` is available as a global npm package.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
|
|
20
|
-
cd agent-cli
|
|
21
|
-
|
|
22
|
-
# Install dependencies and build
|
|
23
|
-
npm install
|
|
24
|
-
npm run build
|
|
25
|
-
|
|
26
|
-
# Link globally (optional, so you can run 'txr' anywhere)
|
|
27
|
-
npm link
|
|
19
|
+
npm install -g @amoghvj/txr
|
|
28
20
|
```
|
|
29
21
|
|
|
30
|
-
|
|
22
|
+
Once installed, the `txr` command will be available anywhere on your system.
|
|
31
23
|
|
|
32
24
|
## Usage Instructions
|
|
33
25
|
|
|
@@ -110,6 +102,7 @@ txr history
|
|
|
110
102
|
|
|
111
103
|
## Advanced Options
|
|
112
104
|
|
|
105
|
+
* `txr clear`: Permanently reset the internal state and delete all transaction history in the active `.txr` folder. Useful if the system enters an unrecoverable state or you want to start fresh. Use `-y` to skip confirmation.
|
|
113
106
|
* `txr run --transactional "cmd"`: Force a command to be tracked as transactional, even if the classifier flags it as unsafe.
|
|
114
107
|
* `txr run --no-transaction "cmd"`: Force a command to run directly without tracking or generating a transaction.
|
|
115
108
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
__require
|
|
10
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// src/core/external-detector.ts
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as crypto from "crypto";
|
|
5
|
+
import { nanoid } from "nanoid";
|
|
6
|
+
var ExternalChangeDetector = class {
|
|
7
|
+
constructor(txStore, fileMap, gitStore, config, projectRoot) {
|
|
8
|
+
this.txStore = txStore;
|
|
9
|
+
this.fileMap = fileMap;
|
|
10
|
+
this.gitStore = gitStore;
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.projectRoot = projectRoot;
|
|
13
|
+
}
|
|
14
|
+
txStore;
|
|
15
|
+
fileMap;
|
|
16
|
+
gitStore;
|
|
17
|
+
config;
|
|
18
|
+
projectRoot;
|
|
19
|
+
async detectAndRecord() {
|
|
20
|
+
return this.detectAndRecordWithSource("external");
|
|
21
|
+
}
|
|
22
|
+
async detectAndRecordWithSource(source) {
|
|
23
|
+
const headTx = this.txStore.getHead();
|
|
24
|
+
if (!headTx) return null;
|
|
25
|
+
const divergences = await this.findDivergences(headTx);
|
|
26
|
+
if (divergences.length === 0) return null;
|
|
27
|
+
for (const div of divergences) {
|
|
28
|
+
if (div.type === "created" || div.type === "modified") {
|
|
29
|
+
try {
|
|
30
|
+
const content = await fs.readFile(div.absolutePath);
|
|
31
|
+
await this.gitStore.writeFile(div.fileId, content);
|
|
32
|
+
} catch {
|
|
33
|
+
await this.gitStore.removeFile(div.fileId).catch(() => {
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
} else if (div.type === "deleted") {
|
|
37
|
+
await this.gitStore.removeFile(div.fileId).catch(() => {
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const isExternal = source === "external";
|
|
42
|
+
const txId = isExternal ? this.txStore.nextExtId() : this.txStore.nextId();
|
|
43
|
+
const actionType = isExternal ? "E" : "U";
|
|
44
|
+
const commitMsg = isExternal ? `txr: ${txId} | [external change \u2014 ${divergences.length} file(s)]` : `txr: ${txId} | passive: ${source}`;
|
|
45
|
+
const commitHash = await this.gitStore.commit(commitMsg);
|
|
46
|
+
const changeManifest = {};
|
|
47
|
+
for (const div of divergences) {
|
|
48
|
+
changeManifest[div.fileId] = div.type;
|
|
49
|
+
}
|
|
50
|
+
const tx = {
|
|
51
|
+
id: txId,
|
|
52
|
+
type: actionType,
|
|
53
|
+
parent: headTx.id,
|
|
54
|
+
commit: commitHash,
|
|
55
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
56
|
+
command: isExternal ? null : source,
|
|
57
|
+
source,
|
|
58
|
+
touchedFiles: divergences.map((d) => d.fileId),
|
|
59
|
+
changeManifest,
|
|
60
|
+
workingDirectory: this.projectRoot,
|
|
61
|
+
exitCode: isExternal ? null : 0,
|
|
62
|
+
attributionTier: null,
|
|
63
|
+
scope: this.config.scope,
|
|
64
|
+
scopeWarnings: []
|
|
65
|
+
};
|
|
66
|
+
for (const div of divergences) {
|
|
67
|
+
if (div.type === "deleted") {
|
|
68
|
+
const entry = this.fileMap.getEntry(div.fileId);
|
|
69
|
+
if (entry && !entry.deletedAt) {
|
|
70
|
+
this.fileMap.markDeleted(div.fileId, txId);
|
|
71
|
+
}
|
|
72
|
+
} else if (div.type === "created") {
|
|
73
|
+
this.fileMap.registerNewFile(div.absolutePath, div.fileId, txId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (isExternal) {
|
|
77
|
+
this.txStore.addExternalTransaction(tx);
|
|
78
|
+
} else {
|
|
79
|
+
const discarded = this.txStore.addTransaction(tx);
|
|
80
|
+
if (discarded.length > 0) {
|
|
81
|
+
this.fileMap.untrackByTransactions(discarded);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
await this.txStore.save();
|
|
85
|
+
await this.fileMap.save();
|
|
86
|
+
const { HistoryStore } = await import("./history-store-2H73O75H.js");
|
|
87
|
+
const historyStore = await HistoryStore.load(path.join(this.projectRoot, ".txr", "metadata"));
|
|
88
|
+
historyStore.append({
|
|
89
|
+
timestamp: tx.timestamp,
|
|
90
|
+
command: isExternal ? null : source,
|
|
91
|
+
transactionId: txId,
|
|
92
|
+
workingDirectory: this.projectRoot,
|
|
93
|
+
exitCode: isExternal ? null : 0,
|
|
94
|
+
classification: isExternal ? "external" : "transactional",
|
|
95
|
+
attributionTier: null,
|
|
96
|
+
durationMs: 0,
|
|
97
|
+
actionType
|
|
98
|
+
});
|
|
99
|
+
await historyStore.save();
|
|
100
|
+
if (isExternal) {
|
|
101
|
+
process.stderr.write(
|
|
102
|
+
`\x1B[33m\u26A0 External changes detected.\x1B[0m ${divergences.length} file(s) modified outside TXR since ${headTx.id}.
|
|
103
|
+
Recorded as \x1B[1m${txId}\x1B[0m. Run \x1B[1mtxr history\x1B[0m to review.
|
|
104
|
+
`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return tx;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Find all filesystem divergences since the HEAD transaction's commit.
|
|
111
|
+
*
|
|
112
|
+
* Strategy:
|
|
113
|
+
* 1. For each file tracked in HEAD's commit tree → compare blob hash to disk hash.
|
|
114
|
+
* 2. Scan watch paths for new files not yet in FileMap.
|
|
115
|
+
*/
|
|
116
|
+
async findDivergences(headTx) {
|
|
117
|
+
const divergences = [];
|
|
118
|
+
const activeFiles = this.fileMap.getActiveFiles();
|
|
119
|
+
for (const { id: fileId, entry } of activeFiles) {
|
|
120
|
+
const absolutePath = entry.path;
|
|
121
|
+
const existsInCommit = await this.gitStore.fileExistsAtCommit(headTx.commit, fileId);
|
|
122
|
+
if (!existsInCommit) continue;
|
|
123
|
+
let existsOnDisk = false;
|
|
124
|
+
let diskHash = null;
|
|
125
|
+
try {
|
|
126
|
+
const content = await fs.readFile(absolutePath);
|
|
127
|
+
existsOnDisk = true;
|
|
128
|
+
diskHash = this.hash(content);
|
|
129
|
+
} catch {
|
|
130
|
+
existsOnDisk = false;
|
|
131
|
+
}
|
|
132
|
+
if (!existsOnDisk) {
|
|
133
|
+
divergences.push({ fileId, absolutePath, type: "deleted" });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const blobContent = await this.gitStore.readBlob(headTx.commit, fileId);
|
|
138
|
+
const blobHash = this.hash(blobContent);
|
|
139
|
+
if (diskHash !== blobHash) {
|
|
140
|
+
divergences.push({ fileId, absolutePath, type: "modified" });
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
divergences.push({ fileId, absolutePath, type: "modified" });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const { glob } = await import("glob");
|
|
148
|
+
const allFiles = await glob("**/*", {
|
|
149
|
+
cwd: this.projectRoot,
|
|
150
|
+
nodir: true,
|
|
151
|
+
absolute: true,
|
|
152
|
+
ignore: [
|
|
153
|
+
".git/**",
|
|
154
|
+
".txr/**",
|
|
155
|
+
"node_modules/**",
|
|
156
|
+
"__pycache__/**",
|
|
157
|
+
".venv/**",
|
|
158
|
+
"venv/**",
|
|
159
|
+
"dist/**",
|
|
160
|
+
"build/**"
|
|
161
|
+
]
|
|
162
|
+
});
|
|
163
|
+
for (const absolutePath of allFiles) {
|
|
164
|
+
if (!this.fileMap.getIdByPath(absolutePath)) {
|
|
165
|
+
const tempFileId = nanoid();
|
|
166
|
+
divergences.push({
|
|
167
|
+
fileId: tempFileId,
|
|
168
|
+
absolutePath,
|
|
169
|
+
type: "created"
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.error("Error scanning for new files:", err);
|
|
175
|
+
}
|
|
176
|
+
return divergences;
|
|
177
|
+
}
|
|
178
|
+
hash(content) {
|
|
179
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export {
|
|
184
|
+
ExternalChangeDetector
|
|
185
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/core/history-store.ts
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
var HistoryStore = class _HistoryStore {
|
|
5
|
+
data;
|
|
6
|
+
filePath;
|
|
7
|
+
constructor(filePath, data) {
|
|
8
|
+
this.filePath = filePath;
|
|
9
|
+
this.data = data;
|
|
10
|
+
}
|
|
11
|
+
static async load(metadataDir) {
|
|
12
|
+
const filePath = path.join(metadataDir, "history.json");
|
|
13
|
+
try {
|
|
14
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
15
|
+
const data = JSON.parse(raw);
|
|
16
|
+
return new _HistoryStore(filePath, data);
|
|
17
|
+
} catch {
|
|
18
|
+
const data = { version: 1, entries: [] };
|
|
19
|
+
return new _HistoryStore(filePath, data);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async save() {
|
|
23
|
+
const tmp = this.filePath + ".tmp";
|
|
24
|
+
await fs.writeFile(tmp, JSON.stringify(this.data, null, 2), "utf-8");
|
|
25
|
+
await fs.rename(tmp, this.filePath);
|
|
26
|
+
}
|
|
27
|
+
/** Append a new history entry. */
|
|
28
|
+
append(entry) {
|
|
29
|
+
this.data.entries.push(entry);
|
|
30
|
+
}
|
|
31
|
+
/** Get all entries (oldest first). */
|
|
32
|
+
getAll() {
|
|
33
|
+
return this.data.entries;
|
|
34
|
+
}
|
|
35
|
+
/** Get the N most recent entries. */
|
|
36
|
+
getRecent(n) {
|
|
37
|
+
return this.data.entries.slice(-n);
|
|
38
|
+
}
|
|
39
|
+
/** Get total entry count. */
|
|
40
|
+
get count() {
|
|
41
|
+
return this.data.entries.length;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
HistoryStore
|
|
47
|
+
};
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
-
});
|
|
7
|
-
|
|
8
1
|
// src/git/git-store.ts
|
|
9
2
|
import * as fs from "fs/promises";
|
|
10
3
|
import * as path from "path";
|
|
@@ -181,6 +174,5 @@ var GitStore = class {
|
|
|
181
174
|
};
|
|
182
175
|
|
|
183
176
|
export {
|
|
184
|
-
__require,
|
|
185
177
|
GitStore
|
|
186
178
|
};
|