opencode-hashline 1.2.0 → 1.3.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,6 +1,6 @@
1
1
  import { Plugin } from '@opencode-ai/plugin';
2
- import { H as HashlineConfig } from './hashline-yhMw1Abs.cjs';
3
- export { a as HashEditInput, b as HashEditOperation, c as HashEditResult, d as HashlineInstance, R as ResolvedRange, V as VerifyHashResult } from './hashline-yhMw1Abs.cjs';
2
+ import { H as HashlineConfig } from './hashline-A7k2yn3G.cjs';
3
+ export { C as CandidateLine, a as HashEditInput, b as HashEditOperation, c as HashEditResult, d as HashlineErrorCode, e as HashlineInstance, R as ResolvedRange, V as VerifyHashResult } from './hashline-A7k2yn3G.cjs';
4
4
 
5
5
  /**
6
6
  * opencode-hashline — Hashline plugin for OpenCode
@@ -1,6 +1,6 @@
1
1
  import { Plugin } from '@opencode-ai/plugin';
2
- import { H as HashlineConfig } from './hashline-yhMw1Abs.js';
3
- export { a as HashEditInput, b as HashEditOperation, c as HashEditResult, d as HashlineInstance, R as ResolvedRange, V as VerifyHashResult } from './hashline-yhMw1Abs.js';
2
+ import { H as HashlineConfig } from './hashline-A7k2yn3G.js';
3
+ export { C as CandidateLine, a as HashEditInput, b as HashEditOperation, c as HashEditResult, d as HashlineErrorCode, e as HashlineInstance, R as ResolvedRange, V as VerifyHashResult } from './hashline-A7k2yn3G.js';
4
4
 
5
5
  /**
6
6
  * opencode-hashline — Hashline plugin for OpenCode
@@ -3,18 +3,20 @@ import {
3
3
  createFileReadAfterHook,
4
4
  createSystemPromptHook,
5
5
  setDebug
6
- } from "./chunk-VPCMHCTB.js";
6
+ } from "./chunk-VSVVWPET.js";
7
7
  import {
8
8
  HashlineCache,
9
+ HashlineError,
9
10
  applyHashEdit,
10
11
  getByteLength,
11
12
  resolveConfig
12
- } from "./chunk-I6RACR3D.js";
13
+ } from "./chunk-GKXY5ZBM.js";
13
14
 
14
15
  // src/index.ts
15
- import { readFileSync as readFileSync2, realpathSync as realpathSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
16
+ import { readFileSync as readFileSync2, realpathSync as realpathSync2, writeFileSync as writeFileSync2, mkdtempSync, openSync, closeSync, rmSync, constants as fsConstants } from "fs";
16
17
  import { join, resolve as resolve2, sep as sep2 } from "path";
17
18
  import { homedir, tmpdir } from "os";
19
+ import { randomBytes } from "crypto";
18
20
  import { fileURLToPath } from "url";
19
21
 
20
22
  // src/hashline-tool.ts
@@ -29,10 +31,12 @@ function createHashlineEditTool(config, cache) {
29
31
  operation: z.enum(["replace", "delete", "insert_before", "insert_after"]).describe("Edit operation"),
30
32
  startRef: z.string().describe('Start hash reference, e.g. "5:a3f" or "#HL 5:a3f|const x = 1;"'),
31
33
  endRef: z.string().optional().describe("End hash reference for range operations. Defaults to startRef when omitted."),
32
- replacement: z.string().max(1e7).optional().describe("Replacement/inserted content. Required for replace/insert operations.")
34
+ replacement: z.string().max(1e7).optional().describe("Replacement/inserted content. Required for replace/insert operations."),
35
+ fileRev: z.string().optional().describe("File revision hash (8-char hex from #HL REV:<hash>). When provided, verifies the file hasn't changed before editing."),
36
+ safeReapply: z.boolean().optional().describe("Enable safe reapply: if a line moved, attempt to find it by content hash. Fails on ambiguous matches.")
33
37
  },
34
38
  async execute(args, context) {
35
- const { path, operation, startRef, endRef, replacement } = args;
39
+ const { path, operation, startRef, endRef, replacement, fileRev, safeReapply } = args;
36
40
  const absPath = isAbsolute(path) ? path : resolve(context.directory, path);
37
41
  const realDirectory = realpathSync(resolve(context.directory));
38
42
  const realWorktree = realpathSync(resolve(context.worktree));
@@ -86,15 +90,21 @@ function createHashlineEditTool(config, cache) {
86
90
  operation,
87
91
  startRef,
88
92
  endRef,
89
- replacement
93
+ replacement,
94
+ fileRev
90
95
  },
91
96
  current,
92
- config.hashLength || void 0
97
+ config.hashLength || void 0,
98
+ safeReapply ?? config.safeReapply
93
99
  );
94
100
  nextContent = result.content;
95
101
  startLine = result.startLine;
96
102
  endLine = result.endLine;
97
103
  } catch (error) {
104
+ if (error instanceof HashlineError) {
105
+ throw new Error(`Hashline edit failed for "${displayPath}":
106
+ ${error.toDiagnostic()}`);
107
+ }
98
108
  const reason = error instanceof Error ? error.message : String(error);
99
109
  throw new Error(`Hashline edit failed for "${displayPath}": ${reason}`);
100
110
  }
@@ -131,6 +141,33 @@ function createHashlineEditTool(config, cache) {
131
141
 
132
142
  // src/index.ts
133
143
  var CONFIG_FILENAME = "opencode-hashline.json";
144
+ var tempDirs = /* @__PURE__ */ new Set();
145
+ var exitListenerRegistered = false;
146
+ function registerTempDir(dir) {
147
+ tempDirs.add(dir);
148
+ if (!exitListenerRegistered) {
149
+ exitListenerRegistered = true;
150
+ process.on("exit", () => {
151
+ for (const d of tempDirs) {
152
+ try {
153
+ rmSync(d, { recursive: true, force: true });
154
+ } catch {
155
+ }
156
+ }
157
+ });
158
+ }
159
+ }
160
+ function writeTempFile(tempDir, content) {
161
+ const name = `hl-${randomBytes(16).toString("hex")}.txt`;
162
+ const tmpPath = join(tempDir, name);
163
+ const fd = openSync(tmpPath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 384);
164
+ try {
165
+ writeFileSync2(fd, content, "utf-8");
166
+ } finally {
167
+ closeSync(fd);
168
+ }
169
+ return tmpPath;
170
+ }
134
171
  function sanitizeConfig(raw) {
135
172
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
136
173
  const r = raw;
@@ -159,6 +196,12 @@ function sanitizeConfig(raw) {
159
196
  if (typeof r.debug === "boolean") {
160
197
  result.debug = r.debug;
161
198
  }
199
+ if (typeof r.fileRev === "boolean") {
200
+ result.fileRev = r.fileRev;
201
+ }
202
+ if (typeof r.safeReapply === "boolean") {
203
+ result.safeReapply = r.safeReapply;
204
+ }
162
205
  return result;
163
206
  }
164
207
  function loadConfigFile(filePath) {
@@ -199,17 +242,8 @@ function createHashlinePlugin(userConfig) {
199
242
  } catch {
200
243
  }
201
244
  }
202
- const tempFiles = /* @__PURE__ */ new Set();
203
- const cleanupTempFiles = () => {
204
- for (const f of tempFiles) {
205
- try {
206
- unlinkSync(f);
207
- } catch {
208
- }
209
- }
210
- tempFiles.clear();
211
- };
212
- process.on("exit", cleanupTempFiles);
245
+ const instanceTmpDir = mkdtempSync(join(tmpdir(), "hashline-"));
246
+ registerTempDir(instanceTmpDir);
213
247
  return {
214
248
  tool: {
215
249
  hashline_edit: createHashlineEditTool(config, cache)
@@ -222,7 +256,7 @@ function createHashlinePlugin(userConfig) {
222
256
  const out = output;
223
257
  const hashLen = config.hashLength || 0;
224
258
  const prefix = config.prefix;
225
- const { formatFileWithHashes, shouldExclude, getByteLength: getByteLength2 } = await import("./hashline-5PFAXY3H.js");
259
+ const { formatFileWithHashes, shouldExclude, getByteLength: getByteLength2 } = await import("./hashline-37RYBX5A.js");
226
260
  for (const p of out.parts ?? []) {
227
261
  if (p.type !== "file") continue;
228
262
  if (!p.url || !p.mime?.startsWith("text/")) continue;
@@ -252,9 +286,7 @@ function createHashlinePlugin(userConfig) {
252
286
  if (config.maxFileSize > 0 && getByteLength2(content) > config.maxFileSize) continue;
253
287
  const cached = cache.get(filePath, content);
254
288
  if (cached) {
255
- const tmpPath2 = join(tmpdir(), `hashline-${p.id}.txt`);
256
- writeFileSync2(tmpPath2, cached, "utf-8");
257
- tempFiles.add(tmpPath2);
289
+ const tmpPath2 = writeTempFile(instanceTmpDir, cached);
258
290
  p.url = `file://${tmpPath2}`;
259
291
  if (config.debug) {
260
292
  try {
@@ -265,10 +297,9 @@ function createHashlinePlugin(userConfig) {
265
297
  }
266
298
  continue;
267
299
  }
268
- const annotated = formatFileWithHashes(content, hashLen || void 0, prefix);
300
+ const annotated = formatFileWithHashes(content, hashLen || void 0, prefix, config.fileRev);
269
301
  cache.set(filePath, content, annotated);
270
- const tmpPath = join(tmpdir(), `hashline-${p.id}.txt`);
271
- writeFileSync2(tmpPath, annotated, "utf-8");
302
+ const tmpPath = writeTempFile(instanceTmpDir, annotated);
272
303
  p.url = `file://${tmpPath}`;
273
304
  if (config.debug) {
274
305
  try {