pi-hashline-edit-pro 0.17.14 → 0.18.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.
- package/README.md +2 -2
- package/package.json +4 -12
- package/prompts/undo-last-replace-guidelines.md +0 -2
- package/src/constants.ts +1 -1
- package/src/hash-store.ts +62 -184
- package/src/hashline/apply.ts +32 -22
- package/src/hashline/hash.ts +114 -43
- package/src/replace-undo.ts +2 -2
- package/src/replace.ts +46 -26
- package/src/utils.ts +6 -0
- package/scripts/cleanup-better-sqlite3.cjs +0 -21
- package/scripts/ensure-better-sqlite3.cjs +0 -60
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ The original uses 2-character hashes of a 16-character alphabet, with the hash b
|
|
|
13
13
|
This fork makes two changes that compound:
|
|
14
14
|
|
|
15
15
|
1. **3-character hash length** over a 64-char URL-safe base64 alphabet (up from 2 characters in the upstream), expanding the hash space from 256 to 262,144 buckets.
|
|
16
|
-
2. **Perfect hashing (collision resolution).** When computing hashes for a file, if a line's base hash collides with an already-assigned hash, the hash is
|
|
16
|
+
2. **Perfect hashing (collision resolution).** When computing hashes for a file, if a line's base hash collides with an already-assigned hash, the next available hash is assigned from a bitset (32KB, 262,144 bits) using a hint cursor for O(1) amortized lookup. This ensures every line gets a unique anchor, even within a 3-character hash space. Two byte-identical lines (e.g. repeated `}` or repeated `import` statements) get different hashes automatically.
|
|
17
17
|
|
|
18
18
|
## Installation
|
|
19
19
|
|
|
@@ -156,7 +156,7 @@ The alphabet is sized for an LLM consumer. The model tokenizes, it doesn't squin
|
|
|
156
156
|
|
|
157
157
|
Before hashing, each line is normalized: carriage returns are stripped and trailing whitespace is trimmed. This `canon()` normalization prevents insignificant whitespace changes from cascade-triggering hash churn across the file. Two lines that differ only in trailing spaces or `\r` characters produce the same hash, so anchor stability is preserved across editor-save cycles that add or remove trailing whitespace.
|
|
158
158
|
|
|
159
|
-
**Perfect hashing (collision resolution):** When computing hashes for a file, if a line's base hash collides with an already-assigned hash, the hash is
|
|
159
|
+
**Perfect hashing (collision resolution):** When computing hashes for a file, if a line's base hash collides with an already-assigned hash, the next available hash is assigned from a bitset (32KB, 262,144 bits) using a hint cursor for O(1) amortized lookup. This ensures every line in a file gets a unique anchor, even with the shorter 3-character hash space. Two byte-identical lines (e.g. repeated `}` or repeated `import` statements) get different hashes automatically.
|
|
160
160
|
The runtime always precomputes the full per-line hash array for a file via `lineHashes(content, path)`, then looks up by line number during validation and during `read` / `replace` response formatting. There is no per-line recomputation that could disagree with what the model saw in its last read. When `path` is provided, `lineHashes` uses a persistent store to preserve hashes for unchanged lines across edits — see [Stable hashing across edits](#stable-hashing-across-edits).
|
|
161
161
|
`HASH_LEN` in `src/hashline/hash.ts` sets the hash body length; bump it to 4 if you need even more entropy without collision resolution.
|
|
162
162
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Strict hashline read/replace tool for pi-coding-agent with hash-anchored edits (3-char, 18-bit, perfect hashing)",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -24,8 +24,7 @@
|
|
|
24
24
|
"src",
|
|
25
25
|
"prompts",
|
|
26
26
|
"README.md",
|
|
27
|
-
"LICENSE"
|
|
28
|
-
"scripts"
|
|
27
|
+
"LICENSE"
|
|
29
28
|
],
|
|
30
29
|
"pi": {
|
|
31
30
|
"extensions": [
|
|
@@ -33,10 +32,8 @@
|
|
|
33
32
|
]
|
|
34
33
|
},
|
|
35
34
|
"dependencies": {
|
|
36
|
-
"better-sqlite3": "^13.0.1",
|
|
37
35
|
"diff": "^8.0.2",
|
|
38
36
|
"file-type": "^21.3.0",
|
|
39
|
-
"sql.js": "1.11",
|
|
40
37
|
"xxhash-wasm": "^1.1.0"
|
|
41
38
|
},
|
|
42
39
|
"peerDependencies": {
|
|
@@ -47,23 +44,18 @@
|
|
|
47
44
|
"test": "vitest run",
|
|
48
45
|
"test:watch": "vitest",
|
|
49
46
|
"lint": "eslint 'src/**/*.ts' 'index.ts'",
|
|
50
|
-
"typecheck": "tsc --noEmit"
|
|
51
|
-
"postinstall": "node scripts/ensure-better-sqlite3.cjs",
|
|
52
|
-
"preuninstall": "node scripts/cleanup-better-sqlite3.cjs"
|
|
47
|
+
"typecheck": "tsc --noEmit"
|
|
53
48
|
},
|
|
54
49
|
"devDependencies": {
|
|
55
50
|
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
56
51
|
"@eslint/js": "^10.0.1",
|
|
57
|
-
"@types/
|
|
58
|
-
"@types/node": "^22.0.0",
|
|
59
|
-
"@types/sql.js": "^1.4.11",
|
|
52
|
+
"@types/node": "^24.0.0",
|
|
60
53
|
"eslint": "^10.7.0",
|
|
61
54
|
"typescript": "^5.8.0",
|
|
62
55
|
"typescript-eslint": "^8.65.0",
|
|
63
56
|
"vitest": "^4.1.8"
|
|
64
57
|
},
|
|
65
58
|
"allowScripts": {
|
|
66
|
-
"better-sqlite3@13.0.1": true,
|
|
67
59
|
"@google/genai@1.52.0": true,
|
|
68
60
|
"koffi@2.16.2": true,
|
|
69
61
|
"protobufjs@7.6.4": true
|
package/src/constants.ts
CHANGED
|
@@ -6,7 +6,7 @@ export const MAX_HASH_LINES = 1_000_000;
|
|
|
6
6
|
export const MAX_HASH_RETRIES = 262_144;
|
|
7
7
|
|
|
8
8
|
export const HASH_STORE_BUSY_TIMEOUT = 1000;
|
|
9
|
-
export const HASH_STORE_VERSION =
|
|
9
|
+
export const HASH_STORE_VERSION = 3;
|
|
10
10
|
export const CONTENT_LINES_NOT_STRING_MSG =
|
|
11
11
|
`[E_BAD_SHAPE] "content_lines" must be a native JSON array of strings, not a JSON string.`
|
|
12
12
|
+ ` Do not serialize the array (e.g. '["line1", "line2"]') — pass it as a proper JSON array: ["line1", "line2"].`;
|
package/src/hash-store.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
2
|
import { readFile, rename, mkdir, stat } from "fs/promises";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
4
|
import { hashStorePath, hashStoreDir, legacyHashStorePath } from "./paths";
|
|
4
|
-
import { errCode } from "./utils";
|
|
5
|
+
import { errCode, splitLines } from "./utils";
|
|
5
6
|
import { initHasher, contentChecksum } from "./hashline/hasher";
|
|
6
7
|
import { HASH_STORE_VERSION, HASH_STORE_BUSY_TIMEOUT } from "./constants";
|
|
7
|
-
import initSqlJs from "sql.js";
|
|
8
|
-
|
|
9
8
|
type SqlParams = (string | number)[];
|
|
10
9
|
|
|
11
10
|
interface Prepared {
|
|
@@ -17,7 +16,7 @@ interface Prepared {
|
|
|
17
16
|
|
|
18
17
|
export interface HashStore {
|
|
19
18
|
readonly stmts: Prepared;
|
|
20
|
-
readonly engine: "
|
|
19
|
+
readonly engine: "node:sqlite";
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
interface LegacySnapshot {
|
|
@@ -36,38 +35,15 @@ function isValidSnapshot(value: unknown): value is LegacySnapshot {
|
|
|
36
35
|
return true;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
let cachedHandle: { path: string; handle: BackendHandle } | null = null;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
let BetterDatabase: any = undefined;
|
|
53
|
-
|
|
54
|
-
async function tryLoadBetter(): Promise<boolean> {
|
|
55
|
-
if (BetterDatabase !== undefined) return BetterDatabase !== null;
|
|
56
|
-
try {
|
|
57
|
-
const mod = await import("better-sqlite3");
|
|
58
|
-
BetterDatabase = mod.default || mod;
|
|
59
|
-
return true;
|
|
60
|
-
} catch {
|
|
61
|
-
BetterDatabase = null;
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function openBetterDb(storePath: string): BackendHandle {
|
|
67
|
-
const db = new BetterDatabase(storePath);
|
|
68
|
-
db.pragma("journal_mode = WAL");
|
|
69
|
-
db.pragma("synchronous = NORMAL");
|
|
70
|
-
db.pragma(`busy_timeout = ${HASH_STORE_BUSY_TIMEOUT}`);
|
|
38
|
+
let cachedDb: { path: string; db: DatabaseSync; stmts: Prepared } | null = null;
|
|
39
|
+
let exitHandlerRegistered = false;
|
|
40
|
+
function openDb(storePath: string): { db: DatabaseSync; stmts: Prepared } {
|
|
41
|
+
const db = new DatabaseSync(storePath, {
|
|
42
|
+
timeout: HASH_STORE_BUSY_TIMEOUT,
|
|
43
|
+
defensive: false,
|
|
44
|
+
} as any);
|
|
45
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
46
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
71
47
|
db.exec(
|
|
72
48
|
"CREATE TABLE IF NOT EXISTS snapshots (" +
|
|
73
49
|
"path TEXT PRIMARY KEY, " +
|
|
@@ -93,169 +69,70 @@ function openBetterDb(storePath: string): BackendHandle {
|
|
|
93
69
|
upsert: (...params) => { upsertStmt.run(...params); },
|
|
94
70
|
};
|
|
95
71
|
|
|
96
|
-
return {
|
|
97
|
-
store: { stmts, engine: "better-sqlite3" },
|
|
98
|
-
close: () => { db.close(); },
|
|
99
|
-
transact: (fn) => { db.transaction(fn).immediate(); },
|
|
100
|
-
prepare: (sql) => {
|
|
101
|
-
const stmt = db.prepare(sql);
|
|
102
|
-
return { run: (...params) => stmt.run(...params), free: () => {} };
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
let SqlJsDatabase: any = null;
|
|
110
|
-
let sqlJsPromise: Promise<void> | null = null;
|
|
111
|
-
|
|
112
|
-
async function ensureSqlJs(): Promise<void> {
|
|
113
|
-
if (sqlJsPromise) return sqlJsPromise;
|
|
114
|
-
sqlJsPromise = initSqlJs().then((SQL) => { SqlJsDatabase = SQL.Database; });
|
|
115
|
-
return sqlJsPromise;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function openSqlJsDb(storePath: string): BackendHandle {
|
|
119
|
-
const data = existsSync(storePath) ? new Uint8Array(readFileSync(storePath)) : undefined;
|
|
120
|
-
const db = new SqlJsDatabase(data);
|
|
121
|
-
|
|
122
|
-
db.run(
|
|
123
|
-
"CREATE TABLE IF NOT EXISTS snapshots (" +
|
|
124
|
-
"path TEXT PRIMARY KEY, " +
|
|
125
|
-
"checksum TEXT NOT NULL, " +
|
|
126
|
-
"line_count INTEGER NOT NULL, " +
|
|
127
|
-
"hashes TEXT NOT NULL, " +
|
|
128
|
-
"updated_at INTEGER NOT NULL" +
|
|
129
|
-
")"
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
function save() {
|
|
133
|
-
writeFileSync(storePath, Buffer.from(db.export()));
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
save();
|
|
137
|
-
|
|
138
|
-
const stmts: Prepared = {
|
|
139
|
-
get: (...params) => {
|
|
140
|
-
const stmt = db.prepare("SELECT hashes FROM snapshots WHERE path = ? AND checksum = ? AND line_count = ?");
|
|
141
|
-
stmt.bind(params);
|
|
142
|
-
let result: Record<string, unknown> | undefined;
|
|
143
|
-
if (stmt.step()) result = stmt.getAsObject() as Record<string, unknown>;
|
|
144
|
-
stmt.free();
|
|
145
|
-
return result;
|
|
146
|
-
},
|
|
147
|
-
allPaths: (...params) => {
|
|
148
|
-
const stmt = db.prepare("SELECT path FROM snapshots");
|
|
149
|
-
if (params.length > 0) stmt.bind(params);
|
|
150
|
-
const results: Record<string, unknown>[] = [];
|
|
151
|
-
while (stmt.step()) results.push(stmt.getAsObject() as Record<string, unknown>);
|
|
152
|
-
stmt.free();
|
|
153
|
-
return results;
|
|
154
|
-
},
|
|
155
|
-
deleteOne: (...params) => {
|
|
156
|
-
if (params.length > 0) db.run("DELETE FROM snapshots WHERE path = ?", params);
|
|
157
|
-
else db.run("DELETE FROM snapshots WHERE path = ?");
|
|
158
|
-
},
|
|
159
|
-
upsert: (...params) => {
|
|
160
|
-
db.run(
|
|
161
|
-
"INSERT INTO snapshots (path, checksum, line_count, hashes, updated_at) VALUES (?, ?, ?, ?, ?) " +
|
|
162
|
-
"ON CONFLICT(path) DO UPDATE SET checksum = excluded.checksum, line_count = excluded.line_count, hashes = excluded.hashes, updated_at = excluded.updated_at",
|
|
163
|
-
params
|
|
164
|
-
);
|
|
165
|
-
},
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
return {
|
|
169
|
-
store: { stmts, engine: "sql.js" },
|
|
170
|
-
close: () => { db.close(); },
|
|
171
|
-
transact: (fn) => {
|
|
172
|
-
db.run("BEGIN IMMEDIATE");
|
|
173
|
-
try { fn(); db.run("COMMIT"); save(); } catch (e) { db.run("ROLLBACK"); throw e; }
|
|
174
|
-
},
|
|
175
|
-
prepare: (sql) => {
|
|
176
|
-
const stmt = db.prepare(sql);
|
|
177
|
-
return {
|
|
178
|
-
run: (...params) => { stmt.bind(params); stmt.step(); stmt.reset(); },
|
|
179
|
-
free: () => { stmt.free(); },
|
|
180
|
-
};
|
|
181
|
-
},
|
|
182
|
-
};
|
|
72
|
+
return { db, stmts };
|
|
183
73
|
}
|
|
184
74
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
let backendPromise: Promise<void> | null = null;
|
|
188
|
-
|
|
189
|
-
async function initBackend(): Promise<void> {
|
|
190
|
-
if (backendPromise) return backendPromise;
|
|
191
|
-
backendPromise = (async () => {
|
|
192
|
-
const hasBetter = await tryLoadBetter();
|
|
193
|
-
if (!hasBetter) await ensureSqlJs();
|
|
194
|
-
})();
|
|
195
|
-
return backendPromise;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
75
|
export async function loadHashStore(): Promise<HashStore> {
|
|
201
76
|
const storePath = hashStorePath();
|
|
202
|
-
if (
|
|
203
|
-
return
|
|
77
|
+
if (cachedDb && cachedDb.path === storePath && cachedDb.db.isOpen) {
|
|
78
|
+
return { stmts: cachedDb.stmts, engine: "node:sqlite" };
|
|
204
79
|
}
|
|
205
80
|
|
|
206
81
|
shutdownHashStore();
|
|
207
82
|
|
|
208
83
|
await initHasher();
|
|
209
84
|
await mkdir(hashStoreDir(), { recursive: true });
|
|
210
|
-
await initBackend();
|
|
211
85
|
|
|
212
86
|
const existed = existsSync(storePath);
|
|
87
|
+
const { db, stmts } = openDb(storePath);
|
|
213
88
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
try {
|
|
217
|
-
handle = openBetterDb(storePath);
|
|
218
|
-
} catch {
|
|
219
|
-
const debug = process.env.PI_HASHLINE_DEBUG === "1" || process.env.PI_HASHLINE_DEBUG === "true";
|
|
220
|
-
if (debug) {
|
|
221
|
-
console.error('better-sqlite3 native binding failed, falling back to sql.js');
|
|
222
|
-
}
|
|
223
|
-
BetterDatabase = null;
|
|
224
|
-
await ensureSqlJs();
|
|
225
|
-
handle = openSqlJsDb(storePath);
|
|
226
|
-
}
|
|
227
|
-
} else {
|
|
228
|
-
await ensureSqlJs();
|
|
229
|
-
handle = openSqlJsDb(storePath);
|
|
89
|
+
if (!existed) {
|
|
90
|
+
await migrateLegacy(db);
|
|
230
91
|
}
|
|
231
92
|
|
|
232
|
-
|
|
233
|
-
|
|
93
|
+
cachedDb = { path: storePath, db, stmts };
|
|
94
|
+
|
|
95
|
+
if (!exitHandlerRegistered) {
|
|
96
|
+
exitHandlerRegistered = true;
|
|
97
|
+
process.once("exit", () => shutdownHashStore());
|
|
98
|
+
for (const sig of ["SIGINT", "SIGTERM"] as const) {
|
|
99
|
+
process.once(sig, () => {
|
|
100
|
+
shutdownHashStore();
|
|
101
|
+
process.kill(process.pid, sig);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
234
104
|
}
|
|
235
105
|
|
|
236
|
-
|
|
237
|
-
return handle.store;
|
|
106
|
+
return { stmts, engine: "node:sqlite" };
|
|
238
107
|
}
|
|
239
108
|
|
|
240
109
|
export function shutdownHashStore(): void {
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
110
|
+
if (cachedDb) {
|
|
111
|
+
try {
|
|
112
|
+
cachedDb.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
cachedDb.db.close();
|
|
116
|
+
cachedDb = null;
|
|
244
117
|
}
|
|
245
118
|
}
|
|
246
119
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
120
|
+
function withStore(fn: () => void): void {
|
|
121
|
+
if (cachedDb) {
|
|
122
|
+
cachedDb.db.exec("BEGIN IMMEDIATE");
|
|
123
|
+
try {
|
|
124
|
+
fn();
|
|
125
|
+
cachedDb.db.exec("COMMIT");
|
|
126
|
+
} catch (e) {
|
|
127
|
+
cachedDb.db.exec("ROLLBACK");
|
|
128
|
+
throw e;
|
|
129
|
+
}
|
|
253
130
|
} else {
|
|
254
131
|
fn();
|
|
255
132
|
}
|
|
256
133
|
}
|
|
257
134
|
|
|
258
|
-
async function migrateLegacy(
|
|
135
|
+
async function migrateLegacy(db: DatabaseSync): Promise<void> {
|
|
259
136
|
const legacyPath = legacyHashStorePath();
|
|
260
137
|
let content: string;
|
|
261
138
|
try {
|
|
@@ -283,20 +160,23 @@ async function migrateLegacy(handle: BackendHandle): Promise<void> {
|
|
|
283
160
|
rows.push([
|
|
284
161
|
key,
|
|
285
162
|
contentChecksum(value.content),
|
|
286
|
-
value.content
|
|
163
|
+
splitLines(value.content).length,
|
|
287
164
|
JSON.stringify(value.hashes),
|
|
288
165
|
Date.now(),
|
|
289
166
|
]);
|
|
290
167
|
}
|
|
291
|
-
|
|
292
168
|
if (rows.length > 0) {
|
|
293
|
-
|
|
294
|
-
|
|
169
|
+
db.exec("BEGIN IMMEDIATE");
|
|
170
|
+
try {
|
|
171
|
+
const stmt = db.prepare(
|
|
295
172
|
"INSERT OR REPLACE INTO snapshots (path, checksum, line_count, hashes, updated_at) VALUES (?, ?, ?, ?, ?)"
|
|
296
173
|
);
|
|
297
174
|
for (const row of rows) stmt.run(...row);
|
|
298
|
-
|
|
299
|
-
})
|
|
175
|
+
db.exec("COMMIT");
|
|
176
|
+
} catch (e) {
|
|
177
|
+
db.exec("ROLLBACK");
|
|
178
|
+
throw e;
|
|
179
|
+
}
|
|
300
180
|
}
|
|
301
181
|
|
|
302
182
|
try {
|
|
@@ -306,15 +186,13 @@ async function migrateLegacy(handle: BackendHandle): Promise<void> {
|
|
|
306
186
|
}
|
|
307
187
|
}
|
|
308
188
|
|
|
309
|
-
|
|
310
|
-
|
|
311
189
|
export function getSnapshot(
|
|
312
190
|
store: HashStore,
|
|
313
191
|
path: string,
|
|
314
192
|
content: string,
|
|
315
193
|
): string[] | undefined {
|
|
316
194
|
const checksum = contentChecksum(content);
|
|
317
|
-
const lineCount = content
|
|
195
|
+
const lineCount = splitLines(content).length;
|
|
318
196
|
const row = store.stmts.get(path, checksum, lineCount);
|
|
319
197
|
return row ? (JSON.parse(row.hashes as string) as string[]) : undefined;
|
|
320
198
|
}
|
|
@@ -327,13 +205,13 @@ export function upsertSnapshot(
|
|
|
327
205
|
hashes: string[],
|
|
328
206
|
): void {
|
|
329
207
|
const hashesJson = JSON.stringify(hashes);
|
|
330
|
-
withStore(
|
|
208
|
+
withStore(() => {
|
|
331
209
|
store.stmts.upsert(path, checksum, lineCount, hashesJson, Date.now());
|
|
332
210
|
});
|
|
333
211
|
}
|
|
334
212
|
|
|
335
213
|
export function deleteSnapshot(store: HashStore, path: string): void {
|
|
336
|
-
withStore(
|
|
214
|
+
withStore(() => {
|
|
337
215
|
store.stmts.deleteOne(path);
|
|
338
216
|
});
|
|
339
217
|
}
|
|
@@ -349,7 +227,7 @@ export async function pruneMissing(store: HashStore): Promise<void> {
|
|
|
349
227
|
}
|
|
350
228
|
}
|
|
351
229
|
if (missing.length === 0) return;
|
|
352
|
-
withStore(
|
|
230
|
+
withStore(() => {
|
|
353
231
|
for (const path of missing) store.stmts.deleteOne(path);
|
|
354
232
|
});
|
|
355
233
|
}
|
package/src/hashline/apply.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { abortIf,
|
|
1
|
+
import { abortIf, splitLines, lastNonEmptyIndex, firstNonEmptyIndex } from "../utils";
|
|
2
2
|
import { _lineHashesPure, HASH_SEP } from "./hash";
|
|
3
3
|
import {
|
|
4
4
|
valEdits,
|
|
@@ -18,22 +18,22 @@ type LIdx = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
export function buildIdx(content: string): LIdx {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
const fileLines = splitLines(content);
|
|
22
|
+
const lineStarts: number[] = [];
|
|
23
|
+
let offset = 0;
|
|
24
|
+
|
|
25
|
+
for (let index = 0; index < fileLines.length; index++) {
|
|
26
|
+
lineStarts.push(offset);
|
|
27
|
+
offset += fileLines[index]!.length;
|
|
28
|
+
if (index < fileLines.length - 1) {
|
|
29
|
+
offset += 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
return {
|
|
34
|
+
fileLines,
|
|
35
|
+
lineStarts,
|
|
36
|
+
};
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
type RESpan = {
|
|
@@ -124,16 +124,26 @@ function resToSpan(
|
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
if (content.endsWith("\n")) {
|
|
128
|
+
return {
|
|
129
|
+
kind: "replace",
|
|
130
|
+
index,
|
|
131
|
+
label,
|
|
132
|
+
start: lineStarts[startLine - 1]!,
|
|
133
|
+
end: content.length,
|
|
134
|
+
replacement: "",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
127
138
|
return {
|
|
128
139
|
kind: "replace",
|
|
129
140
|
index,
|
|
130
141
|
label,
|
|
131
142
|
start: Math.max(0, lineStarts[startLine - 1]! - 1),
|
|
132
|
-
end:
|
|
143
|
+
end: content.length,
|
|
133
144
|
replacement: "",
|
|
134
145
|
};
|
|
135
146
|
}
|
|
136
|
-
|
|
137
147
|
function assertNoConflict(spans: RESpan[]): void {
|
|
138
148
|
for (let leftIndex = 0; leftIndex < spans.length; leftIndex++) {
|
|
139
149
|
const left = spans[leftIndex]!;
|
|
@@ -375,14 +385,14 @@ export function changedRange(
|
|
|
375
385
|
if (original.length === 0) {
|
|
376
386
|
return {
|
|
377
387
|
firstChangedLine: 1,
|
|
378
|
-
lastChangedLine:
|
|
388
|
+
lastChangedLine: splitLines(result).length,
|
|
379
389
|
};
|
|
380
390
|
}
|
|
381
391
|
|
|
382
392
|
if (result.startsWith(original) && original.endsWith("\n")) {
|
|
383
393
|
return {
|
|
384
|
-
firstChangedLine:
|
|
385
|
-
lastChangedLine:
|
|
394
|
+
firstChangedLine: splitLines(original).length + 1,
|
|
395
|
+
lastChangedLine: splitLines(result).length,
|
|
386
396
|
};
|
|
387
397
|
}
|
|
388
398
|
|
|
@@ -415,7 +425,7 @@ export function changedRange(
|
|
|
415
425
|
const firstChangedLine = idxToLine(firstDiff + 1, result);
|
|
416
426
|
let lastChangedLine: number;
|
|
417
427
|
if (lastRes < firstDiff) {
|
|
418
|
-
lastChangedLine = result.length === 0 ? 1 :
|
|
428
|
+
lastChangedLine = result.length === 0 ? 1 : splitLines(result).length;
|
|
419
429
|
} else if (
|
|
420
430
|
firstDiff === 0 &&
|
|
421
431
|
original.length > 0 &&
|
package/src/hashline/hash.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { splitLines } from "../utils";
|
|
2
2
|
import {
|
|
3
3
|
loadHashStore,
|
|
4
4
|
type HashStore,
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
upsertSnapshot,
|
|
7
7
|
} from "../hash-store";
|
|
8
8
|
import { xxh32, contentChecksum, initHasher } from "./hasher";
|
|
9
|
-
|
|
10
9
|
export { initHasher };
|
|
11
10
|
|
|
12
11
|
export const HASH_LEN = 3;
|
|
@@ -22,21 +21,19 @@ const ALPH_SAFE = ALPH.replace(/-/g, "\\-");
|
|
|
22
21
|
const ALPH_RE = new RegExp(`^[${ALPH_SAFE}]+$`);
|
|
23
22
|
export const HASH_CLASS = `[${ALPH_SAFE}]{${HASH_LEN}}`;
|
|
24
23
|
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
out +=
|
|
32
|
-
ALPH[
|
|
33
|
-
(n >>> ((HASH_LEN - 1 - j) * ALPH_BITS)) &
|
|
34
|
-
ALPH_MASK
|
|
35
|
-
]!;
|
|
36
|
-
}
|
|
37
|
-
return out;
|
|
24
|
+
function idxToHash(idx: number): string {
|
|
25
|
+
let out = "";
|
|
26
|
+
for (let j = 0; j < HASH_LEN; j++) {
|
|
27
|
+
out += ALPH[(idx >>> ((HASH_LEN - 1 - j) * ALPH_BITS)) & ALPH_MASK]!;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
38
30
|
}
|
|
39
31
|
|
|
32
|
+
const HASH_TABLE: string[] = Array.from(
|
|
33
|
+
{ length: 262_144 },
|
|
34
|
+
(_, i) => idxToHash(i),
|
|
35
|
+
);
|
|
36
|
+
|
|
40
37
|
export const HL_PREFIX_RE = new RegExp(
|
|
41
38
|
`^\\s*(?:>>>|>>)?\\s*${HASH_CLASS}│`,
|
|
42
39
|
);
|
|
@@ -47,33 +44,85 @@ export const DIFF_MINUS_RE = /^-\s*\d+\s{4}/;
|
|
|
47
44
|
|
|
48
45
|
export const HL_BARE_PREFIX_RE = new RegExp(`^\\s*(${HASH_CLASS})│`);
|
|
49
46
|
|
|
50
|
-
|
|
51
47
|
function canon(line: string): string {
|
|
52
48
|
return line.replace(/\r/g, "").trimEnd();
|
|
53
49
|
}
|
|
54
50
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
51
|
+
const BITSET_WORDS = 8192;
|
|
52
|
+
|
|
53
|
+
function getBit(bits: Uint32Array, idx: number): boolean {
|
|
54
|
+
return (bits[idx >>> 5] >>> (idx & 31) & 1) !== 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function setBit(bits: Uint32Array, idx: number): void {
|
|
58
|
+
bits[idx >>> 5] |= 1 << (idx & 31);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function nextZeroBit(bits: Uint32Array, start: number): number {
|
|
62
|
+
const totalWords = bits.length;
|
|
63
|
+
const totalBits = totalWords * 32;
|
|
64
|
+
|
|
65
|
+
if (start >= totalBits) start = 0;
|
|
66
|
+
|
|
67
|
+
const wordIdx = start >>> 5;
|
|
68
|
+
const bitOffset = start & 31;
|
|
69
|
+
|
|
70
|
+
let word = bits[wordIdx];
|
|
71
|
+
for (let b = bitOffset; b < 32; b++) {
|
|
72
|
+
if ((word >>> b & 1) === 0) return wordIdx * 32 + b;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (let w = wordIdx + 1; w < totalWords; w++) {
|
|
76
|
+
word = bits[w];
|
|
77
|
+
if (~word !== 0) {
|
|
78
|
+
for (let b = 0; b < 32; b++) {
|
|
79
|
+
if ((word >>> b & 1) === 0) return w * 32 + b;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (let w = 0; w < wordIdx; w++) {
|
|
85
|
+
word = bits[w];
|
|
86
|
+
if (~word !== 0) {
|
|
87
|
+
for (let b = 0; b < 32; b++) {
|
|
88
|
+
if ((word >>> b & 1) === 0) return w * 32 + b;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
word = bits[wordIdx];
|
|
94
|
+
for (let b = 0; b < bitOffset; b++) {
|
|
95
|
+
if ((word >>> b & 1) === 0) return wordIdx * 32 + b;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
throw new Error("Hash space exhausted");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function assignHash(used: Uint32Array, baseIdx: number, hint: { value: number }): string {
|
|
102
|
+
if (!getBit(used, baseIdx)) {
|
|
103
|
+
setBit(used, baseIdx);
|
|
104
|
+
hint.value = baseIdx + 1;
|
|
105
|
+
return HASH_TABLE[baseIdx];
|
|
106
|
+
}
|
|
107
|
+
const start = hint.value > baseIdx + 1 ? hint.value : baseIdx + 1;
|
|
108
|
+
const nextIdx = nextZeroBit(used, start);
|
|
109
|
+
setBit(used, nextIdx);
|
|
110
|
+
hint.value = nextIdx + 1;
|
|
111
|
+
return HASH_TABLE[nextIdx];
|
|
65
112
|
}
|
|
66
113
|
|
|
67
114
|
export function _lineHashesPure(content: string): string[] {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
115
|
+
const lines = splitLines(content);
|
|
116
|
+
const hashes = new Array<string>(lines.length);
|
|
117
|
+
const used = new Uint32Array(BITSET_WORDS);
|
|
118
|
+
const hint = { value: 0 };
|
|
119
|
+
|
|
120
|
+
for (let i = 0; i < lines.length; i++) {
|
|
121
|
+
const c = canon(lines[i]!);
|
|
122
|
+
const baseIdx = xxh32(c) >>> 14;
|
|
123
|
+
hashes[i] = assignHash(used, baseIdx, hint);
|
|
124
|
+
}
|
|
125
|
+
return hashes;
|
|
77
126
|
}
|
|
78
127
|
|
|
79
128
|
export async function lineHashes(
|
|
@@ -96,7 +145,7 @@ export async function lineHashes(
|
|
|
96
145
|
previous.removedHashes,
|
|
97
146
|
);
|
|
98
147
|
if (persist !== false) {
|
|
99
|
-
upsertSnapshot(hashStore, path, contentChecksum(content), content
|
|
148
|
+
upsertSnapshot(hashStore, path, contentChecksum(content), splitLines(content).length, newHashes);
|
|
100
149
|
}
|
|
101
150
|
return newHashes;
|
|
102
151
|
}
|
|
@@ -108,23 +157,41 @@ export async function lineHashes(
|
|
|
108
157
|
|
|
109
158
|
const newHashes = _lineHashesPure(content);
|
|
110
159
|
if (persist !== false) {
|
|
111
|
-
upsertSnapshot(hashStore, path, contentChecksum(content), content
|
|
160
|
+
upsertSnapshot(hashStore, path, contentChecksum(content), splitLines(content).length, newHashes);
|
|
112
161
|
}
|
|
113
162
|
return newHashes;
|
|
114
163
|
}
|
|
115
164
|
|
|
165
|
+
function hashToIndex(hash: string): number {
|
|
166
|
+
let idx = 0;
|
|
167
|
+
for (let j = 0; j < HASH_LEN; j++) {
|
|
168
|
+
const charIdx = ALPH.indexOf(hash[j]!);
|
|
169
|
+
if (charIdx < 0) return -1;
|
|
170
|
+
idx = (idx << ALPH_BITS) | charIdx;
|
|
171
|
+
}
|
|
172
|
+
return idx;
|
|
173
|
+
}
|
|
174
|
+
|
|
116
175
|
function mapStableHashes(
|
|
117
176
|
oldContent: string,
|
|
118
177
|
oldHashes: string[],
|
|
119
178
|
newContent: string,
|
|
120
179
|
removedHashes?: Set<string>,
|
|
121
180
|
): string[] {
|
|
122
|
-
const newLines = newContent
|
|
181
|
+
const newLines = splitLines(newContent);
|
|
123
182
|
const newHashes = new Array<string>(newLines.length);
|
|
124
|
-
const used = new
|
|
183
|
+
const used = new Uint32Array(BITSET_WORDS);
|
|
184
|
+
const hint = { value: 0 };
|
|
185
|
+
|
|
186
|
+
if (removedHashes) {
|
|
187
|
+
for (const hash of removedHashes) {
|
|
188
|
+
const idx = hashToIndex(hash);
|
|
189
|
+
if (idx >= 0) setBit(used, idx);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
125
192
|
|
|
126
193
|
const contentMap = new Map<string, { index: number; hash: string }[]>();
|
|
127
|
-
const oldLines = oldContent
|
|
194
|
+
const oldLines = splitLines(oldContent);
|
|
128
195
|
for (let i = 0; i < oldLines.length; i++) {
|
|
129
196
|
const line = oldLines[i]!;
|
|
130
197
|
const entry = { index: i, hash: oldHashes[i]! };
|
|
@@ -155,14 +222,18 @@ function mapStableHashes(
|
|
|
155
222
|
if (removedHashes?.has(candidates[bestIdx]!.hash)) continue;
|
|
156
223
|
const match = candidates.splice(bestIdx, 1)[0]!;
|
|
157
224
|
newHashes[i] = match.hash;
|
|
158
|
-
|
|
225
|
+
const matchIdx = hashToIndex(match.hash);
|
|
226
|
+
if (matchIdx >= 0) {
|
|
227
|
+
setBit(used, matchIdx);
|
|
228
|
+
if (matchIdx + 1 > hint.value) hint.value = matchIdx + 1;
|
|
229
|
+
}
|
|
159
230
|
}
|
|
160
231
|
|
|
161
232
|
for (let i = 0; i < newLines.length; i++) {
|
|
162
233
|
if (newHashes[i]) continue;
|
|
163
234
|
const c = canon(newLines[i]!);
|
|
164
|
-
const
|
|
165
|
-
newHashes[i] =
|
|
235
|
+
const baseIdx = xxh32(c) >>> 14;
|
|
236
|
+
newHashes[i] = assignHash(used, baseIdx, hint);
|
|
166
237
|
}
|
|
167
238
|
return newHashes;
|
|
168
239
|
}
|
package/src/replace-undo.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { contentChecksum } from "./hashline/hasher";
|
|
|
7
7
|
import { resolveTarget, writeAtomic } from "./fs-write";
|
|
8
8
|
import { toCwd } from "./paths";
|
|
9
9
|
import { toLF, stripBOM, genDiff, restoreEndings } from "./replace-diff";
|
|
10
|
-
import { cntDiff } from "./utils";
|
|
10
|
+
import { cntDiff, splitLines } from "./utils";
|
|
11
11
|
import { loadP, loadGuide } from "./prompts";
|
|
12
12
|
import { buildMetrics } from "./replace-response";
|
|
13
13
|
export interface UndoEntry {
|
|
@@ -84,7 +84,7 @@ export function regReplaceUndo(pi: ExtensionAPI): void {
|
|
|
84
84
|
);
|
|
85
85
|
|
|
86
86
|
const store = await loadHashStore();
|
|
87
|
-
upsertSnapshot(store, mutationTargetPath, contentChecksum(undo.content), undo.content
|
|
87
|
+
upsertSnapshot(store, mutationTargetPath, contentChecksum(undo.content), splitLines(undo.content).length, undo.hashes);
|
|
88
88
|
|
|
89
89
|
clearUndo(mutationTargetPath);
|
|
90
90
|
|
package/src/replace.ts
CHANGED
|
@@ -148,6 +148,7 @@ export function assertReq(
|
|
|
148
148
|
throw new Error('[E_BAD_SHAPE] Edit request requires a "changes" array. Each change is { content_lines: [...], hash_range_inclusive: ["<START>", "<END>"] }.');
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
+
|
|
151
152
|
export interface ExecPipelineOptions {
|
|
152
153
|
accessMode?: number;
|
|
153
154
|
signal?: AbortSignal;
|
|
@@ -155,6 +156,46 @@ export interface ExecPipelineOptions {
|
|
|
155
156
|
noPersist?: boolean;
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
function collectRemovedHashes(
|
|
160
|
+
resolved: { hash_range_inclusive: [{ hash: string }, { hash: string }] }[],
|
|
161
|
+
originalHashes: string[],
|
|
162
|
+
): Set<string> {
|
|
163
|
+
const removedHashes = new Set<string>();
|
|
164
|
+
for (const edit of resolved) {
|
|
165
|
+
const startHash = edit.hash_range_inclusive[0].hash;
|
|
166
|
+
const endHash = edit.hash_range_inclusive[1].hash;
|
|
167
|
+
const startLine = originalHashes.indexOf(startHash);
|
|
168
|
+
const endLine = originalHashes.indexOf(endHash);
|
|
169
|
+
if (startLine >= 0 && endLine >= 0) {
|
|
170
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
171
|
+
removedHashes.add(originalHashes[i]!);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return removedHashes;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function countLineChanges(
|
|
179
|
+
resolved: { hash_range_inclusive: [{ hash: string }, { hash: string }]; content_lines: string[] }[],
|
|
180
|
+
originalHashes: string[],
|
|
181
|
+
noopEdits: { editIndex: number }[] | undefined,
|
|
182
|
+
): { totalAddedLines: number; totalRemovedLines: number } {
|
|
183
|
+
let totalAddedLines = 0;
|
|
184
|
+
let totalRemovedLines = 0;
|
|
185
|
+
const noopIndices = new Set(noopEdits?.map((n) => n.editIndex) ?? []);
|
|
186
|
+
for (let i = 0; i < resolved.length; i++) {
|
|
187
|
+
if (noopIndices.has(i)) continue;
|
|
188
|
+
const edit = resolved[i]!;
|
|
189
|
+
const startLine = originalHashes.indexOf(edit.hash_range_inclusive[0].hash);
|
|
190
|
+
const endLine = originalHashes.indexOf(edit.hash_range_inclusive[1].hash);
|
|
191
|
+
if (startLine >= 0 && endLine >= 0) {
|
|
192
|
+
totalRemovedLines += endLine - startLine + 1;
|
|
193
|
+
}
|
|
194
|
+
totalAddedLines += edit.content_lines.length;
|
|
195
|
+
}
|
|
196
|
+
return { totalAddedLines, totalRemovedLines };
|
|
197
|
+
}
|
|
198
|
+
|
|
158
199
|
export async function execPipeline(
|
|
159
200
|
params: ReqParams,
|
|
160
201
|
cwd: string,
|
|
@@ -187,18 +228,7 @@ export async function execPipeline(
|
|
|
187
228
|
|
|
188
229
|
const result = anchorResult.content;
|
|
189
230
|
|
|
190
|
-
const removedHashes =
|
|
191
|
-
for (const edit of resolved) {
|
|
192
|
-
const startHash = edit.hash_range_inclusive[0].hash;
|
|
193
|
-
const endHash = edit.hash_range_inclusive[1].hash;
|
|
194
|
-
const startLine = originalHashes.indexOf(startHash);
|
|
195
|
-
const endLine = originalHashes.indexOf(endHash);
|
|
196
|
-
if (startLine >= 0 && endLine >= 0) {
|
|
197
|
-
for (let i = startLine; i <= endLine; i++) {
|
|
198
|
-
removedHashes.add(originalHashes[i]!);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
231
|
+
const removedHashes = collectRemovedHashes(resolved, originalHashes);
|
|
202
232
|
|
|
203
233
|
const noPersist = options?.noPersist;
|
|
204
234
|
const resultHashes = await lineHashes(result, absolutePath, {
|
|
@@ -209,19 +239,9 @@ export async function execPipeline(
|
|
|
209
239
|
|
|
210
240
|
const warnings = [...(anchorResult.warnings ?? [])];
|
|
211
241
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
for (let i = 0; i < resolved.length; i++) {
|
|
216
|
-
if (noopIndices.has(i)) continue;
|
|
217
|
-
const edit = resolved[i]!;
|
|
218
|
-
const startLine = originalHashes.indexOf(edit.hash_range_inclusive[0].hash);
|
|
219
|
-
const endLine = originalHashes.indexOf(edit.hash_range_inclusive[1].hash);
|
|
220
|
-
if (startLine >= 0 && endLine >= 0) {
|
|
221
|
-
totalRemovedLines += endLine - startLine + 1;
|
|
222
|
-
}
|
|
223
|
-
totalAddedLines += edit.content_lines.length;
|
|
224
|
-
}
|
|
242
|
+
const { totalAddedLines, totalRemovedLines } = countLineChanges(
|
|
243
|
+
resolved, originalHashes, anchorResult.noopEdits,
|
|
244
|
+
);
|
|
225
245
|
|
|
226
246
|
return {
|
|
227
247
|
path,
|
|
@@ -289,6 +309,7 @@ export function reuseMarkdown(context: any, content: string, theme: any): Markdo
|
|
|
289
309
|
m.setText(content);
|
|
290
310
|
return m;
|
|
291
311
|
}
|
|
312
|
+
|
|
292
313
|
const MODE_CFG = {
|
|
293
314
|
flat: {
|
|
294
315
|
desc: " Only one edit per call. The `hash_range_inclusive` and `content_lines` fields sit at the top level of the request object.",
|
|
@@ -435,7 +456,6 @@ export function buildToolDef(opts: { flat: boolean; autoRead?: boolean }): ToolD
|
|
|
435
456
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
436
457
|
const canonical = normReq(params);
|
|
437
458
|
|
|
438
|
-
|
|
439
459
|
const normalizedParams = canonical as { path: string; changes: HTEdit[] };
|
|
440
460
|
const path = normalizedParams.path;
|
|
441
461
|
const absolutePath = toCwd(path, ctx.cwd);
|
package/src/utils.ts
CHANGED
|
@@ -6,6 +6,12 @@ export function has(record: Record<string, unknown>, key: string): boolean {
|
|
|
6
6
|
return Object.hasOwn(record, key);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export function splitLines(text: string): string[] {
|
|
10
|
+
if (text.length === 0) return [""];
|
|
11
|
+
const lines = text.split("\n");
|
|
12
|
+
return text.endsWith("\n") ? lines.slice(0, -1) : lines;
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
export function visLines(text: string): string[] {
|
|
10
16
|
if (text.length === 0) return [];
|
|
11
17
|
const lines = text.split("\n");
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
const fs = require("fs");
|
|
2
|
-
const path = require("path");
|
|
3
|
-
|
|
4
|
-
function cleanNodeModules(nm) {
|
|
5
|
-
if (!fs.existsSync(nm)) return;
|
|
6
|
-
const entries = fs.readdirSync(nm, { withFileTypes: true });
|
|
7
|
-
for (const entry of entries) {
|
|
8
|
-
if (entry.name.startsWith(".better-sqlite3-")) {
|
|
9
|
-
const full = path.join(nm, entry.name);
|
|
10
|
-
try {
|
|
11
|
-
fs.rmSync(full, { recursive: true, force: true });
|
|
12
|
-
console.error("Cleaned up stale better-sqlite3 build artifact:", entry.name);
|
|
13
|
-
} catch {
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const pkgDir = path.resolve(__dirname, "..");
|
|
20
|
-
cleanNodeModules(path.resolve(pkgDir, "node_modules"));
|
|
21
|
-
cleanNodeModules(path.resolve(pkgDir, ".."));
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
const { execSync } = require("child_process");
|
|
2
|
-
const fs = require("fs");
|
|
3
|
-
const path = require("path");
|
|
4
|
-
|
|
5
|
-
const root = path.resolve(__dirname, "..");
|
|
6
|
-
const bsqlDir = path.join(root, "node_modules", "better-sqlite3");
|
|
7
|
-
|
|
8
|
-
function removeStaleArtifacts() {
|
|
9
|
-
for (const dir of [root, bsqlDir]) {
|
|
10
|
-
if (!fs.existsSync(dir)) continue;
|
|
11
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
12
|
-
for (const entry of entries) {
|
|
13
|
-
if (entry.name.startsWith(".better-sqlite3-")) {
|
|
14
|
-
const full = path.join(dir, entry.name);
|
|
15
|
-
try {
|
|
16
|
-
fs.rmSync(full, { recursive: true, force: true });
|
|
17
|
-
console.error("Removed stale artifact:", entry.name);
|
|
18
|
-
} catch {
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
const buildDir = path.join(bsqlDir, "build");
|
|
24
|
-
if (fs.existsSync(buildDir)) {
|
|
25
|
-
fs.rmSync(buildDir, { recursive: true, force: true });
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
require(bsqlDir);
|
|
31
|
-
process.exit(0);
|
|
32
|
-
} catch (e) {
|
|
33
|
-
const msg = e && typeof e.message === "string" ? e.message : String(e);
|
|
34
|
-
if (/GLIBC|Cannot find module|dlo|not found/i.test(msg)) {
|
|
35
|
-
console.error("better-sqlite3 prebuilt incompatible, rebuilding from source...");
|
|
36
|
-
removeStaleArtifacts();
|
|
37
|
-
const prebuildDir = path.join(bsqlDir, "prebuilds");
|
|
38
|
-
if (fs.existsSync(prebuildDir)) {
|
|
39
|
-
const platform = process.platform + "-" + process.arch;
|
|
40
|
-
const prebuilt = path.join(prebuildDir, platform + ".node");
|
|
41
|
-
if (fs.existsSync(prebuilt)) {
|
|
42
|
-
fs.unlinkSync(prebuilt);
|
|
43
|
-
console.error("Removed incompatible prebuilt:", platform + ".node");
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
execSync("npx --yes node-gyp rebuild", {
|
|
49
|
-
cwd: bsqlDir,
|
|
50
|
-
stdio: "inherit",
|
|
51
|
-
timeout: 300000,
|
|
52
|
-
});
|
|
53
|
-
console.error("better-sqlite3 rebuilt successfully from source.");
|
|
54
|
-
} catch (rebuildErr) {
|
|
55
|
-
console.error("better-sqlite3 rebuild failed:", rebuildErr.message);
|
|
56
|
-
console.error("Will fall back to sql.js at runtime.");
|
|
57
|
-
process.exit(0);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|