@amoghvj/txr 0.1.0 → 0.1.1

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.
@@ -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
+ };
@@ -0,0 +1,6 @@
1
+ import {
2
+ GitStore
3
+ } from "./chunk-XKFFIQXW.js";
4
+ export {
5
+ GitStore
6
+ };
package/dist/index.js CHANGED
@@ -1,194 +1,16 @@
1
1
  #!/usr/bin/env node
2
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
- }) : x)(function(x) {
5
- if (typeof require !== "undefined") return require.apply(this, arguments);
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 git from "isomorphic-git";
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 = path2.join(projectRoot, TXR_DIR);
220
- const repoDir = path2.join(txrDir, REPO_DIR);
221
- const metadataDir = path2.join(txrDir, METADATA_DIR);
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 fs2.access(txrDir);
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 fs2.mkdir(metadataDir, { recursive: true });
50
+ await fs.mkdir(metadataDir, { recursive: true });
229
51
  const gitStore = new GitStore(repoDir);
230
52
  await gitStore.init();
231
- await fs2.writeFile(
232
- path2.join(metadataDir, "mapping.json"),
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 fs2.writeFile(
237
- path2.join(metadataDir, "transactions.json"),
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 fs2.writeFile(
242
- path2.join(metadataDir, "history.json"),
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 fs2.writeFile(
248
- path2.join(metadataDir, "config.json"),
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 = path2.resolve(startDir);
76
+ let dir = path.resolve(startDir);
255
77
  while (true) {
256
- const candidate = path2.join(dir, TXR_DIR);
78
+ const candidate = path.join(dir, TXR_DIR);
257
79
  try {
258
- const stat4 = await fs2.stat(candidate);
259
- if (stat4.isDirectory()) return dir;
80
+ const stat3 = await fs.stat(candidate);
81
+ if (stat3.isDirectory()) return dir;
260
82
  } catch {
261
83
  }
262
- const parent = path2.dirname(dir);
84
+ const parent = path.dirname(dir);
263
85
  if (parent === dir) {
264
- throw new Error(
265
- "Not a txr project (or any parent). Run `txr init` first."
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 = path2.join(projectRoot, TXR_DIR);
102
+ const txrDir = path.join(projectRoot, TXR_DIR);
273
103
  return {
274
104
  txrDir,
275
- repoDir: path2.join(txrDir, REPO_DIR),
276
- metadataDir: path2.join(txrDir, METADATA_DIR),
277
- lockFile: path2.join(txrDir, "lock")
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 fs2.readFile(path2.join(metadataDir, "config.json"), "utf-8");
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 fs3 from "fs/promises";
291
- import * as path3 from "path";
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 = path3.join(metadataDir, "transactions.json");
131
+ const filePath = path2.join(metadataDir, "transactions.json");
302
132
  try {
303
- const raw = await fs3.readFile(filePath, "utf-8");
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 fs3.writeFile(tmp, JSON.stringify(this.data, null, 2), "utf-8");
320
- await fs3.rename(tmp, this.filePath);
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 fs4 from "fs/promises";
400
- import * as path4 from "path";
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 = path4.join(metadataDir, "history.json");
239
+ const filePath = path3.join(metadataDir, "history.json");
410
240
  try {
411
- const raw = await fs4.readFile(filePath, "utf-8");
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 fs4.writeFile(tmp, JSON.stringify(this.data, null, 2), "utf-8");
422
- await fs4.rename(tmp, this.filePath);
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 fs5 from "fs/promises";
444
- import * as path5 from "path";
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 = path5.join(metadataDir, "mapping.json");
289
+ const filePath = path4.join(metadataDir, "mapping.json");
460
290
  try {
461
- const raw = await fs5.readFile(filePath, "utf-8");
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 fs5.writeFile(tmp, JSON.stringify(this.data, null, 2), "utf-8");
473
- await fs5.rename(tmp, this.filePath);
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 path5.resolve(p).replace(/\\/g, "/");
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 fs7 from "fs/promises";
556
- import * as path8 from "path";
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 fs6 from "fs/promises";
560
- import * as path6 from "path";
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) => path6.isAbsolute(p) ? p : path6.resolve(cwd, 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 stat4;
431
+ let stat3;
602
432
  try {
603
- stat4 = await fs6.stat(currentPath);
433
+ stat3 = await fs5.stat(currentPath);
604
434
  } catch {
605
435
  return;
606
436
  }
607
437
  const normalized = currentPath.replace(/\\/g, "/");
608
- const relativePath = path6.relative(rootPath, currentPath).replace(/\\/g, "/");
438
+ const relativePath = path5.relative(rootPath, currentPath).replace(/\\/g, "/");
609
439
  for (const pattern of ignorePaths) {
610
- if (minimatch(relativePath, pattern, { dot: true }) || minimatch(path6.basename(currentPath), pattern, { dot: true })) {
440
+ if (minimatch(relativePath, pattern, { dot: true }) || minimatch(path5.basename(currentPath), pattern, { dot: true })) {
611
441
  return;
612
442
  }
613
443
  }
614
- if (stat4.isFile()) {
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: stat4.size
449
+ size: stat3.size
620
450
  });
621
- } else if (stat4.isDirectory()) {
451
+ } else if (stat3.isDirectory()) {
622
452
  let entries;
623
453
  try {
624
- entries = await fs6.readdir(currentPath);
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
- path6.join(currentPath, entry),
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 fs6.readFile(filePath);
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 path7 from "path";
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 = path7.resolve(projectRoot).replace(/\\/g, "/").toLowerCase();
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 = path7.resolve(cleaned).replace(/\\/g, "/").toLowerCase();
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 = path7.resolve(cleaned).replace(/\\/g, "/").toLowerCase();
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) => path8.isAbsolute(p) ? p : path8.resolve(cwd, 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 = path8.resolve(this.projectRoot).replace(/\\/g, "/").toLowerCase();
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 fs7.readFile(change.absolutePath);
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 fs8 from "fs/promises";
1087
- import * as path9 from "path";
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 fs8.mkdir(path9.dirname(op.path), { recursive: true });
1146
- await fs8.writeFile(op.path, op.content);
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 fs8.unlink(op.path);
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 fs8.mkdir(path9.dirname(filePath), { recursive: true });
1208
- await fs8.writeFile(filePath, content);
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 fs8.unlink(filePath);
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 fs8.readFile(filePath);
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 fs8.readFile(filePath);
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 fs8.access(filePath);
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 = path9.dirname(filePath);
1180
+ let dir = path8.dirname(filePath);
1345
1181
  for (let i = 0; i < 10; i++) {
1346
1182
  try {
1347
- const entries = await fs8.readdir(dir);
1183
+ const entries = await fs7.readdir(dir);
1348
1184
  if (entries.length === 0) {
1349
- await fs8.rmdir(dir);
1350
- dir = path9.dirname(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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amoghvj/txr",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Transactional command runner — undo any shell command",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",