hotsheet 0.18.1-beta.1 → 0.18.1-beta.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/cli.js +124 -21
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -15681,10 +15681,16 @@ __export(fsyncWrap_exports, {
|
|
|
15681
15681
|
fsyncDbDir: () => fsyncDbDir,
|
|
15682
15682
|
fsyncDbDirAsync: () => fsyncDbDirAsync,
|
|
15683
15683
|
fsyncDir: () => fsyncDir,
|
|
15684
|
-
fsyncDirAsync: () => fsyncDirAsync
|
|
15684
|
+
fsyncDirAsync: () => fsyncDirAsync,
|
|
15685
|
+
isUnsupportedFsyncError: () => isUnsupportedFsyncError
|
|
15685
15686
|
});
|
|
15686
15687
|
import { closeSync, existsSync as existsSync2, fsyncSync, openSync, promises as fsp2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
15687
15688
|
import { join as join2 } from "path";
|
|
15689
|
+
function isUnsupportedFsyncError(err, platform2 = process.platform) {
|
|
15690
|
+
if (platform2 !== "win32") return false;
|
|
15691
|
+
const code = err?.code;
|
|
15692
|
+
return code !== void 0 && WIN32_UNFLUSHABLE_CODES.has(code);
|
|
15693
|
+
}
|
|
15688
15694
|
function fsyncDir(path, fsyncFn = fsyncSync) {
|
|
15689
15695
|
if (!existsSync2(path)) return { filesFlushed: 0, errors: 0 };
|
|
15690
15696
|
const counters = { filesFlushed: 0, errors: 0 };
|
|
@@ -15719,8 +15725,10 @@ function walkAndFsync(dir, counters, fsyncFn) {
|
|
|
15719
15725
|
fsyncFn(fd);
|
|
15720
15726
|
counters.filesFlushed++;
|
|
15721
15727
|
} catch (err) {
|
|
15722
|
-
|
|
15723
|
-
|
|
15728
|
+
if (!isUnsupportedFsyncError(err)) {
|
|
15729
|
+
console.error(`[fsyncWrap] fsync ${p} failed:`, err);
|
|
15730
|
+
counters.errors++;
|
|
15731
|
+
}
|
|
15724
15732
|
} finally {
|
|
15725
15733
|
if (fd !== null) {
|
|
15726
15734
|
try {
|
|
@@ -15769,8 +15777,10 @@ async function walkAndFsyncAsync(dir, counters, fsyncFn) {
|
|
|
15769
15777
|
await fsyncFn(handle);
|
|
15770
15778
|
counters.filesFlushed++;
|
|
15771
15779
|
} catch (err) {
|
|
15772
|
-
|
|
15773
|
-
|
|
15780
|
+
if (!isUnsupportedFsyncError(err)) {
|
|
15781
|
+
console.error(`[fsyncWrap] fsync ${p} failed:`, err);
|
|
15782
|
+
counters.errors++;
|
|
15783
|
+
}
|
|
15774
15784
|
} finally {
|
|
15775
15785
|
if (handle !== null) {
|
|
15776
15786
|
try {
|
|
@@ -15785,10 +15795,11 @@ async function walkAndFsyncAsync(dir, counters, fsyncFn) {
|
|
|
15785
15795
|
async function fsyncDbDirAsync(dataDir, fsyncFn = defaultAsyncFsyncFn) {
|
|
15786
15796
|
return fsyncDirAsync(join2(dataDir, "db"), fsyncFn);
|
|
15787
15797
|
}
|
|
15788
|
-
var defaultAsyncFsyncFn;
|
|
15798
|
+
var WIN32_UNFLUSHABLE_CODES, defaultAsyncFsyncFn;
|
|
15789
15799
|
var init_fsyncWrap = __esm({
|
|
15790
15800
|
"src/db/fsyncWrap.ts"() {
|
|
15791
15801
|
"use strict";
|
|
15802
|
+
WIN32_UNFLUSHABLE_CODES = /* @__PURE__ */ new Set(["EPERM", "EACCES", "ENOTSUP", "EINVAL"]);
|
|
15792
15803
|
defaultAsyncFsyncFn = (handle) => handle.sync();
|
|
15793
15804
|
}
|
|
15794
15805
|
});
|
|
@@ -16361,13 +16372,40 @@ function clearRecoveryMarker(dataDir) {
|
|
|
16361
16372
|
} catch {
|
|
16362
16373
|
}
|
|
16363
16374
|
}
|
|
16375
|
+
function pendingRecoveryPath(dataDir) {
|
|
16376
|
+
return join7(dataDir, PENDING_RECOVERY_FILENAME);
|
|
16377
|
+
}
|
|
16378
|
+
function readPendingRecovery(dataDir) {
|
|
16379
|
+
const path = pendingRecoveryPath(dataDir);
|
|
16380
|
+
if (!existsSync6(path)) return null;
|
|
16381
|
+
try {
|
|
16382
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
16383
|
+
const result = external_exports.object({ attempts: external_exports.number() }).loose().safeParse(parsed);
|
|
16384
|
+
return { attempts: result.success ? result.data.attempts : 1 };
|
|
16385
|
+
} catch {
|
|
16386
|
+
return { attempts: 1 };
|
|
16387
|
+
}
|
|
16388
|
+
}
|
|
16389
|
+
function writePendingRecovery(dataDir, attempts) {
|
|
16390
|
+
try {
|
|
16391
|
+
writeFileSync2(pendingRecoveryPath(dataDir), JSON.stringify({ attempts, requestedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
16392
|
+
} catch (writeErr) {
|
|
16393
|
+
console.error("Could not write pending-recovery marker:", getErrorMessage(writeErr));
|
|
16394
|
+
}
|
|
16395
|
+
}
|
|
16396
|
+
function clearPendingRecovery(dataDir) {
|
|
16397
|
+
try {
|
|
16398
|
+
rmSync2(pendingRecoveryPath(dataDir), { force: true });
|
|
16399
|
+
} catch {
|
|
16400
|
+
}
|
|
16401
|
+
}
|
|
16364
16402
|
function runWithDataDir(dataDir, fn) {
|
|
16365
16403
|
return requestDataDir.run(dataDir, fn);
|
|
16366
16404
|
}
|
|
16367
16405
|
function getDataDir() {
|
|
16368
16406
|
const contextDataDir = requestDataDir.getStore();
|
|
16369
16407
|
if (contextDataDir !== void 0) return contextDataDir;
|
|
16370
|
-
if (defaultDbPath !== null) return defaultDbPath.replace(
|
|
16408
|
+
if (defaultDbPath !== null) return defaultDbPath.replace(/[\\/]db$/, "");
|
|
16371
16409
|
throw new Error("Data directory not available. Call setDataDir() or use runWithDataDir().");
|
|
16372
16410
|
}
|
|
16373
16411
|
function setDataDir(dataDir) {
|
|
@@ -16439,6 +16477,8 @@ async function getDbForDir(dataDir) {
|
|
|
16439
16477
|
async function getDbByPath(dbPath) {
|
|
16440
16478
|
const existing = databases.get(dbPath);
|
|
16441
16479
|
if (existing) return existing;
|
|
16480
|
+
const recovered = await completeDeferredRecovery(dbPath);
|
|
16481
|
+
if (recovered !== null) return recovered;
|
|
16442
16482
|
let db;
|
|
16443
16483
|
try {
|
|
16444
16484
|
db = await openAndCacheDb(dbPath);
|
|
@@ -16461,8 +16501,16 @@ async function getDbByPath(dbPath) {
|
|
|
16461
16501
|
}
|
|
16462
16502
|
async function openAndCacheDb(dbPath, loadDataDir) {
|
|
16463
16503
|
const db = createPglite(dbPath, loadDataDir !== void 0 ? { loadDataDir } : {});
|
|
16464
|
-
|
|
16465
|
-
|
|
16504
|
+
try {
|
|
16505
|
+
await db.waitReady;
|
|
16506
|
+
await initSchema(db);
|
|
16507
|
+
} catch (err) {
|
|
16508
|
+
try {
|
|
16509
|
+
await db.close();
|
|
16510
|
+
} catch {
|
|
16511
|
+
}
|
|
16512
|
+
throw err;
|
|
16513
|
+
}
|
|
16466
16514
|
databases.set(dbPath, db);
|
|
16467
16515
|
return db;
|
|
16468
16516
|
}
|
|
@@ -16506,6 +16554,57 @@ async function tryRestoreFromSources(dbPath, dataDir) {
|
|
|
16506
16554
|
}
|
|
16507
16555
|
return null;
|
|
16508
16556
|
}
|
|
16557
|
+
async function renameDirWithRetry(from, to) {
|
|
16558
|
+
const maxAttempts = 5;
|
|
16559
|
+
for (let attempt = 1; ; attempt++) {
|
|
16560
|
+
try {
|
|
16561
|
+
renameSync(from, to);
|
|
16562
|
+
return;
|
|
16563
|
+
} catch (renameErr) {
|
|
16564
|
+
const code = renameErr.code;
|
|
16565
|
+
const retryable = code === "EPERM" || code === "EBUSY" || code === "EACCES" || code === "ENOTEMPTY";
|
|
16566
|
+
if (!retryable || attempt >= maxAttempts) throw renameErr;
|
|
16567
|
+
await new Promise((resolve11) => setTimeout(resolve11, 100 * attempt));
|
|
16568
|
+
}
|
|
16569
|
+
}
|
|
16570
|
+
}
|
|
16571
|
+
async function completeDeferredRecovery(dbPath) {
|
|
16572
|
+
const dataDir = dbPath.replace(/[\\/]db$/, "");
|
|
16573
|
+
const pending = readPendingRecovery(dataDir);
|
|
16574
|
+
if (pending === null) return null;
|
|
16575
|
+
if (pending.attempts > MAX_DEFERRED_RECOVERY_ATTEMPTS) {
|
|
16576
|
+
console.error(`[db] deferred recovery gave up after ${String(pending.attempts)} attempts; leaving the corrupt cluster for manual rescue.`);
|
|
16577
|
+
clearPendingRecovery(dataDir);
|
|
16578
|
+
return null;
|
|
16579
|
+
}
|
|
16580
|
+
if (!existsSync6(dbPath)) {
|
|
16581
|
+
clearPendingRecovery(dataDir);
|
|
16582
|
+
return null;
|
|
16583
|
+
}
|
|
16584
|
+
console.error("[db] completing deferred recovery \u2014 a prior launch could not move the corrupt database aside in-process (Windows handle lock)\u2026");
|
|
16585
|
+
const corruptPath = `${dbPath}-corrupt-${Date.now()}`;
|
|
16586
|
+
try {
|
|
16587
|
+
await renameDirWithRetry(dbPath, corruptPath);
|
|
16588
|
+
} catch (renameErr) {
|
|
16589
|
+
writePendingRecovery(dataDir, pending.attempts + 1);
|
|
16590
|
+
console.error(`[db] deferred recovery could not move db/ yet: ${getErrorMessage(renameErr)}`);
|
|
16591
|
+
return null;
|
|
16592
|
+
}
|
|
16593
|
+
const restored = await tryRestoreFromSources(dbPath, dataDir);
|
|
16594
|
+
writeRecoveryMarker(dataDir, {
|
|
16595
|
+
corruptPath,
|
|
16596
|
+
recoveredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16597
|
+
errorMessage: "Database was corrupt and could not be preserved in-process (Windows handle lock); recovered on the next restart.",
|
|
16598
|
+
...restored !== null ? { restoredFrom: restored.label, restoredTicketCount: restored.ticketCount } : {}
|
|
16599
|
+
});
|
|
16600
|
+
clearPendingRecovery(dataDir);
|
|
16601
|
+
if (restored !== null) {
|
|
16602
|
+
console.error(`[db] deferred recovery restored from ${restored.label} (${String(restored.ticketCount)} tickets).`);
|
|
16603
|
+
return restored.db;
|
|
16604
|
+
}
|
|
16605
|
+
console.error("[db] deferred recovery: no snapshot/backup could be loaded; starting with a fresh empty database.");
|
|
16606
|
+
return null;
|
|
16607
|
+
}
|
|
16509
16608
|
async function recoverFromOpenFailure(dbPath, err, forceRecover) {
|
|
16510
16609
|
const message = getErrorMessage(err);
|
|
16511
16610
|
const stack = err instanceof Error ? err.stack : void 0;
|
|
@@ -16530,16 +16629,18 @@ async function recoverFromOpenFailure(dbPath, err, forceRecover) {
|
|
|
16530
16629
|
}
|
|
16531
16630
|
}
|
|
16532
16631
|
}
|
|
16632
|
+
const dataDir = dbPath.replace(/[\\/]db$/, "");
|
|
16533
16633
|
const corruptPath = `${dbPath}-corrupt-${Date.now()}`;
|
|
16534
16634
|
console.error(`Database appears to be corrupt. Preserving as ${corruptPath} ...`);
|
|
16535
16635
|
try {
|
|
16536
|
-
|
|
16636
|
+
await renameDirWithRetry(dbPath, corruptPath);
|
|
16537
16637
|
} catch (renameErr) {
|
|
16538
16638
|
const renameMessage = getErrorMessage(renameErr);
|
|
16539
|
-
|
|
16639
|
+
const prev = readPendingRecovery(dataDir);
|
|
16640
|
+
writePendingRecovery(dataDir, (prev?.attempts ?? 0) + 1);
|
|
16641
|
+
console.error(`Could not preserve corrupt database directory in-process: ${renameMessage}. Wrote a pending-recovery marker \u2014 Hot Sheet will auto-recover from the latest snapshot on the next restart.`);
|
|
16540
16642
|
throw err;
|
|
16541
16643
|
}
|
|
16542
|
-
const dataDir = dbPath.replace(/[\\/]db$/, "");
|
|
16543
16644
|
const restored = await tryRestoreFromSources(dbPath, dataDir);
|
|
16544
16645
|
if (restored !== null) {
|
|
16545
16646
|
writeRecoveryMarker(dataDir, {
|
|
@@ -16808,7 +16909,7 @@ async function migrateNoteIds(db) {
|
|
|
16808
16909
|
}
|
|
16809
16910
|
}
|
|
16810
16911
|
}
|
|
16811
|
-
var SCHEMA_VERSION, RECOVERY_MARKER_FILENAME, databases, defaultDbPath, requestDataDir;
|
|
16912
|
+
var SCHEMA_VERSION, RECOVERY_MARKER_FILENAME, PENDING_RECOVERY_FILENAME, MAX_DEFERRED_RECOVERY_ATTEMPTS, databases, defaultDbPath, requestDataDir;
|
|
16812
16913
|
var init_connection = __esm({
|
|
16813
16914
|
"src/db/connection.ts"() {
|
|
16814
16915
|
"use strict";
|
|
@@ -16817,6 +16918,8 @@ var init_connection = __esm({
|
|
|
16817
16918
|
init_pglite();
|
|
16818
16919
|
SCHEMA_VERSION = 4;
|
|
16819
16920
|
RECOVERY_MARKER_FILENAME = ".db-recovery-marker.json";
|
|
16921
|
+
PENDING_RECOVERY_FILENAME = ".db-pending-recovery.json";
|
|
16922
|
+
MAX_DEFERRED_RECOVERY_ATTEMPTS = 3;
|
|
16820
16923
|
databases = /* @__PURE__ */ new Map();
|
|
16821
16924
|
defaultDbPath = null;
|
|
16822
16925
|
requestDataDir = new AsyncLocalStorage();
|
|
@@ -20394,7 +20497,7 @@ function claudeAllowRulePattern(dataDir) {
|
|
|
20394
20497
|
return `mcp__${getMcpServerKey(dataDir)}__*`;
|
|
20395
20498
|
}
|
|
20396
20499
|
function projectRoot(dataDir) {
|
|
20397
|
-
return dataDir.replace(
|
|
20500
|
+
return dataDir.replace(/[\\/]\.hotsheet[\\/]?$/, "");
|
|
20398
20501
|
}
|
|
20399
20502
|
function claudeDir(dataDir) {
|
|
20400
20503
|
return join18(projectRoot(dataDir), ".claude");
|
|
@@ -20514,7 +20617,7 @@ function getChannelServerPath() {
|
|
|
20514
20617
|
return { command: process.execPath, args: [distPath] };
|
|
20515
20618
|
}
|
|
20516
20619
|
function projectRoot2(dataDir) {
|
|
20517
|
-
return dataDir.replace(
|
|
20620
|
+
return dataDir.replace(/[\\/]\.hotsheet[\\/]?$/, "");
|
|
20518
20621
|
}
|
|
20519
20622
|
function registerChannel(dataDir) {
|
|
20520
20623
|
const root2 = projectRoot2(dataDir);
|
|
@@ -33197,18 +33300,18 @@ function isDemoMode() {
|
|
|
33197
33300
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
33198
33301
|
var SHELL_PATH_TIMEOUT_MS = 2e3;
|
|
33199
33302
|
function mergePaths(currentPath, shellPath) {
|
|
33200
|
-
const
|
|
33201
|
-
const existing = currentPath.split(
|
|
33303
|
+
const sep2 = ":";
|
|
33304
|
+
const existing = currentPath.split(sep2).filter((s) => s !== "");
|
|
33202
33305
|
const existingSet = new Set(existing);
|
|
33203
33306
|
const additions = [];
|
|
33204
|
-
for (const dir of shellPath.split(
|
|
33307
|
+
for (const dir of shellPath.split(sep2)) {
|
|
33205
33308
|
const trimmed = dir.trim();
|
|
33206
33309
|
if (trimmed === "" || existingSet.has(trimmed)) continue;
|
|
33207
33310
|
existingSet.add(trimmed);
|
|
33208
33311
|
additions.push(trimmed);
|
|
33209
33312
|
}
|
|
33210
33313
|
if (additions.length === 0) return currentPath;
|
|
33211
|
-
return [...additions, ...existing].join(
|
|
33314
|
+
return [...additions, ...existing].join(sep2);
|
|
33212
33315
|
}
|
|
33213
33316
|
function readLoginShellPath(execOverride) {
|
|
33214
33317
|
const shell = process.env.SHELL;
|
|
@@ -33280,7 +33383,7 @@ init_helpers();
|
|
|
33280
33383
|
init_notify();
|
|
33281
33384
|
import { existsSync as existsSync19, mkdirSync as mkdirSync11, readFileSync as readFileSync14, rmSync as rmSync7, writeFileSync as writeFileSync13 } from "fs";
|
|
33282
33385
|
import { Hono as Hono2 } from "hono";
|
|
33283
|
-
import { basename as basename3, extname, join as join26, resolve as resolve7 } from "path";
|
|
33386
|
+
import { basename as basename3, extname, join as join26, resolve as resolve7, sep } from "path";
|
|
33284
33387
|
var attachmentRoutes = new Hono2();
|
|
33285
33388
|
attachmentRoutes.post("/tickets/:id/attachments", async (c) => {
|
|
33286
33389
|
const id = parseIntParam(c, "id");
|
|
@@ -33367,7 +33470,7 @@ attachmentRoutes.get("/attachments/file/*", (c) => {
|
|
|
33367
33470
|
const dataDir = c.get("dataDir");
|
|
33368
33471
|
const attachDir = resolve7(join26(dataDir, "attachments"));
|
|
33369
33472
|
const fullPath = resolve7(join26(attachDir, filePath));
|
|
33370
|
-
if (!fullPath.startsWith(attachDir +
|
|
33473
|
+
if (!fullPath.startsWith(attachDir + sep) && fullPath !== attachDir) {
|
|
33371
33474
|
return c.json({ error: "Invalid path" }, 403);
|
|
33372
33475
|
}
|
|
33373
33476
|
if (!existsSync19(fullPath)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hotsheet",
|
|
3
|
-
"version": "0.18.1-beta.
|
|
3
|
+
"version": "0.18.1-beta.3",
|
|
4
4
|
"description": "A lightweight local project management tool. Create, categorize, and prioritize tickets with a fast bullet-list interface, then export an Up Next worklist for AI tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"scripts": {
|
|
31
31
|
"dev": "npm run build:client && npm run build:plugins && tsx --tsconfig tsconfig.json src/cli.ts --replace",
|
|
32
32
|
"build": "tsup",
|
|
33
|
-
"build:client": "
|
|
33
|
+
"build:client": "node scripts/build-client.mjs",
|
|
34
34
|
"build:plugins": "for d in plugins/*/; do if [ -f \"$d/manifest.json\" ]; then name=$(basename $d); mkdir -p dist/plugins/$name && npx esbuild $d/src/index.ts --bundle --format=esm --outfile=dist/plugins/$name/index.js --platform=node --target=node20 && cp $d/manifest.json dist/plugins/$name/; fi; done",
|
|
35
35
|
"test": "vitest run --coverage",
|
|
36
36
|
"test:watch": "vitest",
|