mellos-mapping 0.24.0 → 0.26.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.
@@ -2,7 +2,7 @@
2
2
  import { PassThrough, Writable } from "node:stream";
3
3
 
4
4
  // src/watch/watch.ts
5
- import { realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
5
+ import { realpathSync as realpathSync3, statSync as statSync2 } from "node:fs";
6
6
  import { dirname as dirname9, join as join8 } from "node:path";
7
7
 
8
8
  // src/domain/types.ts
@@ -262,9 +262,8 @@ function resolveProjectDirectory(cwd, stopAt = [homedir(), tmpdir()]) {
262
262
  }
263
263
 
264
264
  // src/store/transaction.ts
265
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
266
- import { dirname as dirname2, join as join2 } from "node:path";
267
- import { randomUUID, createHash } from "node:crypto";
265
+ import { closeSync, fstatSync, lstatSync, mkdirSync, openSync, realpathSync as realpathSync2 } from "node:fs";
266
+ import { basename, dirname as dirname2, join as join2 } from "node:path";
268
267
 
269
268
  // src/domain/context.ts
270
269
  function sourceError(raw) {
@@ -554,6 +553,30 @@ function parseMap(raw, path) {
554
553
  return textError ? err({ kind: "bad-shape", path, detail: textError }) : ok(map);
555
554
  }
556
555
 
556
+ // src/store/native-lock.ts
557
+ import { existsSync as existsSync2 } from "node:fs";
558
+ import { createRequire } from "node:module";
559
+ import { fileURLToPath } from "node:url";
560
+ var loaded;
561
+ function nativeLock() {
562
+ if (loaded) return loaded;
563
+ if (Number(process.versions.napi ?? 0) < 9) {
564
+ throw new Error("Project locks require Node-API 9 (Node 18.17+ or 20.3+).");
565
+ }
566
+ const candidates = [
567
+ new URL("./native-lock.cjs", import.meta.url),
568
+ new URL("../../dist/native-lock.cjs", import.meta.url)
569
+ ];
570
+ const entry = candidates.find((candidate) => existsSync2(candidate));
571
+ if (!entry) throw new Error("The installed package is missing dist/native-lock.cjs; reinstall the complete package.");
572
+ const backend = createRequire(import.meta.url)(fileURLToPath(entry));
573
+ if (typeof backend.tryLock !== "function" || typeof backend.unlock !== "function") {
574
+ throw new Error("The installed native lock backend is invalid.");
575
+ }
576
+ loaded = backend;
577
+ return backend;
578
+ }
579
+
557
580
  // src/store/transaction.ts
558
581
  var LedgerError = class extends Error {
559
582
  constructor(code, message, details = {}) {
@@ -564,61 +587,78 @@ var LedgerError = class extends Error {
564
587
  };
565
588
  function storeDirectory(file) {
566
589
  const dir = dirname2(file);
567
- return dir.endsWith("/pages") || dir.endsWith("\\pages") ? dirname2(dir) : dir;
568
- }
569
- function deadOwner(lock) {
590
+ if (basename(dir).toLowerCase() === "pages") return dirname2(dir);
591
+ let canonical = dir;
570
592
  try {
571
- const owner = JSON.parse(readFileSync(join2(lock, "owner.json"), "utf8"));
572
- if (!Number.isSafeInteger(owner.pid) || owner.pid <= 0) return false;
573
- try {
574
- process.kill(owner.pid, 0);
575
- return false;
576
- } catch (e) {
577
- return e.code === "ESRCH";
578
- }
579
- } catch {
580
- return false;
593
+ canonical = realpathSync2.native(dir);
594
+ } catch (error) {
595
+ if (error.code !== "ENOENT") throw error;
596
+ }
597
+ return basename(canonical).toLowerCase() === "pages" ? dirname2(canonical) : canonical;
598
+ }
599
+ function checkLockPath(lock) {
600
+ const entry = lstatSync(lock, { throwIfNoEntry: false });
601
+ if (entry?.isDirectory()) {
602
+ throw new LedgerError(
603
+ "LOCK_MIGRATION_REQUIRED",
604
+ `Legacy directory lock at ${lock}. Stop all old MCP servers, viewers and watchers for this project, then move that directory aside for inspection and retry. Never remove the new regular lock file.`,
605
+ { path: lock }
606
+ );
607
+ }
608
+ if (entry && !entry.isFile()) {
609
+ throw new LedgerError("LOCK_UNAVAILABLE", `Project lock must be a regular file, not a symlink or special file: ${lock}`, { path: lock });
581
610
  }
582
611
  }
583
612
  function withStoreLock(file, action) {
613
+ let backend;
614
+ try {
615
+ backend = nativeLock();
616
+ } catch (error) {
617
+ throw new LedgerError("LOCK_UNAVAILABLE", `Cannot load the operating-system lock backend: ${error instanceof Error ? error.message : String(error)}`);
618
+ }
584
619
  const dir = storeDirectory(file);
585
620
  mkdirSync(dir, { recursive: true });
586
621
  const lock = join2(dir, ".write-lock");
587
- const owner = join2(lock, "owner.json");
588
- const token = randomUUID();
622
+ checkLockPath(lock);
623
+ let fd;
589
624
  try {
590
- mkdirSync(lock);
625
+ fd = openSync(lock, "a+", 384);
591
626
  } catch (error) {
592
- if (error.code !== "EEXIST") throw error;
593
- if (!deadOwner(lock)) throw new LedgerError("BUSY", `Another writer owns ${lock}; retry after it completes. An orphan without owner metadata needs manual inspection.`);
594
- try {
595
- mkdirSync(join2(lock, ".reap"));
596
- } catch {
597
- throw new LedgerError("BUSY", "Another process is recovering the writer lock.");
598
- }
599
- if (!deadOwner(lock)) {
600
- rmSync(join2(lock, ".reap"), { recursive: true, force: true });
601
- throw new LedgerError("BUSY", "Writer ownership changed.");
602
- }
603
- rmSync(lock, { recursive: true });
604
- try {
605
- mkdirSync(lock);
606
- } catch {
607
- throw new LedgerError("BUSY", "Another writer acquired the recovered lock.");
608
- }
627
+ checkLockPath(lock);
628
+ throw new LedgerError("LOCK_UNAVAILABLE", `Cannot open project lock ${lock}: ${error instanceof Error ? error.message : String(error)}`, { path: lock });
609
629
  }
610
- let initialized = false;
630
+ let acquired = false;
611
631
  try {
612
- writeFileSync(owner, JSON.stringify({ pid: process.pid, token }), { flag: "wx" });
613
- initialized = true;
632
+ checkLockPath(lock);
633
+ if (!fstatSync(fd).isFile()) throw new LedgerError("LOCK_UNAVAILABLE", `Project lock is not a regular file: ${lock}`);
634
+ try {
635
+ acquired = backend.tryLock(fd);
636
+ } catch (error) {
637
+ throw new LedgerError("LOCK_UNAVAILABLE", `Cannot acquire project lock ${lock}: ${error instanceof Error ? error.message : String(error)}`, { path: lock });
638
+ }
639
+ if (!acquired) throw new LedgerError("BUSY", `Another writer owns ${lock}; retry after it completes.`);
614
640
  return action();
615
641
  } finally {
616
642
  try {
617
- if (!initialized || JSON.parse(readFileSync(owner, "utf8")).token === token) rmSync(lock, { recursive: true });
643
+ if (acquired) backend.unlock(fd);
618
644
  } catch {
645
+ warnLockCleanup(`Could not explicitly unlock ${lock}; closing its file handle.`);
646
+ } finally {
647
+ try {
648
+ closeSync(fd);
649
+ } catch {
650
+ warnLockCleanup(`Could not close the project lock handle for ${lock}; restart this process before retrying writes.`);
651
+ }
619
652
  }
620
653
  }
621
654
  }
655
+ function warnLockCleanup(message) {
656
+ try {
657
+ process.stderr.write(`mellos-mapping: ${message}
658
+ `);
659
+ } catch {
660
+ }
661
+ }
622
662
 
623
663
  // src/semantics/vocabulary.ts
624
664
  var STATUS_GLYPHS = {
@@ -1748,7 +1788,7 @@ function paint(scene, opts) {
1748
1788
  }
1749
1789
 
1750
1790
  // src/store/atomic.ts
1751
- import { mkdirSync as mkdirSync2, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
1791
+ import { mkdirSync as mkdirSync2, renameSync, rmSync, writeFileSync } from "node:fs";
1752
1792
  import { dirname as dirname3 } from "node:path";
1753
1793
  var RENAME_BACKOFF_STEP_MS = 10;
1754
1794
  var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOENT"]);
@@ -1757,7 +1797,7 @@ function sleepSync(ms) {
1757
1797
  }
1758
1798
  function discardTemp(tmp) {
1759
1799
  try {
1760
- rmSync2(tmp, { force: true });
1800
+ rmSync(tmp, { force: true });
1761
1801
  } catch {
1762
1802
  }
1763
1803
  }
@@ -1768,7 +1808,7 @@ function writeAtomic(path, contents, maxAttempts) {
1768
1808
  const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2, 10)}.tmp`;
1769
1809
  try {
1770
1810
  mkdirSync2(dirname3(path), { recursive: true });
1771
- writeFileSync2(tmp, contents, "utf8");
1811
+ writeFileSync(tmp, contents, "utf8");
1772
1812
  } catch (e) {
1773
1813
  discardTemp(tmp);
1774
1814
  return err({ kind: "save-failed", path, detail: `writing the temp file failed: ${errnoOf(e)}` });
@@ -1791,8 +1831,8 @@ function writeAtomic(path, contents, maxAttempts) {
1791
1831
  }
1792
1832
 
1793
1833
  // src/store/pages.ts
1794
- import { existsSync as existsSync2, readdirSync, rmSync as rmSync3 } from "node:fs";
1795
- import { basename, dirname as dirname4, join as join3 } from "node:path";
1834
+ import { existsSync as existsSync3, readdirSync, rmSync as rmSync2 } from "node:fs";
1835
+ import { basename as basename2, dirname as dirname4, join as join3 } from "node:path";
1796
1836
  var STORE_DIR_NAME = ".mellos";
1797
1837
  var STATE_FILE_RELATIVE_PATH = join3(STORE_DIR_NAME, "map.json");
1798
1838
  var PAGES_DIR_NAME = "pages";
@@ -1801,12 +1841,12 @@ function pageFilePath(defaultFile, page) {
1801
1841
  }
1802
1842
  function pageIdOfFile(defaultFile, path) {
1803
1843
  if (path === defaultFile) return void 0;
1804
- const name = basename(path);
1844
+ const name = basename2(path);
1805
1845
  return name.endsWith(".json") ? name.slice(0, -".json".length) : name;
1806
1846
  }
1807
1847
  function listPageFiles(defaultFile) {
1808
1848
  const out = [];
1809
- if (existsSync2(defaultFile)) out.push(defaultFile);
1849
+ if (existsSync3(defaultFile)) out.push(defaultFile);
1810
1850
  let entries = [];
1811
1851
  try {
1812
1852
  entries = readdirSync(join3(dirname4(defaultFile), PAGES_DIR_NAME));
@@ -1819,7 +1859,7 @@ function listPageFiles(defaultFile) {
1819
1859
  }
1820
1860
  function deletePageFile(path) {
1821
1861
  try {
1822
- rmSync3(path, { force: true });
1862
+ rmSync2(path, { force: true });
1823
1863
  return ok(void 0);
1824
1864
  } catch (e) {
1825
1865
  return err({ kind: "delete-failed", path, detail: errnoOf(e) });
@@ -1827,7 +1867,7 @@ function deletePageFile(path) {
1827
1867
  }
1828
1868
 
1829
1869
  // src/store/channels.ts
1830
- import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync as rmSync5 } from "node:fs";
1870
+ import { existsSync as existsSync4, readFileSync as readFileSync2, rmSync as rmSync4 } from "node:fs";
1831
1871
  import { dirname as dirname6, join as join5 } from "node:path";
1832
1872
 
1833
1873
  // src/store/json-text.ts
@@ -1839,7 +1879,7 @@ function stripBom(text) {
1839
1879
  }
1840
1880
 
1841
1881
  // src/store/viewers.ts
1842
- import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync, rmSync as rmSync4 } from "node:fs";
1882
+ import { readdirSync as readdirSync2, readFileSync, statSync, rmSync as rmSync3 } from "node:fs";
1843
1883
  import { dirname as dirname5, join as join4 } from "node:path";
1844
1884
  var VIEWERS_DIR_NAME = "viewers";
1845
1885
  var VIEWER_FILE_VERSION = 1;
@@ -1857,7 +1897,7 @@ function publishViewer(defaultFile, pid, report) {
1857
1897
  }
1858
1898
  function retireViewer(defaultFile, pid) {
1859
1899
  try {
1860
- rmSync4(viewerFilePath(defaultFile, pid), { force: true });
1900
+ rmSync3(viewerFilePath(defaultFile, pid), { force: true });
1861
1901
  } catch {
1862
1902
  }
1863
1903
  }
@@ -1874,15 +1914,15 @@ function paneChannelPath(defaultFile, channel, pid) {
1874
1914
  }
1875
1915
  function takeFocusRequest(defaultFile, pid) {
1876
1916
  const targeted = pid === void 0 ? void 0 : focusFilePath(defaultFile, pid);
1877
- const path = targeted !== void 0 && existsSync3(targeted) ? targeted : focusFilePath(defaultFile);
1917
+ const path = targeted !== void 0 && existsSync4(targeted) ? targeted : focusFilePath(defaultFile);
1878
1918
  let raw;
1879
1919
  try {
1880
- raw = readFileSync3(path, "utf8");
1920
+ raw = readFileSync2(path, "utf8");
1881
1921
  } catch {
1882
1922
  return void 0;
1883
1923
  }
1884
1924
  try {
1885
- rmSync5(path, { force: true });
1925
+ rmSync4(path, { force: true });
1886
1926
  } catch {
1887
1927
  }
1888
1928
  let parsed;
@@ -1904,10 +1944,10 @@ function quitFilePath(defaultFile, pid) {
1904
1944
  }
1905
1945
  function takeQuitRequest(defaultFile, pid) {
1906
1946
  const targeted = pid === void 0 ? void 0 : quitFilePath(defaultFile, pid);
1907
- const path = targeted !== void 0 && existsSync3(targeted) ? targeted : quitFilePath(defaultFile);
1947
+ const path = targeted !== void 0 && existsSync4(targeted) ? targeted : quitFilePath(defaultFile);
1908
1948
  let raw;
1909
1949
  try {
1910
- raw = readFileSync3(path, "utf8");
1950
+ raw = readFileSync2(path, "utf8");
1911
1951
  } catch {
1912
1952
  return false;
1913
1953
  }
@@ -1922,7 +1962,7 @@ function takeQuitRequest(defaultFile, pid) {
1922
1962
  }
1923
1963
  function sweepQuitRequest(defaultFile, pid) {
1924
1964
  try {
1925
- rmSync5(quitFilePath(defaultFile, pid), { force: true });
1965
+ rmSync4(quitFilePath(defaultFile, pid), { force: true });
1926
1966
  } catch {
1927
1967
  }
1928
1968
  }
@@ -1935,7 +1975,7 @@ function configFilePath(defaultFile) {
1935
1975
  }
1936
1976
 
1937
1977
  // src/store/migration.ts
1938
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, renameSync as renameSync2 } from "node:fs";
1978
+ import { existsSync as existsSync5, renameSync as renameSync2 } from "node:fs";
1939
1979
  import { dirname as dirname8, join as join7 } from "node:path";
1940
1980
  var LEGACY_STATE_FILE_RELATIVE_PATH = join7(".claude", "mellos-mapping.json");
1941
1981
  var LEGACY_PAGES_DIR_NAME = "mellos-mapping.pages";
@@ -1945,22 +1985,24 @@ function migrateLegacyStore(defaultFile) {
1945
1985
  const legacyDefault = join7(projectRoot, LEGACY_STATE_FILE_RELATIVE_PATH);
1946
1986
  const legacyPages = join7(dirname8(legacyDefault), LEGACY_PAGES_DIR_NAME);
1947
1987
  const legacyConfig = join7(dirname8(legacyDefault), LEGACY_CONFIG_FILE_NAME);
1948
- const hasLegacy = existsSync4(legacyDefault) || existsSync4(legacyPages) || existsSync4(legacyConfig);
1949
- const hasCurrent = existsSync4(defaultFile) || existsSync4(join7(dirname8(defaultFile), PAGES_DIR_NAME)) || existsSync4(configFilePath(defaultFile));
1950
- if (!hasLegacy || hasCurrent) return false;
1951
- mkdirSync3(dirname8(defaultFile), { recursive: true });
1952
- if (existsSync4(legacyDefault)) renameSync2(legacyDefault, defaultFile);
1953
- if (existsSync4(legacyPages)) renameSync2(legacyPages, join7(dirname8(defaultFile), PAGES_DIR_NAME));
1954
- if (existsSync4(legacyConfig)) renameSync2(legacyConfig, configFilePath(defaultFile));
1955
- return true;
1988
+ const hasLegacy = () => existsSync5(legacyDefault) || existsSync5(legacyPages) || existsSync5(legacyConfig);
1989
+ const hasCurrent = () => existsSync5(defaultFile) || existsSync5(join7(dirname8(defaultFile), PAGES_DIR_NAME)) || existsSync5(configFilePath(defaultFile));
1990
+ if (!hasLegacy() || hasCurrent()) return false;
1991
+ return withStoreLock(defaultFile, () => {
1992
+ if (!hasLegacy() || hasCurrent()) return false;
1993
+ if (existsSync5(legacyDefault)) renameSync2(legacyDefault, defaultFile);
1994
+ if (existsSync5(legacyPages)) renameSync2(legacyPages, join7(dirname8(defaultFile), PAGES_DIR_NAME));
1995
+ if (existsSync5(legacyConfig)) renameSync2(legacyConfig, configFilePath(defaultFile));
1996
+ return true;
1997
+ });
1956
1998
  }
1957
1999
 
1958
2000
  // src/store/maps.ts
1959
- import { readFileSync as readFileSync4 } from "node:fs";
2001
+ import { readFileSync as readFileSync3 } from "node:fs";
1960
2002
  function loadMapFile(path) {
1961
2003
  let text;
1962
2004
  try {
1963
- text = readFileSync4(path, "utf8");
2005
+ text = readFileSync3(path, "utf8");
1964
2006
  } catch (e) {
1965
2007
  const code = e.code;
1966
2008
  if (code === "ENOENT") return err({ kind: "not-found", path });
@@ -2345,22 +2387,22 @@ function scan(state, input2) {
2345
2387
  pages.push(held);
2346
2388
  continue;
2347
2389
  }
2348
- const loaded = input2.load(file);
2349
- if (loaded.ok) {
2390
+ const loaded2 = input2.load(file);
2391
+ if (loaded2.ok) {
2350
2392
  if (!first) changed.push(file);
2351
2393
  const fresh = !first && file !== previousActive;
2352
2394
  if (fresh) freshened.push(file);
2353
- pages.push({ file, state: { kind: "loaded", map: loaded.value, mtimeMs }, fresh });
2395
+ pages.push({ file, state: { kind: "loaded", map: loaded2.value, mtimeMs }, fresh });
2354
2396
  continue;
2355
2397
  }
2356
2398
  pages.push({
2357
2399
  file,
2358
2400
  state: {
2359
2401
  kind: "faulted",
2360
- fault: loaded.error,
2402
+ fault: loaded2.error,
2361
2403
  lastGood: mapOf(held),
2362
2404
  mtimeMs,
2363
- transient: isTransient(loaded.error)
2405
+ transient: isTransient(loaded2.error)
2364
2406
  },
2365
2407
  fresh: held?.fresh ?? false
2366
2408
  });