@wrongstack/plugins 0.291.2 → 0.292.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/duplicate-code-detector/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAoM/C,QAAA,MAAM,MAAM,EAAE,MA8Qb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/duplicate-code-detector/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAiN/C,QAAA,MAAM,MAAM,EAAE,MAySb,CAAC;eAEa,MAAM"}
@@ -1,5 +1,5 @@
1
1
  // src/duplicate-code-detector/index.ts
2
- import { readFileSync } from "node:fs";
2
+ import { readFileSync, statSync as statSync2 } from "node:fs";
3
3
  import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2 } from "node:path";
4
4
 
5
5
  // src/runtime/index.ts
@@ -70,7 +70,8 @@ var state = {
70
70
  warningCount: 0,
71
71
  errorCount: 0,
72
72
  hookUnregister: null,
73
- lastHookWarning: /* @__PURE__ */ new Map()
73
+ lastHookWarning: /* @__PURE__ */ new Map(),
74
+ fileIndex: /* @__PURE__ */ new Map()
74
75
  };
75
76
  var DEFAULTS = {
76
77
  enabled: false,
@@ -218,6 +219,7 @@ var plugin = {
218
219
  state.warningCount = 0;
219
220
  state.errorCount = 0;
220
221
  state.lastHookWarning.clear();
222
+ state.fileIndex.clear();
221
223
  if (state.hookUnregister) {
222
224
  try {
223
225
  state.hookUnregister();
@@ -226,6 +228,27 @@ var plugin = {
226
228
  state.hookUnregister = null;
227
229
  }
228
230
  const cfg = readConfig(api.config.extensions?.["duplicate-code-detector"]);
231
+ function readCachedWindows(filePath, minLines) {
232
+ let st;
233
+ try {
234
+ st = statSync2(filePath);
235
+ } catch {
236
+ return null;
237
+ }
238
+ const cached = state.fileIndex.get(filePath);
239
+ if (cached && cached.mtimeMs === st.mtimeMs && cached.size === st.size) {
240
+ return cached.windows;
241
+ }
242
+ let content;
243
+ try {
244
+ content = readFileSync(filePath, "utf-8");
245
+ } catch {
246
+ return null;
247
+ }
248
+ const windows = extractWindows(filePath, content, minLines);
249
+ state.fileIndex.set(filePath, { mtimeMs: st.mtimeMs, size: st.size, windows });
250
+ return windows;
251
+ }
229
252
  const hook = (input) => {
230
253
  if (!cfg.enabled) return;
231
254
  if (input.toolResult?.isError) return;
@@ -240,33 +263,27 @@ var plugin = {
240
263
  const lastWarning = state.lastHookWarning.get(sourcePath);
241
264
  if (lastWarning !== void 0 && now - lastWarning < 6e4) return;
242
265
  const changedFile = resolve2(process.cwd(), sourcePath);
243
- let content;
244
- try {
245
- content = readFileSync(changedFile, "utf-8");
246
- } catch {
266
+ const changedWindows = readCachedWindows(changedFile, cfg.minLines);
267
+ if (changedWindows === null) {
247
268
  state.errorCount += 1;
248
269
  return;
249
270
  }
250
- const changedWindows = extractWindows(changedFile, content, cfg.minLines);
251
271
  if (changedWindows.length === 0) return;
252
272
  const projectRoot = resolve2(process.cwd());
253
- let otherFiles;
273
+ let otherFilePaths;
254
274
  try {
255
- const filePaths = collectSourceFiles(projectRoot, { extensions: cfg.extensions, excludeDirs: cfg.excludeDirs }).filter((p) => p !== changedFile);
256
- otherFiles = /* @__PURE__ */ new Map();
257
- for (const p of filePaths) {
258
- try {
259
- otherFiles.set(p, readFileSync(p, "utf-8"));
260
- } catch {
261
- }
262
- }
275
+ otherFilePaths = collectSourceFiles(projectRoot, {
276
+ extensions: cfg.extensions,
277
+ excludeDirs: cfg.excludeDirs
278
+ }).filter((p) => p !== changedFile);
263
279
  } catch {
264
280
  state.errorCount += 1;
265
281
  return;
266
282
  }
267
283
  const existingWindows = [];
268
- for (const [p, c] of otherFiles.entries()) {
269
- existingWindows.push(...extractWindows(p, c, cfg.minLines));
284
+ for (const p of otherFilePaths) {
285
+ const w = readCachedWindows(p, cfg.minLines);
286
+ if (w !== null) existingWindows.push(...w);
270
287
  }
271
288
  const hits = [];
272
289
  for (const cw of changedWindows) {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/format-on-save/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AA2N/C,QAAA,MAAM,MAAM,EAAE,MA2Ob,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/format-on-save/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AA0P/C,QAAA,MAAM,MAAM,EAAE,MAuPb,CAAC;eAEa,MAAM"}
@@ -1,6 +1,6 @@
1
1
  // src/format-on-save/index.ts
2
- import { execFileSync, execSync } from "node:child_process";
3
- import { existsSync, statSync } from "node:fs";
2
+ import { execFile } from "node:child_process";
3
+ import { access, stat } from "node:fs/promises";
4
4
 
5
5
  // src/runtime/index.ts
6
6
  import { basename, extname, isAbsolute, relative, resolve } from "node:path";
@@ -78,29 +78,46 @@ function evictExpired(ttlMs) {
78
78
  if (ts < cutoff) recentlyCovered.delete(path);
79
79
  }
80
80
  }
81
- function formatFile(filePath, timeoutMs) {
81
+ async function formatFile(filePath, timeoutMs) {
82
82
  if (!withinProject(filePath)) return null;
83
- if (!existsSync(filePath)) return null;
83
+ try {
84
+ await access(filePath);
85
+ } catch {
86
+ return null;
87
+ }
84
88
  let bytesBefore;
85
89
  try {
86
- bytesBefore = statSync(filePath).size;
90
+ bytesBefore = (await stat(filePath)).size;
87
91
  } catch {
88
92
  return null;
89
93
  }
90
94
  try {
91
- execFileSync("npx", ["biome", "format", "--write", filePath], {
92
- encoding: "utf-8",
93
- timeout: timeoutMs,
94
- cwd: process.cwd(),
95
- stdio: ["pipe", "pipe", "pipe"]
95
+ await new Promise((resolve2, reject) => {
96
+ execFile(
97
+ "npx",
98
+ ["biome", "format", "--write", filePath],
99
+ {
100
+ encoding: "utf-8",
101
+ timeout: timeoutMs,
102
+ cwd: process.cwd(),
103
+ windowsHide: true,
104
+ maxBuffer: 16 * 1024 * 1024
105
+ },
106
+ (err) => {
107
+ if (err) {
108
+ const e = err;
109
+ if (e.killed) return reject(err);
110
+ }
111
+ resolve2();
112
+ }
113
+ );
96
114
  });
97
- } catch (err) {
98
- const e = err;
99
- if (e.killed) return null;
115
+ } catch {
116
+ return null;
100
117
  }
101
118
  let bytesAfter;
102
119
  try {
103
- bytesAfter = statSync(filePath).size;
120
+ bytesAfter = (await stat(filePath)).size;
104
121
  } catch {
105
122
  return null;
106
123
  }
@@ -108,11 +125,22 @@ function formatFile(filePath, timeoutMs) {
108
125
  return { changed: true, bytesBefore, bytesAfter };
109
126
  }
110
127
  try {
111
- execFileSync("npx", ["biome", "format", filePath], {
112
- encoding: "utf-8",
113
- timeout: timeoutMs,
114
- cwd: process.cwd(),
115
- stdio: ["pipe", "pipe", "pipe"]
128
+ await new Promise((resolve2, reject) => {
129
+ execFile(
130
+ "npx",
131
+ ["biome", "format", filePath],
132
+ {
133
+ encoding: "utf-8",
134
+ timeout: timeoutMs,
135
+ cwd: process.cwd(),
136
+ windowsHide: true,
137
+ maxBuffer: 16 * 1024 * 1024
138
+ },
139
+ (err) => {
140
+ if (err) return reject(err);
141
+ resolve2();
142
+ }
143
+ );
116
144
  });
117
145
  return { changed: false, bytesBefore, bytesAfter };
118
146
  } catch {
@@ -153,7 +181,7 @@ var plugin = {
153
181
  }
154
182
  }
155
183
  },
156
- setup(api) {
184
+ async setup(api) {
157
185
  clearRegistrations();
158
186
  state.invocationCount = 0;
159
187
  state.formattedCount = 0;
@@ -165,11 +193,21 @@ var plugin = {
165
193
  const cfg = readConfig(api.config.extensions?.["format-on-save"]);
166
194
  let biomeAvailable = false;
167
195
  try {
168
- execSync("npx biome --version", {
169
- encoding: "utf-8",
170
- timeout: 5e3,
171
- cwd: process.cwd(),
172
- stdio: ["pipe", "pipe", "pipe"]
196
+ await new Promise((resolve2, reject) => {
197
+ execFile(
198
+ "npx",
199
+ ["biome", "--version"],
200
+ {
201
+ encoding: "utf-8",
202
+ timeout: 5e3,
203
+ cwd: process.cwd(),
204
+ windowsHide: true
205
+ },
206
+ (err) => {
207
+ if (err) reject(err);
208
+ else resolve2();
209
+ }
210
+ );
173
211
  });
174
212
  biomeAvailable = true;
175
213
  api.log.info("format-on-save: biome detected");
@@ -177,7 +215,7 @@ var plugin = {
177
215
  biomeAvailable = false;
178
216
  api.log.warn("format-on-save: biome not found \u2014 hook will be a no-op");
179
217
  }
180
- const hook = (input) => {
218
+ const hook = async (input) => {
181
219
  if (!cfg.enabled || !biomeAvailable) return;
182
220
  if (input.toolResult?.isError) return;
183
221
  const toolName = input.toolName ?? "";
@@ -199,7 +237,7 @@ var plugin = {
199
237
  }
200
238
  }
201
239
  state.invocationCount += 1;
202
- const result = formatFile(filePath, cfg.timeoutMs);
240
+ const result = await formatFile(filePath, cfg.timeoutMs);
203
241
  if (!result) {
204
242
  state.errorCount += 1;
205
243
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/git-autocommit/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AA+R/C,QAAA,MAAM,MAAM,EAAE,MAqXb,CAAC;eAEa,MAAM"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/git-autocommit/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAoT/C,QAAA,MAAM,MAAM,EAAE,MAsXb,CAAC;eAEa,MAAM"}
@@ -1,5 +1,5 @@
1
1
  // src/git-autocommit/index.ts
2
- import { execFileSync } from "node:child_process";
2
+ import { execFile } from "node:child_process";
3
3
  import { existsSync } from "node:fs";
4
4
  var API_VERSION = "^0.1.10";
5
5
  var commitCount = { value: 0 };
@@ -7,31 +7,43 @@ var lastCommit = { hash: null, at: null };
7
7
  var llmGenerated = { value: 0 };
8
8
  var DEFAULT_GIT_TIMEOUT_MS = 3e4;
9
9
  var GIT_COMMIT_TIMEOUT_MS = 5 * 6e4;
10
- function runGit(args, cwd, timeoutMs = DEFAULT_GIT_TIMEOUT_MS) {
11
- try {
12
- return execFileSync("git", args, {
13
- encoding: "utf-8",
14
- cwd,
15
- stdio: ["pipe", "pipe", "pipe"],
16
- timeout: timeoutMs,
17
- maxBuffer: 10 * 1024 * 1024,
18
- windowsHide: true
19
- }).trim();
20
- } catch (err) {
21
- const e = err;
22
- throw new Error(`git command failed: ${e.message ?? e.stderr ?? String(err)}`);
23
- }
10
+ async function runGit(args, cwd, timeoutMs = DEFAULT_GIT_TIMEOUT_MS) {
11
+ return await new Promise((resolvePromise, rejectPromise) => {
12
+ execFile(
13
+ "git",
14
+ args,
15
+ {
16
+ encoding: "utf-8",
17
+ cwd,
18
+ windowsHide: true,
19
+ timeout: timeoutMs,
20
+ maxBuffer: 10 * 1024 * 1024
21
+ },
22
+ (err, stdout) => {
23
+ if (err) {
24
+ const e = err;
25
+ rejectPromise(
26
+ new Error(
27
+ `git command failed: ${e.message ?? e.stderr ?? String(err)}`
28
+ )
29
+ );
30
+ return;
31
+ }
32
+ resolvePromise(stdout.trim());
33
+ }
34
+ );
35
+ });
24
36
  }
25
- function getChangedFiles(cwd) {
26
- const output = runGit(["status", "--porcelain"], cwd);
37
+ async function getChangedFiles(cwd) {
38
+ const output = await runGit(["status", "--porcelain"], cwd);
27
39
  if (!output) return [];
28
40
  return output.split("\n").filter((l) => l.trim()).map((l) => l.slice(3).trim());
29
41
  }
30
- function getStagedFiles(cwd) {
31
- const output = runGit(["diff", "--cached", "--name-only"], cwd);
42
+ async function getStagedFiles(cwd) {
43
+ const output = await runGit(["diff", "--cached", "--name-only"], cwd);
32
44
  return output ? output.split("\n").filter(Boolean) : [];
33
45
  }
34
- function stageFiles(files, cwd) {
46
+ async function stageFiles(files, cwd) {
35
47
  if (!files || !Array.isArray(files)) return;
36
48
  const existing = files.filter((f) => {
37
49
  try {
@@ -41,14 +53,14 @@ function stageFiles(files, cwd) {
41
53
  }
42
54
  });
43
55
  if (existing.length === 0) throw new Error("No files exist to stage");
44
- runGit(["add", ...existing], cwd);
56
+ await runGit(["add", ...existing], cwd);
45
57
  }
46
- function commitWithMessage(message, cwd) {
47
- return runGit(["commit", "-m", message], cwd, GIT_COMMIT_TIMEOUT_MS);
58
+ async function commitWithMessage(message, cwd) {
59
+ return await runGit(["commit", "-m", message], cwd, GIT_COMMIT_TIMEOUT_MS);
48
60
  }
49
- function getWorktrees(cwd) {
61
+ async function getWorktrees(cwd) {
50
62
  try {
51
- const out = runGit(["worktree", "list", "--porcelain"], cwd);
63
+ const out = await runGit(["worktree", "list", "--porcelain"], cwd);
52
64
  if (!out) return [];
53
65
  const entries = [];
54
66
  let current = {};
@@ -68,18 +80,18 @@ function getWorktrees(cwd) {
68
80
  return [];
69
81
  }
70
82
  }
71
- function simultaneousEditWarning(cwd) {
72
- const worktrees = getWorktrees(cwd);
83
+ async function simultaneousEditWarning(cwd) {
84
+ const worktrees = await getWorktrees(cwd);
73
85
  if (worktrees.length > 1) {
74
86
  const otherBranches = worktrees.filter((wt) => wt.branch).map((wt) => wt.branch.replace("refs/heads/", ""));
75
87
  return `\u26A0 Simultaneous edits detected: ${worktrees.length} active worktrees (${otherBranches.join(", ")}). Changes from other agents may mix into this commit. Consider using worktree isolation or verifying the diff below before committing.`;
76
88
  }
77
89
  return null;
78
90
  }
79
- function getStagedDiff(cwd) {
91
+ async function getStagedDiff(cwd) {
80
92
  try {
81
- const stat = runGit(["diff", "--cached", "--stat"], cwd);
82
- const diff = runGit(["diff", "--cached"], cwd);
93
+ const stat = await runGit(["diff", "--cached", "--stat"], cwd);
94
+ const diff = await runGit(["diff", "--cached"], cwd);
83
95
  const MAX_DIFF = 2e4;
84
96
  const truncated = diff.length > MAX_DIFF ? diff.slice(0, MAX_DIFF) + "\n\n... (diff truncated)" : diff;
85
97
  return { stat: stat || "(no stat)", diff: truncated || "(clean)" };
@@ -87,9 +99,9 @@ function getStagedDiff(cwd) {
87
99
  return { stat: "(unavailable)", diff: "(unavailable)" };
88
100
  }
89
101
  }
90
- function externalChangesSinceStage(cwd) {
102
+ async function externalChangesSinceStage(cwd) {
91
103
  try {
92
- const out = runGit(["status", "--porcelain"], cwd);
104
+ const out = await runGit(["status", "--porcelain"], cwd);
93
105
  if (!out) return null;
94
106
  const unstaged = out.split("\n").filter((l) => l.trim()).filter((l) => {
95
107
  const idx = l[0] ?? " ";
@@ -264,7 +276,7 @@ var plugin = {
264
276
  }
265
277
  if (files && files.length > 0) {
266
278
  try {
267
- stageFiles(files);
279
+ await stageFiles(files);
268
280
  } catch (err) {
269
281
  return {
270
282
  ok: false,
@@ -274,20 +286,20 @@ var plugin = {
274
286
  }
275
287
  let staged = [];
276
288
  try {
277
- staged = getStagedFiles();
289
+ staged = await getStagedFiles();
278
290
  } catch {
279
291
  staged = [];
280
292
  }
281
293
  if (staged.length === 0) {
282
294
  try {
283
- const changed = getChangedFiles();
295
+ const changed = await getChangedFiles();
284
296
  if (changed.length > 0) {
285
297
  try {
286
- stageFiles(changed);
298
+ await stageFiles(changed);
287
299
  } catch {
288
300
  }
289
301
  try {
290
- staged = getStagedFiles();
302
+ staged = await getStagedFiles();
291
303
  } catch {
292
304
  staged = [];
293
305
  }
@@ -295,7 +307,7 @@ var plugin = {
295
307
  } catch {
296
308
  }
297
309
  }
298
- const { stat, diff: stagedDiff } = getStagedDiff();
310
+ const { stat, diff: stagedDiff } = await getStagedDiff();
299
311
  let generatedByLlm = false;
300
312
  if (wantGenerate && staged.length > 0) {
301
313
  const g = await generateCommitFromDiff(api, stat, stagedDiff);
@@ -341,8 +353,8 @@ var plugin = {
341
353
  error: "Nothing staged. Add files with git add or provide files input."
342
354
  };
343
355
  }
344
- const worktreeWarn = simultaneousEditWarning();
345
- const externalChanges = externalChangesSinceStage();
356
+ const worktreeWarn = await simultaneousEditWarning();
357
+ const externalChanges = await externalChangesSinceStage();
346
358
  let externalWarning = null;
347
359
  if (externalChanges && externalChanges.length > 0) {
348
360
  const preview = externalChanges.slice(0, 10).join(", ");
@@ -369,13 +381,13 @@ ${stagedDiff}
369
381
  let preCommitDiff = stagedDiff;
370
382
  let preCommitStat = stat;
371
383
  if (staged.length === 0) {
372
- const fresh = getStagedDiff();
384
+ const fresh = await getStagedDiff();
373
385
  preCommitDiff = fresh.diff;
374
386
  preCommitStat = fresh.stat;
375
387
  }
376
388
  let hash = "";
377
389
  try {
378
- hash = commitWithMessage(msg);
390
+ hash = await commitWithMessage(msg);
379
391
  } catch (err) {
380
392
  return {
381
393
  ok: false,