pi-hashline-edit-pro 0.17.13 → 0.18.0
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/package.json +4 -12
- package/src/hash-store.ts +43 -179
- package/scripts/cleanup-better-sqlite3.cjs +0 -21
- package/scripts/ensure-better-sqlite3.cjs +0 -60
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
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/hash-store.ts
CHANGED
|
@@ -1,10 +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
5
|
import { errCode } 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
8
|
|
|
9
9
|
type SqlParams = (string | number)[];
|
|
10
10
|
|
|
@@ -17,7 +17,7 @@ interface Prepared {
|
|
|
17
17
|
|
|
18
18
|
export interface HashStore {
|
|
19
19
|
readonly stmts: Prepared;
|
|
20
|
-
readonly engine: "
|
|
20
|
+
readonly engine: "node:sqlite";
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
interface LegacySnapshot {
|
|
@@ -36,38 +36,15 @@ function isValidSnapshot(value: unknown): value is LegacySnapshot {
|
|
|
36
36
|
return true;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
let cachedDb: { path: string; db: DatabaseSync; stmts: Prepared } | null = null;
|
|
39
40
|
|
|
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}`);
|
|
41
|
+
function openDb(storePath: string): { db: DatabaseSync; stmts: Prepared } {
|
|
42
|
+
const db = new DatabaseSync(storePath, {
|
|
43
|
+
timeout: HASH_STORE_BUSY_TIMEOUT,
|
|
44
|
+
defensive: false,
|
|
45
|
+
} as any);
|
|
46
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
47
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
71
48
|
db.exec(
|
|
72
49
|
"CREATE TABLE IF NOT EXISTS snapshots (" +
|
|
73
50
|
"path TEXT PRIMARY KEY, " +
|
|
@@ -93,169 +70,54 @@ function openBetterDb(storePath: string): BackendHandle {
|
|
|
93
70
|
upsert: (...params) => { upsertStmt.run(...params); },
|
|
94
71
|
};
|
|
95
72
|
|
|
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
|
-
};
|
|
73
|
+
return { db, stmts };
|
|
183
74
|
}
|
|
184
75
|
|
|
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
76
|
export async function loadHashStore(): Promise<HashStore> {
|
|
201
77
|
const storePath = hashStorePath();
|
|
202
|
-
if (
|
|
203
|
-
return
|
|
78
|
+
if (cachedDb && cachedDb.path === storePath && cachedDb.db.isOpen) {
|
|
79
|
+
return { stmts: cachedDb.stmts, engine: "node:sqlite" };
|
|
204
80
|
}
|
|
205
81
|
|
|
206
82
|
shutdownHashStore();
|
|
207
83
|
|
|
208
84
|
await initHasher();
|
|
209
85
|
await mkdir(hashStoreDir(), { recursive: true });
|
|
210
|
-
await initBackend();
|
|
211
86
|
|
|
212
87
|
const existed = existsSync(storePath);
|
|
213
|
-
|
|
214
|
-
let handle: BackendHandle;
|
|
215
|
-
if (BetterDatabase) {
|
|
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);
|
|
230
|
-
}
|
|
88
|
+
const { db, stmts } = openDb(storePath);
|
|
231
89
|
|
|
232
90
|
if (!existed) {
|
|
233
|
-
await migrateLegacy(
|
|
91
|
+
await migrateLegacy(db);
|
|
234
92
|
}
|
|
235
93
|
|
|
236
|
-
|
|
237
|
-
return
|
|
94
|
+
cachedDb = { path: storePath, db, stmts };
|
|
95
|
+
return { stmts, engine: "node:sqlite" };
|
|
238
96
|
}
|
|
239
97
|
|
|
240
98
|
export function shutdownHashStore(): void {
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
99
|
+
if (cachedDb) {
|
|
100
|
+
cachedDb.db.close();
|
|
101
|
+
cachedDb = null;
|
|
244
102
|
}
|
|
245
103
|
}
|
|
246
104
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
105
|
+
function withStore(fn: () => void): void {
|
|
106
|
+
if (cachedDb) {
|
|
107
|
+
cachedDb.db.exec("BEGIN IMMEDIATE");
|
|
108
|
+
try {
|
|
109
|
+
fn();
|
|
110
|
+
cachedDb.db.exec("COMMIT");
|
|
111
|
+
} catch (e) {
|
|
112
|
+
cachedDb.db.exec("ROLLBACK");
|
|
113
|
+
throw e;
|
|
114
|
+
}
|
|
253
115
|
} else {
|
|
254
116
|
fn();
|
|
255
117
|
}
|
|
256
118
|
}
|
|
257
119
|
|
|
258
|
-
async function migrateLegacy(
|
|
120
|
+
async function migrateLegacy(db: DatabaseSync): Promise<void> {
|
|
259
121
|
const legacyPath = legacyHashStorePath();
|
|
260
122
|
let content: string;
|
|
261
123
|
try {
|
|
@@ -290,13 +152,17 @@ async function migrateLegacy(handle: BackendHandle): Promise<void> {
|
|
|
290
152
|
}
|
|
291
153
|
|
|
292
154
|
if (rows.length > 0) {
|
|
293
|
-
|
|
294
|
-
|
|
155
|
+
db.exec("BEGIN IMMEDIATE");
|
|
156
|
+
try {
|
|
157
|
+
const stmt = db.prepare(
|
|
295
158
|
"INSERT OR REPLACE INTO snapshots (path, checksum, line_count, hashes, updated_at) VALUES (?, ?, ?, ?, ?)"
|
|
296
159
|
);
|
|
297
160
|
for (const row of rows) stmt.run(...row);
|
|
298
|
-
|
|
299
|
-
})
|
|
161
|
+
db.exec("COMMIT");
|
|
162
|
+
} catch (e) {
|
|
163
|
+
db.exec("ROLLBACK");
|
|
164
|
+
throw e;
|
|
165
|
+
}
|
|
300
166
|
}
|
|
301
167
|
|
|
302
168
|
try {
|
|
@@ -306,8 +172,6 @@ async function migrateLegacy(handle: BackendHandle): Promise<void> {
|
|
|
306
172
|
}
|
|
307
173
|
}
|
|
308
174
|
|
|
309
|
-
|
|
310
|
-
|
|
311
175
|
export function getSnapshot(
|
|
312
176
|
store: HashStore,
|
|
313
177
|
path: string,
|
|
@@ -327,13 +191,13 @@ export function upsertSnapshot(
|
|
|
327
191
|
hashes: string[],
|
|
328
192
|
): void {
|
|
329
193
|
const hashesJson = JSON.stringify(hashes);
|
|
330
|
-
withStore(
|
|
194
|
+
withStore(() => {
|
|
331
195
|
store.stmts.upsert(path, checksum, lineCount, hashesJson, Date.now());
|
|
332
196
|
});
|
|
333
197
|
}
|
|
334
198
|
|
|
335
199
|
export function deleteSnapshot(store: HashStore, path: string): void {
|
|
336
|
-
withStore(
|
|
200
|
+
withStore(() => {
|
|
337
201
|
store.stmts.deleteOne(path);
|
|
338
202
|
});
|
|
339
203
|
}
|
|
@@ -349,7 +213,7 @@ export async function pruneMissing(store: HashStore): Promise<void> {
|
|
|
349
213
|
}
|
|
350
214
|
}
|
|
351
215
|
if (missing.length === 0) return;
|
|
352
|
-
withStore(
|
|
216
|
+
withStore(() => {
|
|
353
217
|
for (const path of missing) store.stmts.deleteOne(path);
|
|
354
218
|
});
|
|
355
219
|
}
|
|
@@ -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, "..", "node_modules"));
|
|
@@ -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
|
-
}
|