opencode-hashline 1.1.1 → 1.1.3
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/dist/{chunk-DDXOFWTU.js → chunk-I6RACR3D.js} +64 -18
- package/dist/{chunk-JOA7B5LK.js → chunk-VPCMHCTB.js} +1 -1
- package/dist/{hashline-GY4XM34F.js → hashline-5PFAXY3H.js} +3 -1
- package/dist/opencode-hashline.cjs +122 -27
- package/dist/opencode-hashline.js +63 -13
- package/dist/utils.cjs +63 -18
- package/dist/utils.js +2 -2
- package/package.json +1 -1
|
@@ -28,7 +28,24 @@ var DEFAULT_EXCLUDE_PATTERNS = [
|
|
|
28
28
|
"**/*.exe",
|
|
29
29
|
"**/*.dll",
|
|
30
30
|
"**/*.so",
|
|
31
|
-
"**/*.dylib"
|
|
31
|
+
"**/*.dylib",
|
|
32
|
+
// Sensitive credential and secret files
|
|
33
|
+
"**/.env",
|
|
34
|
+
"**/.env.*",
|
|
35
|
+
"**/*.pem",
|
|
36
|
+
"**/*.key",
|
|
37
|
+
"**/*.p12",
|
|
38
|
+
"**/*.pfx",
|
|
39
|
+
"**/id_rsa",
|
|
40
|
+
"**/id_rsa.pub",
|
|
41
|
+
"**/id_ed25519",
|
|
42
|
+
"**/id_ed25519.pub",
|
|
43
|
+
"**/id_ecdsa",
|
|
44
|
+
"**/id_ecdsa.pub",
|
|
45
|
+
"**/.npmrc",
|
|
46
|
+
"**/.netrc",
|
|
47
|
+
"**/credentials",
|
|
48
|
+
"**/credentials.json"
|
|
32
49
|
];
|
|
33
50
|
var DEFAULT_PREFIX = "#HL ";
|
|
34
51
|
var DEFAULT_CONFIG = {
|
|
@@ -88,16 +105,24 @@ function computeLineHash(idx, line, hashLen = 3) {
|
|
|
88
105
|
return hash.toString(16).padStart(hashLen, "0");
|
|
89
106
|
}
|
|
90
107
|
function formatFileWithHashes(content, hashLen, prefix) {
|
|
91
|
-
const
|
|
108
|
+
const normalized = content.includes("\r\n") ? content.replace(/\r\n/g, "\n") : content;
|
|
109
|
+
const lines = normalized.split("\n");
|
|
92
110
|
const effectiveLen = hashLen && hashLen >= 3 ? hashLen : getAdaptiveHashLength(lines.length);
|
|
93
111
|
const effectivePrefix = prefix === void 0 ? DEFAULT_PREFIX : prefix === false ? "" : prefix;
|
|
94
112
|
const hashes = new Array(lines.length);
|
|
95
113
|
const seen = /* @__PURE__ */ new Map();
|
|
114
|
+
const upgraded = /* @__PURE__ */ new Set();
|
|
96
115
|
for (let idx = 0; idx < lines.length; idx++) {
|
|
97
116
|
const hash = computeLineHash(idx, lines[idx], effectiveLen);
|
|
98
117
|
if (seen.has(hash)) {
|
|
99
118
|
const longerLen = Math.min(effectiveLen + 1, 8);
|
|
119
|
+
const prevIdx = seen.get(hash);
|
|
120
|
+
if (!upgraded.has(prevIdx)) {
|
|
121
|
+
hashes[prevIdx] = computeLineHash(prevIdx, lines[prevIdx], longerLen);
|
|
122
|
+
upgraded.add(prevIdx);
|
|
123
|
+
}
|
|
100
124
|
hashes[idx] = computeLineHash(idx, lines[idx], longerLen);
|
|
125
|
+
upgraded.add(idx);
|
|
101
126
|
} else {
|
|
102
127
|
seen.set(hash, idx);
|
|
103
128
|
hashes[idx] = hash;
|
|
@@ -116,7 +141,9 @@ function stripHashes(content, prefix) {
|
|
|
116
141
|
hashLinePattern = new RegExp(`^([+ \\-])?${escapedPrefix}\\d+:[0-9a-f]{2,8}\\|`);
|
|
117
142
|
stripRegexCache.set(escapedPrefix, hashLinePattern);
|
|
118
143
|
}
|
|
119
|
-
|
|
144
|
+
const lineEnding = detectLineEnding(content);
|
|
145
|
+
const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
|
|
146
|
+
const result = normalized.split("\n").map((line) => {
|
|
120
147
|
const match = line.match(hashLinePattern);
|
|
121
148
|
if (match) {
|
|
122
149
|
const patchMarker = match[1] || "";
|
|
@@ -124,11 +151,13 @@ function stripHashes(content, prefix) {
|
|
|
124
151
|
}
|
|
125
152
|
return line;
|
|
126
153
|
}).join("\n");
|
|
154
|
+
return lineEnding === "\r\n" ? result.replace(/\n/g, "\r\n") : result;
|
|
127
155
|
}
|
|
128
156
|
function parseHashRef(ref) {
|
|
129
157
|
const match = ref.match(/^(\d+):([0-9a-f]{2,8})$/);
|
|
130
158
|
if (!match) {
|
|
131
|
-
|
|
159
|
+
const display = ref.length > 100 ? `${ref.slice(0, 100)}\u2026` : ref;
|
|
160
|
+
throw new Error(`Invalid hash reference: "${display}". Expected format: "<line>:<2-8 char hex>"`);
|
|
132
161
|
}
|
|
133
162
|
return {
|
|
134
163
|
line: parseInt(match[1], 10),
|
|
@@ -145,8 +174,9 @@ function normalizeHashRef(ref) {
|
|
|
145
174
|
if (annotated) {
|
|
146
175
|
return `${parseInt(annotated[1], 10)}:${annotated[2].toLowerCase()}`;
|
|
147
176
|
}
|
|
177
|
+
const display = ref.length > 100 ? `${ref.slice(0, 100)}\u2026` : ref;
|
|
148
178
|
throw new Error(
|
|
149
|
-
`Invalid hash reference: "${
|
|
179
|
+
`Invalid hash reference: "${display}". Expected "<line>:<hash>" or an annotated line like "#HL <line>:<hash>|..."`
|
|
150
180
|
);
|
|
151
181
|
}
|
|
152
182
|
function buildHashMap(content, hashLen) {
|
|
@@ -189,12 +219,14 @@ function resolveRange(startRef, endRef, content, hashLen) {
|
|
|
189
219
|
`Invalid range: start line ${start.line} is after end line ${end.line}`
|
|
190
220
|
);
|
|
191
221
|
}
|
|
192
|
-
const
|
|
193
|
-
const
|
|
222
|
+
const lineEnding = detectLineEnding(content);
|
|
223
|
+
const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
|
|
224
|
+
const lines = normalized.split("\n");
|
|
225
|
+
const startVerify = verifyHash(start.line, start.hash, normalized, hashLen, lines);
|
|
194
226
|
if (!startVerify.valid) {
|
|
195
227
|
throw new Error(`Start reference invalid: ${startVerify.message}`);
|
|
196
228
|
}
|
|
197
|
-
const endVerify = verifyHash(end.line, end.hash,
|
|
229
|
+
const endVerify = verifyHash(end.line, end.hash, normalized, hashLen, lines);
|
|
198
230
|
if (!endVerify.valid) {
|
|
199
231
|
throw new Error(`End reference invalid: ${endVerify.message}`);
|
|
200
232
|
}
|
|
@@ -203,22 +235,27 @@ function resolveRange(startRef, endRef, content, hashLen) {
|
|
|
203
235
|
startLine: start.line,
|
|
204
236
|
endLine: end.line,
|
|
205
237
|
lines: rangeLines,
|
|
206
|
-
content: rangeLines.join(
|
|
238
|
+
content: rangeLines.join(lineEnding)
|
|
207
239
|
};
|
|
208
240
|
}
|
|
209
241
|
function replaceRange(startRef, endRef, content, replacement, hashLen) {
|
|
210
|
-
const
|
|
211
|
-
const
|
|
242
|
+
const lineEnding = detectLineEnding(content);
|
|
243
|
+
const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
|
|
244
|
+
const range = resolveRange(startRef, endRef, normalized, hashLen);
|
|
245
|
+
const lines = normalized.split("\n");
|
|
212
246
|
const before = lines.slice(0, range.startLine - 1);
|
|
213
247
|
const after = lines.slice(range.endLine);
|
|
214
248
|
const replacementLines = replacement.split("\n");
|
|
215
|
-
|
|
249
|
+
const result = [...before, ...replacementLines, ...after].join("\n");
|
|
250
|
+
return lineEnding === "\r\n" ? result.replace(/\n/g, "\r\n") : result;
|
|
216
251
|
}
|
|
217
252
|
function applyHashEdit(input, content, hashLen) {
|
|
253
|
+
const lineEnding = detectLineEnding(content);
|
|
254
|
+
const workContent = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
|
|
218
255
|
const normalizedStart = normalizeHashRef(input.startRef);
|
|
219
256
|
const start = parseHashRef(normalizedStart);
|
|
220
|
-
const lines =
|
|
221
|
-
const startVerify = verifyHash(start.line, start.hash,
|
|
257
|
+
const lines = workContent.split("\n");
|
|
258
|
+
const startVerify = verifyHash(start.line, start.hash, workContent, hashLen, lines);
|
|
222
259
|
if (!startVerify.valid) {
|
|
223
260
|
throw new Error(`Start reference invalid: ${startVerify.message}`);
|
|
224
261
|
}
|
|
@@ -237,7 +274,7 @@ function applyHashEdit(input, content, hashLen) {
|
|
|
237
274
|
operation: input.operation,
|
|
238
275
|
startLine: start.line,
|
|
239
276
|
endLine: start.line,
|
|
240
|
-
content: next2
|
|
277
|
+
content: lineEnding === "\r\n" ? next2.replace(/\n/g, "\r\n") : next2
|
|
241
278
|
};
|
|
242
279
|
}
|
|
243
280
|
const normalizedEnd = normalizeHashRef(input.endRef ?? input.startRef);
|
|
@@ -247,7 +284,7 @@ function applyHashEdit(input, content, hashLen) {
|
|
|
247
284
|
`Invalid range: start line ${start.line} is after end line ${end.line}`
|
|
248
285
|
);
|
|
249
286
|
}
|
|
250
|
-
const endVerify = verifyHash(end.line, end.hash,
|
|
287
|
+
const endVerify = verifyHash(end.line, end.hash, workContent, hashLen, lines);
|
|
251
288
|
if (!endVerify.valid) {
|
|
252
289
|
throw new Error(`End reference invalid: ${endVerify.message}`);
|
|
253
290
|
}
|
|
@@ -263,7 +300,7 @@ function applyHashEdit(input, content, hashLen) {
|
|
|
263
300
|
operation: input.operation,
|
|
264
301
|
startLine: start.line,
|
|
265
302
|
endLine: end.line,
|
|
266
|
-
content: next
|
|
303
|
+
content: lineEnding === "\r\n" ? next.replace(/\n/g, "\r\n") : next
|
|
267
304
|
};
|
|
268
305
|
}
|
|
269
306
|
var HashlineCache = class {
|
|
@@ -325,10 +362,15 @@ var HashlineCache = class {
|
|
|
325
362
|
return this.cache.size;
|
|
326
363
|
}
|
|
327
364
|
};
|
|
365
|
+
var globMatcherCache = /* @__PURE__ */ new Map();
|
|
328
366
|
function matchesGlob(filePath, pattern) {
|
|
329
367
|
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
330
368
|
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
331
|
-
|
|
369
|
+
let isMatch = globMatcherCache.get(normalizedPattern);
|
|
370
|
+
if (!isMatch) {
|
|
371
|
+
isMatch = picomatch(normalizedPattern, { dot: true });
|
|
372
|
+
globMatcherCache.set(normalizedPattern, isMatch);
|
|
373
|
+
}
|
|
332
374
|
return isMatch(normalizedPath);
|
|
333
375
|
}
|
|
334
376
|
function shouldExclude(filePath, patterns) {
|
|
@@ -338,6 +380,9 @@ var textEncoder = new TextEncoder();
|
|
|
338
380
|
function getByteLength(content) {
|
|
339
381
|
return textEncoder.encode(content).length;
|
|
340
382
|
}
|
|
383
|
+
function detectLineEnding(content) {
|
|
384
|
+
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
385
|
+
}
|
|
341
386
|
function createHashline(config) {
|
|
342
387
|
const resolved = resolveConfig(config);
|
|
343
388
|
const cache = new HashlineCache(resolved.cacheSize);
|
|
@@ -410,5 +455,6 @@ export {
|
|
|
410
455
|
matchesGlob,
|
|
411
456
|
shouldExclude,
|
|
412
457
|
getByteLength,
|
|
458
|
+
detectLineEnding,
|
|
413
459
|
createHashline
|
|
414
460
|
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
buildHashMap,
|
|
8
8
|
computeLineHash,
|
|
9
9
|
createHashline,
|
|
10
|
+
detectLineEnding,
|
|
10
11
|
formatFileWithHashes,
|
|
11
12
|
getAdaptiveHashLength,
|
|
12
13
|
getByteLength,
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
shouldExclude,
|
|
20
21
|
stripHashes,
|
|
21
22
|
verifyHash
|
|
22
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-I6RACR3D.js";
|
|
23
24
|
export {
|
|
24
25
|
DEFAULT_CONFIG,
|
|
25
26
|
DEFAULT_EXCLUDE_PATTERNS,
|
|
@@ -29,6 +30,7 @@ export {
|
|
|
29
30
|
buildHashMap,
|
|
30
31
|
computeLineHash,
|
|
31
32
|
createHashline,
|
|
33
|
+
detectLineEnding,
|
|
32
34
|
formatFileWithHashes,
|
|
33
35
|
getAdaptiveHashLength,
|
|
34
36
|
getByteLength,
|
|
@@ -41,6 +41,7 @@ __export(hashline_exports, {
|
|
|
41
41
|
buildHashMap: () => buildHashMap,
|
|
42
42
|
computeLineHash: () => computeLineHash,
|
|
43
43
|
createHashline: () => createHashline,
|
|
44
|
+
detectLineEnding: () => detectLineEnding,
|
|
44
45
|
formatFileWithHashes: () => formatFileWithHashes,
|
|
45
46
|
getAdaptiveHashLength: () => getAdaptiveHashLength,
|
|
46
47
|
getByteLength: () => getByteLength,
|
|
@@ -100,16 +101,24 @@ function computeLineHash(idx, line, hashLen = 3) {
|
|
|
100
101
|
return hash.toString(16).padStart(hashLen, "0");
|
|
101
102
|
}
|
|
102
103
|
function formatFileWithHashes(content, hashLen, prefix) {
|
|
103
|
-
const
|
|
104
|
+
const normalized = content.includes("\r\n") ? content.replace(/\r\n/g, "\n") : content;
|
|
105
|
+
const lines = normalized.split("\n");
|
|
104
106
|
const effectiveLen = hashLen && hashLen >= 3 ? hashLen : getAdaptiveHashLength(lines.length);
|
|
105
107
|
const effectivePrefix = prefix === void 0 ? DEFAULT_PREFIX : prefix === false ? "" : prefix;
|
|
106
108
|
const hashes = new Array(lines.length);
|
|
107
109
|
const seen = /* @__PURE__ */ new Map();
|
|
110
|
+
const upgraded = /* @__PURE__ */ new Set();
|
|
108
111
|
for (let idx = 0; idx < lines.length; idx++) {
|
|
109
112
|
const hash = computeLineHash(idx, lines[idx], effectiveLen);
|
|
110
113
|
if (seen.has(hash)) {
|
|
111
114
|
const longerLen = Math.min(effectiveLen + 1, 8);
|
|
115
|
+
const prevIdx = seen.get(hash);
|
|
116
|
+
if (!upgraded.has(prevIdx)) {
|
|
117
|
+
hashes[prevIdx] = computeLineHash(prevIdx, lines[prevIdx], longerLen);
|
|
118
|
+
upgraded.add(prevIdx);
|
|
119
|
+
}
|
|
112
120
|
hashes[idx] = computeLineHash(idx, lines[idx], longerLen);
|
|
121
|
+
upgraded.add(idx);
|
|
113
122
|
} else {
|
|
114
123
|
seen.set(hash, idx);
|
|
115
124
|
hashes[idx] = hash;
|
|
@@ -127,7 +136,9 @@ function stripHashes(content, prefix) {
|
|
|
127
136
|
hashLinePattern = new RegExp(`^([+ \\-])?${escapedPrefix}\\d+:[0-9a-f]{2,8}\\|`);
|
|
128
137
|
stripRegexCache.set(escapedPrefix, hashLinePattern);
|
|
129
138
|
}
|
|
130
|
-
|
|
139
|
+
const lineEnding = detectLineEnding(content);
|
|
140
|
+
const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
|
|
141
|
+
const result = normalized.split("\n").map((line) => {
|
|
131
142
|
const match = line.match(hashLinePattern);
|
|
132
143
|
if (match) {
|
|
133
144
|
const patchMarker = match[1] || "";
|
|
@@ -135,11 +146,13 @@ function stripHashes(content, prefix) {
|
|
|
135
146
|
}
|
|
136
147
|
return line;
|
|
137
148
|
}).join("\n");
|
|
149
|
+
return lineEnding === "\r\n" ? result.replace(/\n/g, "\r\n") : result;
|
|
138
150
|
}
|
|
139
151
|
function parseHashRef(ref) {
|
|
140
152
|
const match = ref.match(/^(\d+):([0-9a-f]{2,8})$/);
|
|
141
153
|
if (!match) {
|
|
142
|
-
|
|
154
|
+
const display = ref.length > 100 ? `${ref.slice(0, 100)}\u2026` : ref;
|
|
155
|
+
throw new Error(`Invalid hash reference: "${display}". Expected format: "<line>:<2-8 char hex>"`);
|
|
143
156
|
}
|
|
144
157
|
return {
|
|
145
158
|
line: parseInt(match[1], 10),
|
|
@@ -156,8 +169,9 @@ function normalizeHashRef(ref) {
|
|
|
156
169
|
if (annotated) {
|
|
157
170
|
return `${parseInt(annotated[1], 10)}:${annotated[2].toLowerCase()}`;
|
|
158
171
|
}
|
|
172
|
+
const display = ref.length > 100 ? `${ref.slice(0, 100)}\u2026` : ref;
|
|
159
173
|
throw new Error(
|
|
160
|
-
`Invalid hash reference: "${
|
|
174
|
+
`Invalid hash reference: "${display}". Expected "<line>:<hash>" or an annotated line like "#HL <line>:<hash>|..."`
|
|
161
175
|
);
|
|
162
176
|
}
|
|
163
177
|
function buildHashMap(content, hashLen) {
|
|
@@ -200,12 +214,14 @@ function resolveRange(startRef, endRef, content, hashLen) {
|
|
|
200
214
|
`Invalid range: start line ${start.line} is after end line ${end.line}`
|
|
201
215
|
);
|
|
202
216
|
}
|
|
203
|
-
const
|
|
204
|
-
const
|
|
217
|
+
const lineEnding = detectLineEnding(content);
|
|
218
|
+
const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
|
|
219
|
+
const lines = normalized.split("\n");
|
|
220
|
+
const startVerify = verifyHash(start.line, start.hash, normalized, hashLen, lines);
|
|
205
221
|
if (!startVerify.valid) {
|
|
206
222
|
throw new Error(`Start reference invalid: ${startVerify.message}`);
|
|
207
223
|
}
|
|
208
|
-
const endVerify = verifyHash(end.line, end.hash,
|
|
224
|
+
const endVerify = verifyHash(end.line, end.hash, normalized, hashLen, lines);
|
|
209
225
|
if (!endVerify.valid) {
|
|
210
226
|
throw new Error(`End reference invalid: ${endVerify.message}`);
|
|
211
227
|
}
|
|
@@ -214,22 +230,27 @@ function resolveRange(startRef, endRef, content, hashLen) {
|
|
|
214
230
|
startLine: start.line,
|
|
215
231
|
endLine: end.line,
|
|
216
232
|
lines: rangeLines,
|
|
217
|
-
content: rangeLines.join(
|
|
233
|
+
content: rangeLines.join(lineEnding)
|
|
218
234
|
};
|
|
219
235
|
}
|
|
220
236
|
function replaceRange(startRef, endRef, content, replacement, hashLen) {
|
|
221
|
-
const
|
|
222
|
-
const
|
|
237
|
+
const lineEnding = detectLineEnding(content);
|
|
238
|
+
const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
|
|
239
|
+
const range = resolveRange(startRef, endRef, normalized, hashLen);
|
|
240
|
+
const lines = normalized.split("\n");
|
|
223
241
|
const before = lines.slice(0, range.startLine - 1);
|
|
224
242
|
const after = lines.slice(range.endLine);
|
|
225
243
|
const replacementLines = replacement.split("\n");
|
|
226
|
-
|
|
244
|
+
const result = [...before, ...replacementLines, ...after].join("\n");
|
|
245
|
+
return lineEnding === "\r\n" ? result.replace(/\n/g, "\r\n") : result;
|
|
227
246
|
}
|
|
228
247
|
function applyHashEdit(input, content, hashLen) {
|
|
248
|
+
const lineEnding = detectLineEnding(content);
|
|
249
|
+
const workContent = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
|
|
229
250
|
const normalizedStart = normalizeHashRef(input.startRef);
|
|
230
251
|
const start = parseHashRef(normalizedStart);
|
|
231
|
-
const lines =
|
|
232
|
-
const startVerify = verifyHash(start.line, start.hash,
|
|
252
|
+
const lines = workContent.split("\n");
|
|
253
|
+
const startVerify = verifyHash(start.line, start.hash, workContent, hashLen, lines);
|
|
233
254
|
if (!startVerify.valid) {
|
|
234
255
|
throw new Error(`Start reference invalid: ${startVerify.message}`);
|
|
235
256
|
}
|
|
@@ -248,7 +269,7 @@ function applyHashEdit(input, content, hashLen) {
|
|
|
248
269
|
operation: input.operation,
|
|
249
270
|
startLine: start.line,
|
|
250
271
|
endLine: start.line,
|
|
251
|
-
content: next2
|
|
272
|
+
content: lineEnding === "\r\n" ? next2.replace(/\n/g, "\r\n") : next2
|
|
252
273
|
};
|
|
253
274
|
}
|
|
254
275
|
const normalizedEnd = normalizeHashRef(input.endRef ?? input.startRef);
|
|
@@ -258,7 +279,7 @@ function applyHashEdit(input, content, hashLen) {
|
|
|
258
279
|
`Invalid range: start line ${start.line} is after end line ${end.line}`
|
|
259
280
|
);
|
|
260
281
|
}
|
|
261
|
-
const endVerify = verifyHash(end.line, end.hash,
|
|
282
|
+
const endVerify = verifyHash(end.line, end.hash, workContent, hashLen, lines);
|
|
262
283
|
if (!endVerify.valid) {
|
|
263
284
|
throw new Error(`End reference invalid: ${endVerify.message}`);
|
|
264
285
|
}
|
|
@@ -274,13 +295,17 @@ function applyHashEdit(input, content, hashLen) {
|
|
|
274
295
|
operation: input.operation,
|
|
275
296
|
startLine: start.line,
|
|
276
297
|
endLine: end.line,
|
|
277
|
-
content: next
|
|
298
|
+
content: lineEnding === "\r\n" ? next.replace(/\n/g, "\r\n") : next
|
|
278
299
|
};
|
|
279
300
|
}
|
|
280
301
|
function matchesGlob(filePath, pattern) {
|
|
281
302
|
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
282
303
|
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
283
|
-
|
|
304
|
+
let isMatch = globMatcherCache.get(normalizedPattern);
|
|
305
|
+
if (!isMatch) {
|
|
306
|
+
isMatch = (0, import_picomatch.default)(normalizedPattern, { dot: true });
|
|
307
|
+
globMatcherCache.set(normalizedPattern, isMatch);
|
|
308
|
+
}
|
|
284
309
|
return isMatch(normalizedPath);
|
|
285
310
|
}
|
|
286
311
|
function shouldExclude(filePath, patterns) {
|
|
@@ -289,6 +314,9 @@ function shouldExclude(filePath, patterns) {
|
|
|
289
314
|
function getByteLength(content) {
|
|
290
315
|
return textEncoder.encode(content).length;
|
|
291
316
|
}
|
|
317
|
+
function detectLineEnding(content) {
|
|
318
|
+
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
319
|
+
}
|
|
292
320
|
function createHashline(config) {
|
|
293
321
|
const resolved = resolveConfig(config);
|
|
294
322
|
const cache = new HashlineCache(resolved.cacheSize);
|
|
@@ -340,7 +368,7 @@ function createHashline(config) {
|
|
|
340
368
|
}
|
|
341
369
|
};
|
|
342
370
|
}
|
|
343
|
-
var import_picomatch, DEFAULT_EXCLUDE_PATTERNS, DEFAULT_PREFIX, DEFAULT_CONFIG, modulusCache, stripRegexCache, HashlineCache, textEncoder;
|
|
371
|
+
var import_picomatch, DEFAULT_EXCLUDE_PATTERNS, DEFAULT_PREFIX, DEFAULT_CONFIG, modulusCache, stripRegexCache, HashlineCache, globMatcherCache, textEncoder;
|
|
344
372
|
var init_hashline = __esm({
|
|
345
373
|
"src/hashline.ts"() {
|
|
346
374
|
"use strict";
|
|
@@ -373,7 +401,24 @@ var init_hashline = __esm({
|
|
|
373
401
|
"**/*.exe",
|
|
374
402
|
"**/*.dll",
|
|
375
403
|
"**/*.so",
|
|
376
|
-
"**/*.dylib"
|
|
404
|
+
"**/*.dylib",
|
|
405
|
+
// Sensitive credential and secret files
|
|
406
|
+
"**/.env",
|
|
407
|
+
"**/.env.*",
|
|
408
|
+
"**/*.pem",
|
|
409
|
+
"**/*.key",
|
|
410
|
+
"**/*.p12",
|
|
411
|
+
"**/*.pfx",
|
|
412
|
+
"**/id_rsa",
|
|
413
|
+
"**/id_rsa.pub",
|
|
414
|
+
"**/id_ed25519",
|
|
415
|
+
"**/id_ed25519.pub",
|
|
416
|
+
"**/id_ecdsa",
|
|
417
|
+
"**/id_ecdsa.pub",
|
|
418
|
+
"**/.npmrc",
|
|
419
|
+
"**/.netrc",
|
|
420
|
+
"**/credentials",
|
|
421
|
+
"**/credentials.json"
|
|
377
422
|
];
|
|
378
423
|
DEFAULT_PREFIX = "#HL ";
|
|
379
424
|
DEFAULT_CONFIG = {
|
|
@@ -447,6 +492,7 @@ var init_hashline = __esm({
|
|
|
447
492
|
return this.cache.size;
|
|
448
493
|
}
|
|
449
494
|
};
|
|
495
|
+
globMatcherCache = /* @__PURE__ */ new Map();
|
|
450
496
|
textEncoder = new TextEncoder();
|
|
451
497
|
}
|
|
452
498
|
});
|
|
@@ -690,23 +736,37 @@ function createHashlineEditTool(config, cache) {
|
|
|
690
736
|
operation: import_zod.z.enum(["replace", "delete", "insert_before", "insert_after"]).describe("Edit operation"),
|
|
691
737
|
startRef: import_zod.z.string().describe('Start hash reference, e.g. "5:a3f" or "#HL 5:a3f|const x = 1;"'),
|
|
692
738
|
endRef: import_zod.z.string().optional().describe("End hash reference for range operations. Defaults to startRef when omitted."),
|
|
693
|
-
replacement: import_zod.z.string().optional().describe("Replacement/inserted content. Required for replace/insert operations.")
|
|
739
|
+
replacement: import_zod.z.string().max(1e7).optional().describe("Replacement/inserted content. Required for replace/insert operations.")
|
|
694
740
|
},
|
|
695
741
|
async execute(args, context) {
|
|
696
742
|
const { path, operation, startRef, endRef, replacement } = args;
|
|
697
743
|
const absPath = (0, import_path2.isAbsolute)(path) ? path : (0, import_path2.resolve)(context.directory, path);
|
|
698
|
-
let realAbs;
|
|
699
|
-
try {
|
|
700
|
-
realAbs = (0, import_fs2.realpathSync)(absPath);
|
|
701
|
-
} catch {
|
|
702
|
-
realAbs = (0, import_path2.resolve)(absPath);
|
|
703
|
-
}
|
|
704
744
|
const realDirectory = (0, import_fs2.realpathSync)((0, import_path2.resolve)(context.directory));
|
|
705
745
|
const realWorktree = (0, import_fs2.realpathSync)((0, import_path2.resolve)(context.worktree));
|
|
706
746
|
function isWithin(filePath, dir) {
|
|
707
747
|
if (dir === import_path2.sep) return false;
|
|
748
|
+
if (process.platform === "win32") {
|
|
749
|
+
if (/^[A-Za-z]:\\$/.test(dir)) return false;
|
|
750
|
+
if (/^\\\\[^\\]+\\[^\\]+$/.test(dir)) return false;
|
|
751
|
+
}
|
|
708
752
|
return filePath === dir || filePath.startsWith(dir + import_path2.sep);
|
|
709
753
|
}
|
|
754
|
+
let realAbs;
|
|
755
|
+
try {
|
|
756
|
+
realAbs = (0, import_fs2.realpathSync)(absPath);
|
|
757
|
+
} catch {
|
|
758
|
+
const parentDir = (0, import_path2.dirname)(absPath);
|
|
759
|
+
let realParent;
|
|
760
|
+
try {
|
|
761
|
+
realParent = (0, import_fs2.realpathSync)(parentDir);
|
|
762
|
+
} catch {
|
|
763
|
+
throw new Error(`Access denied: cannot verify parent directory for "${path}"`);
|
|
764
|
+
}
|
|
765
|
+
if (!isWithin(realParent, realDirectory) && !isWithin(realParent, realWorktree)) {
|
|
766
|
+
throw new Error(`Access denied: "${path}" resolves outside the project directory`);
|
|
767
|
+
}
|
|
768
|
+
realAbs = (0, import_path2.resolve)(absPath);
|
|
769
|
+
}
|
|
710
770
|
if (!isWithin(realAbs, realDirectory) && !isWithin(realAbs, realWorktree)) {
|
|
711
771
|
throw new Error(`Access denied: "${path}" resolves outside the project directory`);
|
|
712
772
|
}
|
|
@@ -719,6 +779,11 @@ function createHashlineEditTool(config, cache) {
|
|
|
719
779
|
const reason = error instanceof Error ? error.message : String(error);
|
|
720
780
|
throw new Error(`Failed to read "${displayPath}": ${reason}`);
|
|
721
781
|
}
|
|
782
|
+
if (config.maxFileSize > 0 && getByteLength(current) > config.maxFileSize) {
|
|
783
|
+
throw new Error(
|
|
784
|
+
`File "${displayPath}" exceeds the configured maximum size (${config.maxFileSize} bytes)`
|
|
785
|
+
);
|
|
786
|
+
}
|
|
722
787
|
let nextContent;
|
|
723
788
|
let startLine;
|
|
724
789
|
let endLine;
|
|
@@ -773,10 +838,40 @@ function createHashlineEditTool(config, cache) {
|
|
|
773
838
|
|
|
774
839
|
// src/index.ts
|
|
775
840
|
var CONFIG_FILENAME = "opencode-hashline.json";
|
|
841
|
+
function sanitizeConfig(raw) {
|
|
842
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
|
|
843
|
+
const r = raw;
|
|
844
|
+
const result = {};
|
|
845
|
+
if (Array.isArray(r.exclude)) {
|
|
846
|
+
result.exclude = r.exclude.filter(
|
|
847
|
+
(p) => typeof p === "string" && p.length <= 512
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
if (typeof r.maxFileSize === "number" && Number.isFinite(r.maxFileSize) && r.maxFileSize >= 0) {
|
|
851
|
+
result.maxFileSize = r.maxFileSize;
|
|
852
|
+
}
|
|
853
|
+
if (typeof r.hashLength === "number" && Number.isFinite(r.hashLength)) {
|
|
854
|
+
result.hashLength = Math.max(0, Math.min(8, Math.floor(r.hashLength)));
|
|
855
|
+
}
|
|
856
|
+
if (typeof r.cacheSize === "number" && Number.isFinite(r.cacheSize) && r.cacheSize > 0) {
|
|
857
|
+
result.cacheSize = Math.min(Math.floor(r.cacheSize), 1e4);
|
|
858
|
+
}
|
|
859
|
+
if (r.prefix === false) {
|
|
860
|
+
result.prefix = false;
|
|
861
|
+
} else if (typeof r.prefix === "string") {
|
|
862
|
+
if (/^[\x20-\x7E]{0,20}$/.test(r.prefix)) {
|
|
863
|
+
result.prefix = r.prefix;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (typeof r.debug === "boolean") {
|
|
867
|
+
result.debug = r.debug;
|
|
868
|
+
}
|
|
869
|
+
return result;
|
|
870
|
+
}
|
|
776
871
|
function loadConfigFile(filePath) {
|
|
777
872
|
try {
|
|
778
873
|
const raw = (0, import_fs3.readFileSync)(filePath, "utf-8");
|
|
779
|
-
return JSON.parse(raw);
|
|
874
|
+
return sanitizeConfig(JSON.parse(raw));
|
|
780
875
|
} catch {
|
|
781
876
|
return void 0;
|
|
782
877
|
}
|
|
@@ -3,12 +3,13 @@ import {
|
|
|
3
3
|
createFileReadAfterHook,
|
|
4
4
|
createSystemPromptHook,
|
|
5
5
|
setDebug
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-VPCMHCTB.js";
|
|
7
7
|
import {
|
|
8
8
|
HashlineCache,
|
|
9
9
|
applyHashEdit,
|
|
10
|
+
getByteLength,
|
|
10
11
|
resolveConfig
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-I6RACR3D.js";
|
|
12
13
|
|
|
13
14
|
// src/index.ts
|
|
14
15
|
import { readFileSync as readFileSync2, realpathSync as realpathSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
@@ -18,7 +19,7 @@ import { fileURLToPath } from "url";
|
|
|
18
19
|
|
|
19
20
|
// src/hashline-tool.ts
|
|
20
21
|
import { readFileSync, realpathSync, writeFileSync } from "fs";
|
|
21
|
-
import { isAbsolute, relative, resolve, sep } from "path";
|
|
22
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "path";
|
|
22
23
|
import { z } from "zod";
|
|
23
24
|
function createHashlineEditTool(config, cache) {
|
|
24
25
|
return {
|
|
@@ -28,23 +29,37 @@ function createHashlineEditTool(config, cache) {
|
|
|
28
29
|
operation: z.enum(["replace", "delete", "insert_before", "insert_after"]).describe("Edit operation"),
|
|
29
30
|
startRef: z.string().describe('Start hash reference, e.g. "5:a3f" or "#HL 5:a3f|const x = 1;"'),
|
|
30
31
|
endRef: z.string().optional().describe("End hash reference for range operations. Defaults to startRef when omitted."),
|
|
31
|
-
replacement: z.string().optional().describe("Replacement/inserted content. Required for replace/insert operations.")
|
|
32
|
+
replacement: z.string().max(1e7).optional().describe("Replacement/inserted content. Required for replace/insert operations.")
|
|
32
33
|
},
|
|
33
34
|
async execute(args, context) {
|
|
34
35
|
const { path, operation, startRef, endRef, replacement } = args;
|
|
35
36
|
const absPath = isAbsolute(path) ? path : resolve(context.directory, path);
|
|
36
|
-
let realAbs;
|
|
37
|
-
try {
|
|
38
|
-
realAbs = realpathSync(absPath);
|
|
39
|
-
} catch {
|
|
40
|
-
realAbs = resolve(absPath);
|
|
41
|
-
}
|
|
42
37
|
const realDirectory = realpathSync(resolve(context.directory));
|
|
43
38
|
const realWorktree = realpathSync(resolve(context.worktree));
|
|
44
39
|
function isWithin(filePath, dir) {
|
|
45
40
|
if (dir === sep) return false;
|
|
41
|
+
if (process.platform === "win32") {
|
|
42
|
+
if (/^[A-Za-z]:\\$/.test(dir)) return false;
|
|
43
|
+
if (/^\\\\[^\\]+\\[^\\]+$/.test(dir)) return false;
|
|
44
|
+
}
|
|
46
45
|
return filePath === dir || filePath.startsWith(dir + sep);
|
|
47
46
|
}
|
|
47
|
+
let realAbs;
|
|
48
|
+
try {
|
|
49
|
+
realAbs = realpathSync(absPath);
|
|
50
|
+
} catch {
|
|
51
|
+
const parentDir = dirname(absPath);
|
|
52
|
+
let realParent;
|
|
53
|
+
try {
|
|
54
|
+
realParent = realpathSync(parentDir);
|
|
55
|
+
} catch {
|
|
56
|
+
throw new Error(`Access denied: cannot verify parent directory for "${path}"`);
|
|
57
|
+
}
|
|
58
|
+
if (!isWithin(realParent, realDirectory) && !isWithin(realParent, realWorktree)) {
|
|
59
|
+
throw new Error(`Access denied: "${path}" resolves outside the project directory`);
|
|
60
|
+
}
|
|
61
|
+
realAbs = resolve(absPath);
|
|
62
|
+
}
|
|
48
63
|
if (!isWithin(realAbs, realDirectory) && !isWithin(realAbs, realWorktree)) {
|
|
49
64
|
throw new Error(`Access denied: "${path}" resolves outside the project directory`);
|
|
50
65
|
}
|
|
@@ -57,6 +72,11 @@ function createHashlineEditTool(config, cache) {
|
|
|
57
72
|
const reason = error instanceof Error ? error.message : String(error);
|
|
58
73
|
throw new Error(`Failed to read "${displayPath}": ${reason}`);
|
|
59
74
|
}
|
|
75
|
+
if (config.maxFileSize > 0 && getByteLength(current) > config.maxFileSize) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`File "${displayPath}" exceeds the configured maximum size (${config.maxFileSize} bytes)`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
60
80
|
let nextContent;
|
|
61
81
|
let startLine;
|
|
62
82
|
let endLine;
|
|
@@ -111,10 +131,40 @@ function createHashlineEditTool(config, cache) {
|
|
|
111
131
|
|
|
112
132
|
// src/index.ts
|
|
113
133
|
var CONFIG_FILENAME = "opencode-hashline.json";
|
|
134
|
+
function sanitizeConfig(raw) {
|
|
135
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
|
|
136
|
+
const r = raw;
|
|
137
|
+
const result = {};
|
|
138
|
+
if (Array.isArray(r.exclude)) {
|
|
139
|
+
result.exclude = r.exclude.filter(
|
|
140
|
+
(p) => typeof p === "string" && p.length <= 512
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (typeof r.maxFileSize === "number" && Number.isFinite(r.maxFileSize) && r.maxFileSize >= 0) {
|
|
144
|
+
result.maxFileSize = r.maxFileSize;
|
|
145
|
+
}
|
|
146
|
+
if (typeof r.hashLength === "number" && Number.isFinite(r.hashLength)) {
|
|
147
|
+
result.hashLength = Math.max(0, Math.min(8, Math.floor(r.hashLength)));
|
|
148
|
+
}
|
|
149
|
+
if (typeof r.cacheSize === "number" && Number.isFinite(r.cacheSize) && r.cacheSize > 0) {
|
|
150
|
+
result.cacheSize = Math.min(Math.floor(r.cacheSize), 1e4);
|
|
151
|
+
}
|
|
152
|
+
if (r.prefix === false) {
|
|
153
|
+
result.prefix = false;
|
|
154
|
+
} else if (typeof r.prefix === "string") {
|
|
155
|
+
if (/^[\x20-\x7E]{0,20}$/.test(r.prefix)) {
|
|
156
|
+
result.prefix = r.prefix;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (typeof r.debug === "boolean") {
|
|
160
|
+
result.debug = r.debug;
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
114
164
|
function loadConfigFile(filePath) {
|
|
115
165
|
try {
|
|
116
166
|
const raw = readFileSync2(filePath, "utf-8");
|
|
117
|
-
return JSON.parse(raw);
|
|
167
|
+
return sanitizeConfig(JSON.parse(raw));
|
|
118
168
|
} catch {
|
|
119
169
|
return void 0;
|
|
120
170
|
}
|
|
@@ -172,7 +222,7 @@ function createHashlinePlugin(userConfig) {
|
|
|
172
222
|
const out = output;
|
|
173
223
|
const hashLen = config.hashLength || 0;
|
|
174
224
|
const prefix = config.prefix;
|
|
175
|
-
const { formatFileWithHashes, shouldExclude, getByteLength } = await import("./hashline-
|
|
225
|
+
const { formatFileWithHashes, shouldExclude, getByteLength: getByteLength2 } = await import("./hashline-5PFAXY3H.js");
|
|
176
226
|
for (const p of out.parts ?? []) {
|
|
177
227
|
if (p.type !== "file") continue;
|
|
178
228
|
if (!p.url || !p.mime?.startsWith("text/")) continue;
|
|
@@ -199,7 +249,7 @@ function createHashlinePlugin(userConfig) {
|
|
|
199
249
|
} catch {
|
|
200
250
|
continue;
|
|
201
251
|
}
|
|
202
|
-
if (config.maxFileSize > 0 &&
|
|
252
|
+
if (config.maxFileSize > 0 && getByteLength2(content) > config.maxFileSize) continue;
|
|
203
253
|
const cached = cache.get(filePath, content);
|
|
204
254
|
if (cached) {
|
|
205
255
|
const tmpPath2 = join(tmpdir(), `hashline-${p.id}.txt`);
|
package/dist/utils.cjs
CHANGED
|
@@ -87,7 +87,24 @@ var DEFAULT_EXCLUDE_PATTERNS = [
|
|
|
87
87
|
"**/*.exe",
|
|
88
88
|
"**/*.dll",
|
|
89
89
|
"**/*.so",
|
|
90
|
-
"**/*.dylib"
|
|
90
|
+
"**/*.dylib",
|
|
91
|
+
// Sensitive credential and secret files
|
|
92
|
+
"**/.env",
|
|
93
|
+
"**/.env.*",
|
|
94
|
+
"**/*.pem",
|
|
95
|
+
"**/*.key",
|
|
96
|
+
"**/*.p12",
|
|
97
|
+
"**/*.pfx",
|
|
98
|
+
"**/id_rsa",
|
|
99
|
+
"**/id_rsa.pub",
|
|
100
|
+
"**/id_ed25519",
|
|
101
|
+
"**/id_ed25519.pub",
|
|
102
|
+
"**/id_ecdsa",
|
|
103
|
+
"**/id_ecdsa.pub",
|
|
104
|
+
"**/.npmrc",
|
|
105
|
+
"**/.netrc",
|
|
106
|
+
"**/credentials",
|
|
107
|
+
"**/credentials.json"
|
|
91
108
|
];
|
|
92
109
|
var DEFAULT_PREFIX = "#HL ";
|
|
93
110
|
var DEFAULT_CONFIG = {
|
|
@@ -147,16 +164,24 @@ function computeLineHash(idx, line, hashLen = 3) {
|
|
|
147
164
|
return hash.toString(16).padStart(hashLen, "0");
|
|
148
165
|
}
|
|
149
166
|
function formatFileWithHashes(content, hashLen, prefix) {
|
|
150
|
-
const
|
|
167
|
+
const normalized = content.includes("\r\n") ? content.replace(/\r\n/g, "\n") : content;
|
|
168
|
+
const lines = normalized.split("\n");
|
|
151
169
|
const effectiveLen = hashLen && hashLen >= 3 ? hashLen : getAdaptiveHashLength(lines.length);
|
|
152
170
|
const effectivePrefix = prefix === void 0 ? DEFAULT_PREFIX : prefix === false ? "" : prefix;
|
|
153
171
|
const hashes = new Array(lines.length);
|
|
154
172
|
const seen = /* @__PURE__ */ new Map();
|
|
173
|
+
const upgraded = /* @__PURE__ */ new Set();
|
|
155
174
|
for (let idx = 0; idx < lines.length; idx++) {
|
|
156
175
|
const hash = computeLineHash(idx, lines[idx], effectiveLen);
|
|
157
176
|
if (seen.has(hash)) {
|
|
158
177
|
const longerLen = Math.min(effectiveLen + 1, 8);
|
|
178
|
+
const prevIdx = seen.get(hash);
|
|
179
|
+
if (!upgraded.has(prevIdx)) {
|
|
180
|
+
hashes[prevIdx] = computeLineHash(prevIdx, lines[prevIdx], longerLen);
|
|
181
|
+
upgraded.add(prevIdx);
|
|
182
|
+
}
|
|
159
183
|
hashes[idx] = computeLineHash(idx, lines[idx], longerLen);
|
|
184
|
+
upgraded.add(idx);
|
|
160
185
|
} else {
|
|
161
186
|
seen.set(hash, idx);
|
|
162
187
|
hashes[idx] = hash;
|
|
@@ -175,7 +200,9 @@ function stripHashes(content, prefix) {
|
|
|
175
200
|
hashLinePattern = new RegExp(`^([+ \\-])?${escapedPrefix}\\d+:[0-9a-f]{2,8}\\|`);
|
|
176
201
|
stripRegexCache.set(escapedPrefix, hashLinePattern);
|
|
177
202
|
}
|
|
178
|
-
|
|
203
|
+
const lineEnding = detectLineEnding(content);
|
|
204
|
+
const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
|
|
205
|
+
const result = normalized.split("\n").map((line) => {
|
|
179
206
|
const match = line.match(hashLinePattern);
|
|
180
207
|
if (match) {
|
|
181
208
|
const patchMarker = match[1] || "";
|
|
@@ -183,11 +210,13 @@ function stripHashes(content, prefix) {
|
|
|
183
210
|
}
|
|
184
211
|
return line;
|
|
185
212
|
}).join("\n");
|
|
213
|
+
return lineEnding === "\r\n" ? result.replace(/\n/g, "\r\n") : result;
|
|
186
214
|
}
|
|
187
215
|
function parseHashRef(ref) {
|
|
188
216
|
const match = ref.match(/^(\d+):([0-9a-f]{2,8})$/);
|
|
189
217
|
if (!match) {
|
|
190
|
-
|
|
218
|
+
const display = ref.length > 100 ? `${ref.slice(0, 100)}\u2026` : ref;
|
|
219
|
+
throw new Error(`Invalid hash reference: "${display}". Expected format: "<line>:<2-8 char hex>"`);
|
|
191
220
|
}
|
|
192
221
|
return {
|
|
193
222
|
line: parseInt(match[1], 10),
|
|
@@ -204,8 +233,9 @@ function normalizeHashRef(ref) {
|
|
|
204
233
|
if (annotated) {
|
|
205
234
|
return `${parseInt(annotated[1], 10)}:${annotated[2].toLowerCase()}`;
|
|
206
235
|
}
|
|
236
|
+
const display = ref.length > 100 ? `${ref.slice(0, 100)}\u2026` : ref;
|
|
207
237
|
throw new Error(
|
|
208
|
-
`Invalid hash reference: "${
|
|
238
|
+
`Invalid hash reference: "${display}". Expected "<line>:<hash>" or an annotated line like "#HL <line>:<hash>|..."`
|
|
209
239
|
);
|
|
210
240
|
}
|
|
211
241
|
function buildHashMap(content, hashLen) {
|
|
@@ -248,12 +278,14 @@ function resolveRange(startRef, endRef, content, hashLen) {
|
|
|
248
278
|
`Invalid range: start line ${start.line} is after end line ${end.line}`
|
|
249
279
|
);
|
|
250
280
|
}
|
|
251
|
-
const
|
|
252
|
-
const
|
|
281
|
+
const lineEnding = detectLineEnding(content);
|
|
282
|
+
const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
|
|
283
|
+
const lines = normalized.split("\n");
|
|
284
|
+
const startVerify = verifyHash(start.line, start.hash, normalized, hashLen, lines);
|
|
253
285
|
if (!startVerify.valid) {
|
|
254
286
|
throw new Error(`Start reference invalid: ${startVerify.message}`);
|
|
255
287
|
}
|
|
256
|
-
const endVerify = verifyHash(end.line, end.hash,
|
|
288
|
+
const endVerify = verifyHash(end.line, end.hash, normalized, hashLen, lines);
|
|
257
289
|
if (!endVerify.valid) {
|
|
258
290
|
throw new Error(`End reference invalid: ${endVerify.message}`);
|
|
259
291
|
}
|
|
@@ -262,22 +294,27 @@ function resolveRange(startRef, endRef, content, hashLen) {
|
|
|
262
294
|
startLine: start.line,
|
|
263
295
|
endLine: end.line,
|
|
264
296
|
lines: rangeLines,
|
|
265
|
-
content: rangeLines.join(
|
|
297
|
+
content: rangeLines.join(lineEnding)
|
|
266
298
|
};
|
|
267
299
|
}
|
|
268
300
|
function replaceRange(startRef, endRef, content, replacement, hashLen) {
|
|
269
|
-
const
|
|
270
|
-
const
|
|
301
|
+
const lineEnding = detectLineEnding(content);
|
|
302
|
+
const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
|
|
303
|
+
const range = resolveRange(startRef, endRef, normalized, hashLen);
|
|
304
|
+
const lines = normalized.split("\n");
|
|
271
305
|
const before = lines.slice(0, range.startLine - 1);
|
|
272
306
|
const after = lines.slice(range.endLine);
|
|
273
307
|
const replacementLines = replacement.split("\n");
|
|
274
|
-
|
|
308
|
+
const result = [...before, ...replacementLines, ...after].join("\n");
|
|
309
|
+
return lineEnding === "\r\n" ? result.replace(/\n/g, "\r\n") : result;
|
|
275
310
|
}
|
|
276
311
|
function applyHashEdit(input, content, hashLen) {
|
|
312
|
+
const lineEnding = detectLineEnding(content);
|
|
313
|
+
const workContent = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
|
|
277
314
|
const normalizedStart = normalizeHashRef(input.startRef);
|
|
278
315
|
const start = parseHashRef(normalizedStart);
|
|
279
|
-
const lines =
|
|
280
|
-
const startVerify = verifyHash(start.line, start.hash,
|
|
316
|
+
const lines = workContent.split("\n");
|
|
317
|
+
const startVerify = verifyHash(start.line, start.hash, workContent, hashLen, lines);
|
|
281
318
|
if (!startVerify.valid) {
|
|
282
319
|
throw new Error(`Start reference invalid: ${startVerify.message}`);
|
|
283
320
|
}
|
|
@@ -296,7 +333,7 @@ function applyHashEdit(input, content, hashLen) {
|
|
|
296
333
|
operation: input.operation,
|
|
297
334
|
startLine: start.line,
|
|
298
335
|
endLine: start.line,
|
|
299
|
-
content: next2
|
|
336
|
+
content: lineEnding === "\r\n" ? next2.replace(/\n/g, "\r\n") : next2
|
|
300
337
|
};
|
|
301
338
|
}
|
|
302
339
|
const normalizedEnd = normalizeHashRef(input.endRef ?? input.startRef);
|
|
@@ -306,7 +343,7 @@ function applyHashEdit(input, content, hashLen) {
|
|
|
306
343
|
`Invalid range: start line ${start.line} is after end line ${end.line}`
|
|
307
344
|
);
|
|
308
345
|
}
|
|
309
|
-
const endVerify = verifyHash(end.line, end.hash,
|
|
346
|
+
const endVerify = verifyHash(end.line, end.hash, workContent, hashLen, lines);
|
|
310
347
|
if (!endVerify.valid) {
|
|
311
348
|
throw new Error(`End reference invalid: ${endVerify.message}`);
|
|
312
349
|
}
|
|
@@ -322,7 +359,7 @@ function applyHashEdit(input, content, hashLen) {
|
|
|
322
359
|
operation: input.operation,
|
|
323
360
|
startLine: start.line,
|
|
324
361
|
endLine: end.line,
|
|
325
|
-
content: next
|
|
362
|
+
content: lineEnding === "\r\n" ? next.replace(/\n/g, "\r\n") : next
|
|
326
363
|
};
|
|
327
364
|
}
|
|
328
365
|
var HashlineCache = class {
|
|
@@ -384,10 +421,15 @@ var HashlineCache = class {
|
|
|
384
421
|
return this.cache.size;
|
|
385
422
|
}
|
|
386
423
|
};
|
|
424
|
+
var globMatcherCache = /* @__PURE__ */ new Map();
|
|
387
425
|
function matchesGlob(filePath, pattern) {
|
|
388
426
|
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
389
427
|
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
390
|
-
|
|
428
|
+
let isMatch = globMatcherCache.get(normalizedPattern);
|
|
429
|
+
if (!isMatch) {
|
|
430
|
+
isMatch = (0, import_picomatch.default)(normalizedPattern, { dot: true });
|
|
431
|
+
globMatcherCache.set(normalizedPattern, isMatch);
|
|
432
|
+
}
|
|
391
433
|
return isMatch(normalizedPath);
|
|
392
434
|
}
|
|
393
435
|
function shouldExclude(filePath, patterns) {
|
|
@@ -397,6 +439,9 @@ var textEncoder = new TextEncoder();
|
|
|
397
439
|
function getByteLength(content) {
|
|
398
440
|
return textEncoder.encode(content).length;
|
|
399
441
|
}
|
|
442
|
+
function detectLineEnding(content) {
|
|
443
|
+
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
444
|
+
}
|
|
400
445
|
function createHashline(config) {
|
|
401
446
|
const resolved = resolveConfig(config);
|
|
402
447
|
const cache = new HashlineCache(resolved.cacheSize);
|
package/dist/utils.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
createFileReadAfterHook,
|
|
4
4
|
createSystemPromptHook,
|
|
5
5
|
isFileReadTool
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-VPCMHCTB.js";
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_CONFIG,
|
|
9
9
|
DEFAULT_EXCLUDE_PATTERNS,
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
shouldExclude,
|
|
26
26
|
stripHashes,
|
|
27
27
|
verifyHash
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-I6RACR3D.js";
|
|
29
29
|
export {
|
|
30
30
|
DEFAULT_CONFIG,
|
|
31
31
|
DEFAULT_EXCLUDE_PATTERNS,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-hashline",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "Hashline plugin for OpenCode — content-addressable line hashing for precise AI code editing",
|
|
5
5
|
"main": "dist/opencode-hashline.cjs",
|
|
6
6
|
"module": "dist/opencode-hashline.js",
|