@amoghvj/txr 0.1.0 → 0.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/README.md +4 -11
- package/dist/chunk-XKFFIQXW.js +186 -0
- package/dist/git-store-XSOIM4KZ.js +6 -0
- package/dist/index.js +192 -266
- package/package.json +1 -1
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,186 @@
|
|
|
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
|
+
// src/git/git-store.ts
|
|
9
|
+
import * as fs from "fs/promises";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import git from "isomorphic-git";
|
|
12
|
+
var GitStore = class {
|
|
13
|
+
repoDir;
|
|
14
|
+
constructor(repoDir) {
|
|
15
|
+
this.repoDir = repoDir;
|
|
16
|
+
}
|
|
17
|
+
/** Initialize a new Git repository with an empty initial commit. */
|
|
18
|
+
async init() {
|
|
19
|
+
await fs.mkdir(this.repoDir, { recursive: true });
|
|
20
|
+
await git.init({ fs, dir: this.repoDir });
|
|
21
|
+
const commitHash = await git.commit({
|
|
22
|
+
fs,
|
|
23
|
+
dir: this.repoDir,
|
|
24
|
+
message: "txr: init",
|
|
25
|
+
author: { name: "txr", email: "txr@internal" }
|
|
26
|
+
});
|
|
27
|
+
return commitHash;
|
|
28
|
+
}
|
|
29
|
+
/** Write a file (by file ID) into the repo working tree and stage it. */
|
|
30
|
+
async writeFile(fileId, content) {
|
|
31
|
+
const filePath = path.join(this.repoDir, fileId);
|
|
32
|
+
await fs.writeFile(filePath, content);
|
|
33
|
+
await git.add({ fs, dir: this.repoDir, filepath: fileId });
|
|
34
|
+
}
|
|
35
|
+
/** Remove a file (by file ID) from the repo working tree and stage the removal. */
|
|
36
|
+
async removeFile(fileId) {
|
|
37
|
+
const filePath = path.join(this.repoDir, fileId);
|
|
38
|
+
try {
|
|
39
|
+
await fs.unlink(filePath);
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
await git.remove({ fs, dir: this.repoDir, filepath: fileId });
|
|
43
|
+
}
|
|
44
|
+
/** Create a commit with all currently staged changes. */
|
|
45
|
+
async commit(message) {
|
|
46
|
+
const commitHash = await git.commit({
|
|
47
|
+
fs,
|
|
48
|
+
dir: this.repoDir,
|
|
49
|
+
message,
|
|
50
|
+
author: { name: "txr", email: "txr@internal" }
|
|
51
|
+
});
|
|
52
|
+
return commitHash;
|
|
53
|
+
}
|
|
54
|
+
/** Read file content at a specific commit. Returns the raw Buffer. */
|
|
55
|
+
async readBlob(commitHash, fileId) {
|
|
56
|
+
const { blob } = await git.readBlob({
|
|
57
|
+
fs,
|
|
58
|
+
dir: this.repoDir,
|
|
59
|
+
oid: commitHash,
|
|
60
|
+
filepath: fileId
|
|
61
|
+
});
|
|
62
|
+
return Buffer.from(blob);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check if a file exists in a specific commit's tree.
|
|
66
|
+
*/
|
|
67
|
+
async fileExistsAtCommit(commitHash, fileId) {
|
|
68
|
+
try {
|
|
69
|
+
await git.readBlob({
|
|
70
|
+
fs,
|
|
71
|
+
dir: this.repoDir,
|
|
72
|
+
oid: commitHash,
|
|
73
|
+
filepath: fileId
|
|
74
|
+
});
|
|
75
|
+
return true;
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Hard reset the repo to a specific commit.
|
|
82
|
+
*
|
|
83
|
+
* isomorphic-git doesn't have a native `reset --hard`, so we:
|
|
84
|
+
* 1. Update the branch ref to point to the target commit.
|
|
85
|
+
* 2. Check out the working tree from that commit.
|
|
86
|
+
*/
|
|
87
|
+
async resetHard(commitHash) {
|
|
88
|
+
await git.writeRef({
|
|
89
|
+
fs,
|
|
90
|
+
dir: this.repoDir,
|
|
91
|
+
ref: "refs/heads/main",
|
|
92
|
+
value: commitHash,
|
|
93
|
+
force: true
|
|
94
|
+
});
|
|
95
|
+
await git.checkout({
|
|
96
|
+
fs,
|
|
97
|
+
dir: this.repoDir,
|
|
98
|
+
ref: "main",
|
|
99
|
+
force: true
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Reset to the initial empty state.
|
|
104
|
+
* Removes all tracked files and creates a fresh empty commit.
|
|
105
|
+
*/
|
|
106
|
+
async resetToEmpty() {
|
|
107
|
+
const entries = await fs.readdir(this.repoDir);
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
if (entry === ".git") continue;
|
|
110
|
+
const fullPath = path.join(this.repoDir, entry);
|
|
111
|
+
const stat2 = await fs.stat(fullPath);
|
|
112
|
+
if (stat2.isFile()) {
|
|
113
|
+
await git.remove({ fs, dir: this.repoDir, filepath: entry });
|
|
114
|
+
await fs.unlink(fullPath);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const commitHash = await git.commit({
|
|
118
|
+
fs,
|
|
119
|
+
dir: this.repoDir,
|
|
120
|
+
message: "txr: reset to empty",
|
|
121
|
+
author: { name: "txr", email: "txr@internal" }
|
|
122
|
+
});
|
|
123
|
+
return commitHash;
|
|
124
|
+
}
|
|
125
|
+
/** Get the current HEAD commit hash. */
|
|
126
|
+
async getHead() {
|
|
127
|
+
return await git.resolveRef({ fs, dir: this.repoDir, ref: "HEAD" });
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* List files that differ between two commits.
|
|
131
|
+
* Returns arrays of added, modified, and deleted file IDs.
|
|
132
|
+
*/
|
|
133
|
+
async diffCommits(commitA, commitB) {
|
|
134
|
+
const treeA = await this.listTree(commitA);
|
|
135
|
+
const treeB = await this.listTree(commitB);
|
|
136
|
+
const added = [];
|
|
137
|
+
const modified = [];
|
|
138
|
+
const deleted = [];
|
|
139
|
+
for (const [filepath, oidB] of treeB) {
|
|
140
|
+
const oidA = treeA.get(filepath);
|
|
141
|
+
if (!oidA) {
|
|
142
|
+
added.push(filepath);
|
|
143
|
+
} else if (oidA !== oidB) {
|
|
144
|
+
modified.push(filepath);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const [filepath] of treeA) {
|
|
148
|
+
if (!treeB.has(filepath)) {
|
|
149
|
+
deleted.push(filepath);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { added, modified, deleted };
|
|
153
|
+
}
|
|
154
|
+
/** List all files in a commit's tree as Map<filepath, blobOid>. */
|
|
155
|
+
async listTree(commitHash) {
|
|
156
|
+
const result = /* @__PURE__ */ new Map();
|
|
157
|
+
try {
|
|
158
|
+
const entries = await git.walk({
|
|
159
|
+
fs,
|
|
160
|
+
dir: this.repoDir,
|
|
161
|
+
trees: [git.TREE({ ref: commitHash })],
|
|
162
|
+
map: async (filepath, [entry]) => {
|
|
163
|
+
if (!entry || filepath === ".") return void 0;
|
|
164
|
+
const type = await entry.type();
|
|
165
|
+
if (type === "blob") {
|
|
166
|
+
const oid = await entry.oid();
|
|
167
|
+
return { filepath, oid };
|
|
168
|
+
}
|
|
169
|
+
return void 0;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
if (entry) {
|
|
174
|
+
result.set(entry.filepath, entry.oid);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export {
|
|
184
|
+
__require,
|
|
185
|
+
GitStore
|
|
186
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,194 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
-
});
|
|
2
|
+
import {
|
|
3
|
+
GitStore,
|
|
4
|
+
__require
|
|
5
|
+
} from "./chunk-XKFFIQXW.js";
|
|
8
6
|
|
|
9
7
|
// src/index.ts
|
|
10
8
|
import { Command } from "commander";
|
|
11
9
|
|
|
12
10
|
// src/cli/commands/init.ts
|
|
13
|
-
import * as fs2 from "fs/promises";
|
|
14
|
-
import * as path2 from "path";
|
|
15
|
-
|
|
16
|
-
// src/git/git-store.ts
|
|
17
11
|
import * as fs from "fs/promises";
|
|
18
12
|
import * as path from "path";
|
|
19
|
-
import
|
|
20
|
-
var GitStore = class {
|
|
21
|
-
repoDir;
|
|
22
|
-
constructor(repoDir) {
|
|
23
|
-
this.repoDir = repoDir;
|
|
24
|
-
}
|
|
25
|
-
/** Initialize a new Git repository with an empty initial commit. */
|
|
26
|
-
async init() {
|
|
27
|
-
await fs.mkdir(this.repoDir, { recursive: true });
|
|
28
|
-
await git.init({ fs, dir: this.repoDir });
|
|
29
|
-
const commitHash = await git.commit({
|
|
30
|
-
fs,
|
|
31
|
-
dir: this.repoDir,
|
|
32
|
-
message: "txr: init",
|
|
33
|
-
author: { name: "txr", email: "txr@internal" }
|
|
34
|
-
});
|
|
35
|
-
return commitHash;
|
|
36
|
-
}
|
|
37
|
-
/** Write a file (by file ID) into the repo working tree and stage it. */
|
|
38
|
-
async writeFile(fileId, content) {
|
|
39
|
-
const filePath = path.join(this.repoDir, fileId);
|
|
40
|
-
await fs.writeFile(filePath, content);
|
|
41
|
-
await git.add({ fs, dir: this.repoDir, filepath: fileId });
|
|
42
|
-
}
|
|
43
|
-
/** Remove a file (by file ID) from the repo working tree and stage the removal. */
|
|
44
|
-
async removeFile(fileId) {
|
|
45
|
-
const filePath = path.join(this.repoDir, fileId);
|
|
46
|
-
try {
|
|
47
|
-
await fs.unlink(filePath);
|
|
48
|
-
} catch {
|
|
49
|
-
}
|
|
50
|
-
await git.remove({ fs, dir: this.repoDir, filepath: fileId });
|
|
51
|
-
}
|
|
52
|
-
/** Create a commit with all currently staged changes. */
|
|
53
|
-
async commit(message) {
|
|
54
|
-
const commitHash = await git.commit({
|
|
55
|
-
fs,
|
|
56
|
-
dir: this.repoDir,
|
|
57
|
-
message,
|
|
58
|
-
author: { name: "txr", email: "txr@internal" }
|
|
59
|
-
});
|
|
60
|
-
return commitHash;
|
|
61
|
-
}
|
|
62
|
-
/** Read file content at a specific commit. Returns the raw Buffer. */
|
|
63
|
-
async readBlob(commitHash, fileId) {
|
|
64
|
-
const { blob } = await git.readBlob({
|
|
65
|
-
fs,
|
|
66
|
-
dir: this.repoDir,
|
|
67
|
-
oid: commitHash,
|
|
68
|
-
filepath: fileId
|
|
69
|
-
});
|
|
70
|
-
return Buffer.from(blob);
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Check if a file exists in a specific commit's tree.
|
|
74
|
-
*/
|
|
75
|
-
async fileExistsAtCommit(commitHash, fileId) {
|
|
76
|
-
try {
|
|
77
|
-
await git.readBlob({
|
|
78
|
-
fs,
|
|
79
|
-
dir: this.repoDir,
|
|
80
|
-
oid: commitHash,
|
|
81
|
-
filepath: fileId
|
|
82
|
-
});
|
|
83
|
-
return true;
|
|
84
|
-
} catch {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Hard reset the repo to a specific commit.
|
|
90
|
-
*
|
|
91
|
-
* isomorphic-git doesn't have a native `reset --hard`, so we:
|
|
92
|
-
* 1. Update the branch ref to point to the target commit.
|
|
93
|
-
* 2. Check out the working tree from that commit.
|
|
94
|
-
*/
|
|
95
|
-
async resetHard(commitHash) {
|
|
96
|
-
await git.writeRef({
|
|
97
|
-
fs,
|
|
98
|
-
dir: this.repoDir,
|
|
99
|
-
ref: "refs/heads/main",
|
|
100
|
-
value: commitHash,
|
|
101
|
-
force: true
|
|
102
|
-
});
|
|
103
|
-
await git.checkout({
|
|
104
|
-
fs,
|
|
105
|
-
dir: this.repoDir,
|
|
106
|
-
ref: "main",
|
|
107
|
-
force: true
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Reset to the initial empty state.
|
|
112
|
-
* Removes all tracked files and creates a fresh empty commit.
|
|
113
|
-
*/
|
|
114
|
-
async resetToEmpty() {
|
|
115
|
-
const entries = await fs.readdir(this.repoDir);
|
|
116
|
-
for (const entry of entries) {
|
|
117
|
-
if (entry === ".git") continue;
|
|
118
|
-
const fullPath = path.join(this.repoDir, entry);
|
|
119
|
-
const stat4 = await fs.stat(fullPath);
|
|
120
|
-
if (stat4.isFile()) {
|
|
121
|
-
await git.remove({ fs, dir: this.repoDir, filepath: entry });
|
|
122
|
-
await fs.unlink(fullPath);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
const commitHash = await git.commit({
|
|
126
|
-
fs,
|
|
127
|
-
dir: this.repoDir,
|
|
128
|
-
message: "txr: reset to empty",
|
|
129
|
-
author: { name: "txr", email: "txr@internal" }
|
|
130
|
-
});
|
|
131
|
-
return commitHash;
|
|
132
|
-
}
|
|
133
|
-
/** Get the current HEAD commit hash. */
|
|
134
|
-
async getHead() {
|
|
135
|
-
return await git.resolveRef({ fs, dir: this.repoDir, ref: "HEAD" });
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* List files that differ between two commits.
|
|
139
|
-
* Returns arrays of added, modified, and deleted file IDs.
|
|
140
|
-
*/
|
|
141
|
-
async diffCommits(commitA, commitB) {
|
|
142
|
-
const treeA = await this.listTree(commitA);
|
|
143
|
-
const treeB = await this.listTree(commitB);
|
|
144
|
-
const added = [];
|
|
145
|
-
const modified = [];
|
|
146
|
-
const deleted = [];
|
|
147
|
-
for (const [filepath, oidB] of treeB) {
|
|
148
|
-
const oidA = treeA.get(filepath);
|
|
149
|
-
if (!oidA) {
|
|
150
|
-
added.push(filepath);
|
|
151
|
-
} else if (oidA !== oidB) {
|
|
152
|
-
modified.push(filepath);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
for (const [filepath] of treeA) {
|
|
156
|
-
if (!treeB.has(filepath)) {
|
|
157
|
-
deleted.push(filepath);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return { added, modified, deleted };
|
|
161
|
-
}
|
|
162
|
-
/** List all files in a commit's tree as Map<filepath, blobOid>. */
|
|
163
|
-
async listTree(commitHash) {
|
|
164
|
-
const result = /* @__PURE__ */ new Map();
|
|
165
|
-
try {
|
|
166
|
-
const entries = await git.walk({
|
|
167
|
-
fs,
|
|
168
|
-
dir: this.repoDir,
|
|
169
|
-
trees: [git.TREE({ ref: commitHash })],
|
|
170
|
-
map: async (filepath, [entry]) => {
|
|
171
|
-
if (!entry || filepath === ".") return void 0;
|
|
172
|
-
const type = await entry.type();
|
|
173
|
-
if (type === "blob") {
|
|
174
|
-
const oid = await entry.oid();
|
|
175
|
-
return { filepath, oid };
|
|
176
|
-
}
|
|
177
|
-
return void 0;
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
for (const entry of entries) {
|
|
181
|
-
if (entry) {
|
|
182
|
-
result.set(entry.filepath, entry.oid);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
} catch {
|
|
186
|
-
}
|
|
187
|
-
return result;
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
// src/cli/commands/init.ts
|
|
13
|
+
import * as os from "os";
|
|
192
14
|
var TXR_DIR = ".txr";
|
|
193
15
|
var REPO_DIR = "repo";
|
|
194
16
|
var METADATA_DIR = "metadata";
|
|
@@ -216,70 +38,78 @@ var GLOBAL_CONFIG = {
|
|
|
216
38
|
// Base watch path; additional paths can be added
|
|
217
39
|
};
|
|
218
40
|
async function initTxr(projectRoot, options = {}) {
|
|
219
|
-
const txrDir =
|
|
220
|
-
const repoDir =
|
|
221
|
-
const metadataDir =
|
|
41
|
+
const txrDir = path.join(projectRoot, TXR_DIR);
|
|
42
|
+
const repoDir = path.join(txrDir, REPO_DIR);
|
|
43
|
+
const metadataDir = path.join(txrDir, METADATA_DIR);
|
|
222
44
|
try {
|
|
223
|
-
await
|
|
45
|
+
await fs.access(txrDir);
|
|
224
46
|
throw new Error(`Already initialized: ${txrDir} exists.`);
|
|
225
47
|
} catch (err) {
|
|
226
48
|
if (err.message?.startsWith("Already initialized")) throw err;
|
|
227
49
|
}
|
|
228
|
-
await
|
|
50
|
+
await fs.mkdir(metadataDir, { recursive: true });
|
|
229
51
|
const gitStore = new GitStore(repoDir);
|
|
230
52
|
await gitStore.init();
|
|
231
|
-
await
|
|
232
|
-
|
|
53
|
+
await fs.writeFile(
|
|
54
|
+
path.join(metadataDir, "mapping.json"),
|
|
233
55
|
JSON.stringify({ version: 1, files: {} }, null, 2),
|
|
234
56
|
"utf-8"
|
|
235
57
|
);
|
|
236
|
-
await
|
|
237
|
-
|
|
58
|
+
await fs.writeFile(
|
|
59
|
+
path.join(metadataDir, "transactions.json"),
|
|
238
60
|
JSON.stringify({ version: 1, head: null, undoStack: [], counter: 0, transactions: {} }, null, 2),
|
|
239
61
|
"utf-8"
|
|
240
62
|
);
|
|
241
|
-
await
|
|
242
|
-
|
|
63
|
+
await fs.writeFile(
|
|
64
|
+
path.join(metadataDir, "history.json"),
|
|
243
65
|
JSON.stringify({ version: 1, entries: [] }, null, 2),
|
|
244
66
|
"utf-8"
|
|
245
67
|
);
|
|
246
68
|
const config = options.scope === "global" ? GLOBAL_CONFIG : DEFAULT_CONFIG;
|
|
247
|
-
await
|
|
248
|
-
|
|
69
|
+
await fs.writeFile(
|
|
70
|
+
path.join(metadataDir, "config.json"),
|
|
249
71
|
JSON.stringify(config, null, 2),
|
|
250
72
|
"utf-8"
|
|
251
73
|
);
|
|
252
74
|
}
|
|
253
75
|
async function findTxrRoot(startDir) {
|
|
254
|
-
let dir =
|
|
76
|
+
let dir = path.resolve(startDir);
|
|
255
77
|
while (true) {
|
|
256
|
-
const candidate =
|
|
78
|
+
const candidate = path.join(dir, TXR_DIR);
|
|
257
79
|
try {
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
80
|
+
const stat3 = await fs.stat(candidate);
|
|
81
|
+
if (stat3.isDirectory()) return dir;
|
|
260
82
|
} catch {
|
|
261
83
|
}
|
|
262
|
-
const parent =
|
|
84
|
+
const parent = path.dirname(dir);
|
|
263
85
|
if (parent === dir) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
86
|
+
const homeDir = os.homedir();
|
|
87
|
+
const homeTxr = path.join(homeDir, TXR_DIR);
|
|
88
|
+
try {
|
|
89
|
+
const homeStat = await fs.stat(homeTxr);
|
|
90
|
+
if (homeStat.isDirectory()) return homeDir;
|
|
91
|
+
} catch {
|
|
92
|
+
console.log(`\x1B[90mNo local workspace found. Auto-initializing global txr in ${homeDir}...\x1B[0m`);
|
|
93
|
+
await initTxr(homeDir, { scope: "global" });
|
|
94
|
+
return homeDir;
|
|
95
|
+
}
|
|
96
|
+
return homeDir;
|
|
267
97
|
}
|
|
268
98
|
dir = parent;
|
|
269
99
|
}
|
|
270
100
|
}
|
|
271
101
|
function getTxrPaths(projectRoot) {
|
|
272
|
-
const txrDir =
|
|
102
|
+
const txrDir = path.join(projectRoot, TXR_DIR);
|
|
273
103
|
return {
|
|
274
104
|
txrDir,
|
|
275
|
-
repoDir:
|
|
276
|
-
metadataDir:
|
|
277
|
-
lockFile:
|
|
105
|
+
repoDir: path.join(txrDir, REPO_DIR),
|
|
106
|
+
metadataDir: path.join(txrDir, METADATA_DIR),
|
|
107
|
+
lockFile: path.join(txrDir, "lock")
|
|
278
108
|
};
|
|
279
109
|
}
|
|
280
110
|
async function loadConfig(metadataDir) {
|
|
281
111
|
try {
|
|
282
|
-
const raw = await
|
|
112
|
+
const raw = await fs.readFile(path.join(metadataDir, "config.json"), "utf-8");
|
|
283
113
|
return JSON.parse(raw);
|
|
284
114
|
} catch {
|
|
285
115
|
return DEFAULT_CONFIG;
|
|
@@ -287,8 +117,8 @@ async function loadConfig(metadataDir) {
|
|
|
287
117
|
}
|
|
288
118
|
|
|
289
119
|
// src/core/transaction-store.ts
|
|
290
|
-
import * as
|
|
291
|
-
import * as
|
|
120
|
+
import * as fs2 from "fs/promises";
|
|
121
|
+
import * as path2 from "path";
|
|
292
122
|
var TransactionStore = class _TransactionStore {
|
|
293
123
|
data;
|
|
294
124
|
filePath;
|
|
@@ -298,9 +128,9 @@ var TransactionStore = class _TransactionStore {
|
|
|
298
128
|
}
|
|
299
129
|
/** Load from disk, or create empty store. */
|
|
300
130
|
static async load(metadataDir) {
|
|
301
|
-
const filePath =
|
|
131
|
+
const filePath = path2.join(metadataDir, "transactions.json");
|
|
302
132
|
try {
|
|
303
|
-
const raw = await
|
|
133
|
+
const raw = await fs2.readFile(filePath, "utf-8");
|
|
304
134
|
const data = JSON.parse(raw);
|
|
305
135
|
return new _TransactionStore(filePath, data);
|
|
306
136
|
} catch {
|
|
@@ -316,8 +146,8 @@ var TransactionStore = class _TransactionStore {
|
|
|
316
146
|
}
|
|
317
147
|
async save() {
|
|
318
148
|
const tmp = this.filePath + ".tmp";
|
|
319
|
-
await
|
|
320
|
-
await
|
|
149
|
+
await fs2.writeFile(tmp, JSON.stringify(this.data, null, 2), "utf-8");
|
|
150
|
+
await fs2.rename(tmp, this.filePath);
|
|
321
151
|
}
|
|
322
152
|
/** Generate the next transaction ID (monotonically increasing). */
|
|
323
153
|
nextId() {
|
|
@@ -396,8 +226,8 @@ var TransactionStore = class _TransactionStore {
|
|
|
396
226
|
};
|
|
397
227
|
|
|
398
228
|
// src/core/history-store.ts
|
|
399
|
-
import * as
|
|
400
|
-
import * as
|
|
229
|
+
import * as fs3 from "fs/promises";
|
|
230
|
+
import * as path3 from "path";
|
|
401
231
|
var HistoryStore = class _HistoryStore {
|
|
402
232
|
data;
|
|
403
233
|
filePath;
|
|
@@ -406,9 +236,9 @@ var HistoryStore = class _HistoryStore {
|
|
|
406
236
|
this.data = data;
|
|
407
237
|
}
|
|
408
238
|
static async load(metadataDir) {
|
|
409
|
-
const filePath =
|
|
239
|
+
const filePath = path3.join(metadataDir, "history.json");
|
|
410
240
|
try {
|
|
411
|
-
const raw = await
|
|
241
|
+
const raw = await fs3.readFile(filePath, "utf-8");
|
|
412
242
|
const data = JSON.parse(raw);
|
|
413
243
|
return new _HistoryStore(filePath, data);
|
|
414
244
|
} catch {
|
|
@@ -418,8 +248,8 @@ var HistoryStore = class _HistoryStore {
|
|
|
418
248
|
}
|
|
419
249
|
async save() {
|
|
420
250
|
const tmp = this.filePath + ".tmp";
|
|
421
|
-
await
|
|
422
|
-
await
|
|
251
|
+
await fs3.writeFile(tmp, JSON.stringify(this.data, null, 2), "utf-8");
|
|
252
|
+
await fs3.rename(tmp, this.filePath);
|
|
423
253
|
}
|
|
424
254
|
/** Append a new history entry. */
|
|
425
255
|
append(entry) {
|
|
@@ -440,8 +270,8 @@ var HistoryStore = class _HistoryStore {
|
|
|
440
270
|
};
|
|
441
271
|
|
|
442
272
|
// src/core/file-map.ts
|
|
443
|
-
import * as
|
|
444
|
-
import * as
|
|
273
|
+
import * as fs4 from "fs/promises";
|
|
274
|
+
import * as path4 from "path";
|
|
445
275
|
import { nanoid } from "nanoid";
|
|
446
276
|
var FileMap = class _FileMap {
|
|
447
277
|
data;
|
|
@@ -456,9 +286,9 @@ var FileMap = class _FileMap {
|
|
|
456
286
|
}
|
|
457
287
|
/** Load mapping from disk, or create empty mapping if file doesn't exist. */
|
|
458
288
|
static async load(metadataDir) {
|
|
459
|
-
const filePath =
|
|
289
|
+
const filePath = path4.join(metadataDir, "mapping.json");
|
|
460
290
|
try {
|
|
461
|
-
const raw = await
|
|
291
|
+
const raw = await fs4.readFile(filePath, "utf-8");
|
|
462
292
|
const data = JSON.parse(raw);
|
|
463
293
|
return new _FileMap(filePath, data);
|
|
464
294
|
} catch {
|
|
@@ -469,12 +299,12 @@ var FileMap = class _FileMap {
|
|
|
469
299
|
/** Persist current state to disk atomically. */
|
|
470
300
|
async save() {
|
|
471
301
|
const tmp = this.filePath + ".tmp";
|
|
472
|
-
await
|
|
473
|
-
await
|
|
302
|
+
await fs4.writeFile(tmp, JSON.stringify(this.data, null, 2), "utf-8");
|
|
303
|
+
await fs4.rename(tmp, this.filePath);
|
|
474
304
|
}
|
|
475
305
|
/** Normalize a path for consistent lookup (forward slashes, resolved). */
|
|
476
306
|
static normalizePath(p) {
|
|
477
|
-
return
|
|
307
|
+
return path4.resolve(p).replace(/\\/g, "/");
|
|
478
308
|
}
|
|
479
309
|
/** Get file ID for a path, or undefined if not mapped. Only returns active (non-deleted) entries. */
|
|
480
310
|
getIdByPath(absolutePath) {
|
|
@@ -552,12 +382,12 @@ var FileMap = class _FileMap {
|
|
|
552
382
|
};
|
|
553
383
|
|
|
554
384
|
// src/core/transaction-engine.ts
|
|
555
|
-
import * as
|
|
556
|
-
import * as
|
|
385
|
+
import * as fs6 from "fs/promises";
|
|
386
|
+
import * as path7 from "path";
|
|
557
387
|
|
|
558
388
|
// src/attribution/tier3-hybrid.ts
|
|
559
|
-
import * as
|
|
560
|
-
import * as
|
|
389
|
+
import * as fs5 from "fs/promises";
|
|
390
|
+
import * as path5 from "path";
|
|
561
391
|
import * as crypto from "crypto";
|
|
562
392
|
import { spawn } from "child_process";
|
|
563
393
|
import { minimatch } from "minimatch";
|
|
@@ -572,7 +402,7 @@ var Tier3HybridProvider = class {
|
|
|
572
402
|
"Using scoped filesystem diff (Tier 3). Changes by concurrent processes within watch paths may be mis-attributed."
|
|
573
403
|
];
|
|
574
404
|
const resolvedWatchPaths = options.watchPaths.map(
|
|
575
|
-
(p) =>
|
|
405
|
+
(p) => path5.isAbsolute(p) ? p : path5.resolve(cwd, p)
|
|
576
406
|
);
|
|
577
407
|
const preSnapshot = await this.snapshotPaths(resolvedWatchPaths, options.ignorePaths);
|
|
578
408
|
const exitCode = await this.executeCommand(command, cwd, options);
|
|
@@ -598,36 +428,36 @@ var Tier3HybridProvider = class {
|
|
|
598
428
|
return snapshots;
|
|
599
429
|
}
|
|
600
430
|
async walkAndHash(currentPath, rootPath, ignorePaths, snapshots) {
|
|
601
|
-
let
|
|
431
|
+
let stat3;
|
|
602
432
|
try {
|
|
603
|
-
|
|
433
|
+
stat3 = await fs5.stat(currentPath);
|
|
604
434
|
} catch {
|
|
605
435
|
return;
|
|
606
436
|
}
|
|
607
437
|
const normalized = currentPath.replace(/\\/g, "/");
|
|
608
|
-
const relativePath =
|
|
438
|
+
const relativePath = path5.relative(rootPath, currentPath).replace(/\\/g, "/");
|
|
609
439
|
for (const pattern of ignorePaths) {
|
|
610
|
-
if (minimatch(relativePath, pattern, { dot: true }) || minimatch(
|
|
440
|
+
if (minimatch(relativePath, pattern, { dot: true }) || minimatch(path5.basename(currentPath), pattern, { dot: true })) {
|
|
611
441
|
return;
|
|
612
442
|
}
|
|
613
443
|
}
|
|
614
|
-
if (
|
|
444
|
+
if (stat3.isFile()) {
|
|
615
445
|
const hash = await this.hashFile(currentPath);
|
|
616
446
|
snapshots.set(normalized, {
|
|
617
447
|
path: normalized,
|
|
618
448
|
hash,
|
|
619
|
-
size:
|
|
449
|
+
size: stat3.size
|
|
620
450
|
});
|
|
621
|
-
} else if (
|
|
451
|
+
} else if (stat3.isDirectory()) {
|
|
622
452
|
let entries;
|
|
623
453
|
try {
|
|
624
|
-
entries = await
|
|
454
|
+
entries = await fs5.readdir(currentPath);
|
|
625
455
|
} catch {
|
|
626
456
|
return;
|
|
627
457
|
}
|
|
628
458
|
for (const entry of entries) {
|
|
629
459
|
await this.walkAndHash(
|
|
630
|
-
|
|
460
|
+
path5.join(currentPath, entry),
|
|
631
461
|
rootPath,
|
|
632
462
|
ignorePaths,
|
|
633
463
|
snapshots
|
|
@@ -636,7 +466,7 @@ var Tier3HybridProvider = class {
|
|
|
636
466
|
}
|
|
637
467
|
}
|
|
638
468
|
async hashFile(filePath) {
|
|
639
|
-
const content = await
|
|
469
|
+
const content = await fs5.readFile(filePath);
|
|
640
470
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
641
471
|
}
|
|
642
472
|
/**
|
|
@@ -719,7 +549,7 @@ function getOrderedProviders() {
|
|
|
719
549
|
}
|
|
720
550
|
|
|
721
551
|
// src/classifier/classifier.ts
|
|
722
|
-
import * as
|
|
552
|
+
import * as path6 from "path";
|
|
723
553
|
var DEFAULT_RULES = [
|
|
724
554
|
// ── Containers & Orchestration ──
|
|
725
555
|
{ pattern: /^docker\b/, type: "non-transactional", category: "container", reason: "Container orchestration \u2014 may modify container/image state" },
|
|
@@ -805,18 +635,18 @@ function classifyCommand(command, scope = "workspace", projectRoot, extraRules =
|
|
|
805
635
|
return { type: "transactional" };
|
|
806
636
|
}
|
|
807
637
|
function detectPathEscape(command, projectRoot) {
|
|
808
|
-
const normalizedRoot =
|
|
638
|
+
const normalizedRoot = path6.resolve(projectRoot).replace(/\\/g, "/").toLowerCase();
|
|
809
639
|
const tokens = command.split(/\s+/);
|
|
810
640
|
for (const token of tokens) {
|
|
811
641
|
const cleaned = token.replace(/["']/g, "");
|
|
812
642
|
if (cleaned.startsWith("/") && !cleaned.startsWith("/dev/null")) {
|
|
813
|
-
const normalizedToken =
|
|
643
|
+
const normalizedToken = path6.resolve(cleaned).replace(/\\/g, "/").toLowerCase();
|
|
814
644
|
if (!normalizedToken.startsWith(normalizedRoot)) {
|
|
815
645
|
return `Path "${cleaned}" is outside the workspace root (${projectRoot})`;
|
|
816
646
|
}
|
|
817
647
|
}
|
|
818
648
|
if (/^[A-Za-z]:[\\/]/.test(cleaned)) {
|
|
819
|
-
const normalizedToken =
|
|
649
|
+
const normalizedToken = path6.resolve(cleaned).replace(/\\/g, "/").toLowerCase();
|
|
820
650
|
if (!normalizedToken.startsWith(normalizedRoot)) {
|
|
821
651
|
return `Path "${cleaned}" is outside the workspace root (${projectRoot})`;
|
|
822
652
|
}
|
|
@@ -868,7 +698,7 @@ var TransactionEngine = class {
|
|
|
868
698
|
}
|
|
869
699
|
const provider = await selectProvider(this.config.preferredTier);
|
|
870
700
|
const resolvedWatchPaths = this.config.watchPaths.map(
|
|
871
|
-
(p) =>
|
|
701
|
+
(p) => path7.isAbsolute(p) ? p : path7.resolve(cwd, p)
|
|
872
702
|
);
|
|
873
703
|
const result = await provider.executeAndTrack(command, cwd, {
|
|
874
704
|
watchPaths: resolvedWatchPaths,
|
|
@@ -880,7 +710,7 @@ var TransactionEngine = class {
|
|
|
880
710
|
const scopeWarnings = [];
|
|
881
711
|
let trackedChanges;
|
|
882
712
|
if (this.config.scope === "workspace") {
|
|
883
|
-
const normalizedRoot =
|
|
713
|
+
const normalizedRoot = path7.resolve(this.projectRoot).replace(/\\/g, "/").toLowerCase();
|
|
884
714
|
trackedChanges = [];
|
|
885
715
|
for (const change of result.changes) {
|
|
886
716
|
const normalizedChange = change.absolutePath.replace(/\\/g, "/").toLowerCase();
|
|
@@ -929,7 +759,7 @@ var TransactionEngine = class {
|
|
|
929
759
|
if (change.type === "deleted" /* Deleted */) {
|
|
930
760
|
await this.gitStore.removeFile(fileId);
|
|
931
761
|
} else {
|
|
932
|
-
const content = await
|
|
762
|
+
const content = await fs6.readFile(change.absolutePath);
|
|
933
763
|
await this.gitStore.writeFile(fileId, content);
|
|
934
764
|
}
|
|
935
765
|
}
|
|
@@ -1083,8 +913,8 @@ async function runCommand(command, options) {
|
|
|
1083
913
|
}
|
|
1084
914
|
|
|
1085
915
|
// src/core/undo-engine.ts
|
|
1086
|
-
import * as
|
|
1087
|
-
import * as
|
|
916
|
+
import * as fs7 from "fs/promises";
|
|
917
|
+
import * as path8 from "path";
|
|
1088
918
|
import * as crypto2 from "crypto";
|
|
1089
919
|
var UndoConflictError = class extends Error {
|
|
1090
920
|
conflicts;
|
|
@@ -1127,11 +957,17 @@ var UndoEngine = class {
|
|
|
1127
957
|
ops.push({ type: "delete", path: filePath });
|
|
1128
958
|
break;
|
|
1129
959
|
case "modified": {
|
|
960
|
+
if (!parentTx) {
|
|
961
|
+
throw new Error(`Cannot undo ${headTx.id}: File was modified in the very first transaction and its original state is untracked.`);
|
|
962
|
+
}
|
|
1130
963
|
const prevContent = await this.gitStore.readBlob(parentTx.commit, fileId);
|
|
1131
964
|
ops.push({ type: "write", path: filePath, content: prevContent });
|
|
1132
965
|
break;
|
|
1133
966
|
}
|
|
1134
967
|
case "deleted": {
|
|
968
|
+
if (!parentTx) {
|
|
969
|
+
throw new Error(`Cannot undo ${headTx.id}: File was deleted in the very first transaction and its original state is untracked.`);
|
|
970
|
+
}
|
|
1135
971
|
const restoredContent = await this.gitStore.readBlob(parentTx.commit, fileId);
|
|
1136
972
|
ops.push({ type: "write", path: filePath, content: restoredContent });
|
|
1137
973
|
break;
|
|
@@ -1142,14 +978,14 @@ var UndoEngine = class {
|
|
|
1142
978
|
const deletes = ops.filter((o) => o.type === "delete");
|
|
1143
979
|
for (const op of writes) {
|
|
1144
980
|
if (op.type === "write") {
|
|
1145
|
-
await
|
|
1146
|
-
await
|
|
981
|
+
await fs7.mkdir(path8.dirname(op.path), { recursive: true });
|
|
982
|
+
await fs7.writeFile(op.path, op.content);
|
|
1147
983
|
}
|
|
1148
984
|
}
|
|
1149
985
|
for (const op of deletes) {
|
|
1150
986
|
if (op.type === "delete") {
|
|
1151
987
|
try {
|
|
1152
|
-
await
|
|
988
|
+
await fs7.unlink(op.path);
|
|
1153
989
|
} catch {
|
|
1154
990
|
}
|
|
1155
991
|
await this.removeEmptyParents(op.path);
|
|
@@ -1204,14 +1040,14 @@ var UndoEngine = class {
|
|
|
1204
1040
|
case "created":
|
|
1205
1041
|
case "modified": {
|
|
1206
1042
|
const content = await this.gitStore.readBlob(redoTx.commit, fileId);
|
|
1207
|
-
await
|
|
1208
|
-
await
|
|
1043
|
+
await fs7.mkdir(path8.dirname(filePath), { recursive: true });
|
|
1044
|
+
await fs7.writeFile(filePath, content);
|
|
1209
1045
|
filesChanged++;
|
|
1210
1046
|
break;
|
|
1211
1047
|
}
|
|
1212
1048
|
case "deleted": {
|
|
1213
1049
|
try {
|
|
1214
|
-
await
|
|
1050
|
+
await fs7.unlink(filePath);
|
|
1215
1051
|
} catch {
|
|
1216
1052
|
}
|
|
1217
1053
|
await this.removeEmptyParents(filePath);
|
|
@@ -1270,7 +1106,7 @@ var UndoEngine = class {
|
|
|
1270
1106
|
continue;
|
|
1271
1107
|
}
|
|
1272
1108
|
const expectedContent = await this.gitStore.readBlob(tx.commit, fileId);
|
|
1273
|
-
const actualContent = await
|
|
1109
|
+
const actualContent = await fs7.readFile(filePath);
|
|
1274
1110
|
if (!this.hashEquals(expectedContent, actualContent)) {
|
|
1275
1111
|
const expectedHash = this.hash(expectedContent).substring(0, 12);
|
|
1276
1112
|
const actualHash = this.hash(actualContent).substring(0, 12);
|
|
@@ -1312,7 +1148,7 @@ var UndoEngine = class {
|
|
|
1312
1148
|
if (parentTx) {
|
|
1313
1149
|
try {
|
|
1314
1150
|
const expectedContent = await this.gitStore.readBlob(parentTx.commit, fileId);
|
|
1315
|
-
const actualContent = await
|
|
1151
|
+
const actualContent = await fs7.readFile(filePath);
|
|
1316
1152
|
if (!this.hashEquals(expectedContent, actualContent)) {
|
|
1317
1153
|
conflicts.push(
|
|
1318
1154
|
` \u2717 ${filePath}
|
|
@@ -1327,7 +1163,7 @@ var UndoEngine = class {
|
|
|
1327
1163
|
}
|
|
1328
1164
|
async fileExists(filePath) {
|
|
1329
1165
|
try {
|
|
1330
|
-
await
|
|
1166
|
+
await fs7.access(filePath);
|
|
1331
1167
|
return true;
|
|
1332
1168
|
} catch {
|
|
1333
1169
|
return false;
|
|
@@ -1341,13 +1177,13 @@ var UndoEngine = class {
|
|
|
1341
1177
|
}
|
|
1342
1178
|
/** Remove empty parent directories up to a reasonable depth. */
|
|
1343
1179
|
async removeEmptyParents(filePath) {
|
|
1344
|
-
let dir =
|
|
1180
|
+
let dir = path8.dirname(filePath);
|
|
1345
1181
|
for (let i = 0; i < 10; i++) {
|
|
1346
1182
|
try {
|
|
1347
|
-
const entries = await
|
|
1183
|
+
const entries = await fs7.readdir(dir);
|
|
1348
1184
|
if (entries.length === 0) {
|
|
1349
|
-
await
|
|
1350
|
-
dir =
|
|
1185
|
+
await fs7.rmdir(dir);
|
|
1186
|
+
dir = path8.dirname(dir);
|
|
1351
1187
|
} else {
|
|
1352
1188
|
break;
|
|
1353
1189
|
}
|
|
@@ -1451,6 +1287,88 @@ async function historyCommand(count = 20) {
|
|
|
1451
1287
|
}
|
|
1452
1288
|
}
|
|
1453
1289
|
|
|
1290
|
+
// src/cli/commands/clear.ts
|
|
1291
|
+
import * as fs8 from "fs/promises";
|
|
1292
|
+
import * as path9 from "path";
|
|
1293
|
+
async function clearCommand(options) {
|
|
1294
|
+
const cwd = process.cwd();
|
|
1295
|
+
let projectRoot;
|
|
1296
|
+
try {
|
|
1297
|
+
projectRoot = await findTxrRoot(cwd);
|
|
1298
|
+
} catch (err) {
|
|
1299
|
+
console.error(`\x1B[31mError:\x1B[0m ${err.message}`);
|
|
1300
|
+
process.exit(1);
|
|
1301
|
+
}
|
|
1302
|
+
if (!options.yes) {
|
|
1303
|
+
const readline = await import("readline");
|
|
1304
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1305
|
+
const answer = await new Promise((resolve6) => {
|
|
1306
|
+
console.log();
|
|
1307
|
+
console.log(`\x1B[33mWARNING:\x1B[0m`);
|
|
1308
|
+
console.log(`This will permanently delete all transaction history, undo history,`);
|
|
1309
|
+
console.log(`redo history, mappings, and internal Git state.`);
|
|
1310
|
+
console.log();
|
|
1311
|
+
console.log(`This operation cannot be undone.`);
|
|
1312
|
+
console.log();
|
|
1313
|
+
rl.question("Continue? [y/N] ", resolve6);
|
|
1314
|
+
});
|
|
1315
|
+
rl.close();
|
|
1316
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
1317
|
+
console.log("\x1B[90mOperation cancelled.\x1B[0m");
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
const txrDir = path9.join(projectRoot, ".txr");
|
|
1322
|
+
const repoDir = path9.join(txrDir, "repo");
|
|
1323
|
+
const metadataDir = path9.join(txrDir, "metadata");
|
|
1324
|
+
try {
|
|
1325
|
+
let retries = 3;
|
|
1326
|
+
while (retries > 0) {
|
|
1327
|
+
try {
|
|
1328
|
+
await fs8.rm(repoDir, { recursive: true, force: true });
|
|
1329
|
+
break;
|
|
1330
|
+
} catch (err) {
|
|
1331
|
+
if (err.code === "EBUSY" || err.code === "EPERM") {
|
|
1332
|
+
retries--;
|
|
1333
|
+
if (retries === 0) {
|
|
1334
|
+
throw new Error(`Files are locked by another process. Please close any IDEs or tools accessing the workspace and try again.
|
|
1335
|
+
Details: ${err.message}`);
|
|
1336
|
+
}
|
|
1337
|
+
await new Promise((resolve6) => setTimeout(resolve6, 500));
|
|
1338
|
+
} else {
|
|
1339
|
+
throw err;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
const { GitStore: GitStore2 } = await import("./git-store-XSOIM4KZ.js");
|
|
1344
|
+
const gitStore = new GitStore2(repoDir);
|
|
1345
|
+
await gitStore.init();
|
|
1346
|
+
await fs8.writeFile(
|
|
1347
|
+
path9.join(metadataDir, "transactions.json"),
|
|
1348
|
+
JSON.stringify({ version: 1, head: null, undoStack: [], counter: 0, transactions: {} }, null, 2),
|
|
1349
|
+
"utf-8"
|
|
1350
|
+
);
|
|
1351
|
+
await fs8.writeFile(
|
|
1352
|
+
path9.join(metadataDir, "mapping.json"),
|
|
1353
|
+
JSON.stringify({ version: 1, files: {} }, null, 2),
|
|
1354
|
+
"utf-8"
|
|
1355
|
+
);
|
|
1356
|
+
await fs8.writeFile(
|
|
1357
|
+
path9.join(metadataDir, "history.json"),
|
|
1358
|
+
JSON.stringify({ version: 1, entries: [] }, null, 2),
|
|
1359
|
+
"utf-8"
|
|
1360
|
+
);
|
|
1361
|
+
console.log();
|
|
1362
|
+
console.log(`\x1B[32m\u2713\x1B[0m Internal state cleared. txr is now ready for a fresh transaction.`);
|
|
1363
|
+
} catch (err) {
|
|
1364
|
+
console.error();
|
|
1365
|
+
console.error(`\x1B[31mReset failed.\x1B[0m`);
|
|
1366
|
+
console.error(`To manually recover, delete the \x1B[1m.txr/repo\x1B[0m folder and run 'txr clear' again.`);
|
|
1367
|
+
console.error(`Error details: ${err.message}`);
|
|
1368
|
+
process.exit(1);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1454
1372
|
// src/index.ts
|
|
1455
1373
|
var program = new Command();
|
|
1456
1374
|
program.name("txr").description("Transactional command runner \u2014 undo any shell command").version("0.1.0");
|
|
@@ -1479,7 +1397,7 @@ program.command("run").description("Execute a command with transactional trackin
|
|
|
1479
1397
|
noTransaction: opts.noTransaction
|
|
1480
1398
|
});
|
|
1481
1399
|
} catch (err) {
|
|
1482
|
-
console.error(`\x1B[31mError:\x1B[0m ${err.message}`);
|
|
1400
|
+
console.error(`\x1B[31mError:\x1B[0m ${err.stack || err.message}`);
|
|
1483
1401
|
process.exit(1);
|
|
1484
1402
|
}
|
|
1485
1403
|
});
|
|
@@ -1515,4 +1433,12 @@ program.command("history").description("Show command history").option("-n, --cou
|
|
|
1515
1433
|
process.exit(1);
|
|
1516
1434
|
}
|
|
1517
1435
|
});
|
|
1436
|
+
program.command("clear").description("Reset the internal state and delete all transaction history").option("-y, --yes", "Skip confirmation prompt").action(async (opts) => {
|
|
1437
|
+
try {
|
|
1438
|
+
await clearCommand({ yes: opts.yes });
|
|
1439
|
+
} catch (err) {
|
|
1440
|
+
console.error(`\x1B[31mError:\x1B[0m ${err.message}`);
|
|
1441
|
+
process.exit(1);
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1518
1444
|
program.parse();
|