mellos-mapping 0.23.0 → 0.25.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/dist/watch.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
2
+ import { createRequire as __mellosCreateRequire } from 'node:module'; const require = __mellosCreateRequire(import.meta.url);
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 parsed2;
@@ -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, input) {
2345
2387
  pages.push(held);
2346
2388
  continue;
2347
2389
  }
2348
- const loaded = input.load(file);
2349
- if (loaded.ok) {
2390
+ const loaded2 = input.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
  });
@@ -2735,12 +2777,12 @@ function viewerReportOf(pane, defaultFile) {
2735
2777
  const shown = pane.activeFile ?? pane.pendingFocusFile ?? defaultFile;
2736
2778
  return { page: pageIdOfFile(defaultFile, shown), follow: pane.follow };
2737
2779
  }
2738
- function runWatcher(cfg, io = nativeWatcherIO()) {
2780
+ function runWatcher(cfg, io2 = nativeWatcherIO()) {
2739
2781
  const renderScene = createWindowRenderer();
2740
2782
  if (migrateLegacyStore(cfg.file)) console.error("mellos-mapping: moved the legacy .claude map store to .mellos/ \u2014 commit the move.");
2741
2783
  sweepQuitRequest(cfg.file);
2742
2784
  sweepQuitRequest(cfg.file, process.pid);
2743
- const interactive = io.interactive;
2785
+ const interactive = io2.interactive;
2744
2786
  const mouseActive = interactive && cfg.mouse;
2745
2787
  let lastFrame = "";
2746
2788
  let spinnerFrame = 0;
@@ -2749,8 +2791,8 @@ function runWatcher(cfg, io = nativeWatcherIO()) {
2749
2791
  const standbyNotice = "waiting for the first mmap_declare ...";
2750
2792
  let map;
2751
2793
  let notice = standbyNotice;
2752
- let lastCols = io.output.columns ?? 0;
2753
- let lastRows = io.output.rows ?? 0;
2794
+ let lastCols = io2.output.columns ?? 0;
2795
+ let lastRows = io2.output.rows ?? 0;
2754
2796
  let pane = initialPaneState(
2755
2797
  cfg.follow,
2756
2798
  cfg.page === void 0 ? void 0 : pageFilePath(cfg.file, cfg.page)
@@ -2777,8 +2819,8 @@ function runWatcher(cfg, io = nativeWatcherIO()) {
2777
2819
  }
2778
2820
  return false;
2779
2821
  };
2780
- const viewWidth = () => usableColumns(io.output.columns ?? FALLBACK_COLUMNS);
2781
- const viewHeight = () => Math.max(1, (io.output.rows ?? FALLBACK_ROWS) - (1 + panelContentRows) - 1 - tabRows());
2822
+ const viewWidth = () => usableColumns(io2.output.columns ?? FALLBACK_COLUMNS);
2823
+ const viewHeight = () => Math.max(1, (io2.output.rows ?? FALLBACK_ROWS) - (1 + panelContentRows) - 1 - tabRows());
2782
2824
  const dividerY = () => tabRows() + viewHeight() + 1;
2783
2825
  const pageTabsOf = (files) => files.map((f) => {
2784
2826
  const entry = entryOf(pane, f);
@@ -2833,20 +2875,20 @@ function runWatcher(cfg, io = nativeWatcherIO()) {
2833
2875
  return lastHits.find((h) => cx >= h.x && cx < h.x + h.w && cy >= h.y && cy < h.y + h.h)?.id;
2834
2876
  };
2835
2877
  const publishPresence = () => {
2836
- io.report?.(viewerReportOf(pane, cfg.file));
2878
+ io2.report?.(viewerReportOf(pane, cfg.file));
2837
2879
  publishViewer(cfg.file, process.pid, {
2838
2880
  ...viewerReportOf(pane, cfg.file),
2839
2881
  ...cfg.owner === void 0 ? {} : { owner: cfg.owner }
2840
2882
  });
2841
2883
  };
2842
- if (interactive) io.input.setRawMode(true);
2843
- const terminal = openTerminalSession({ interactive, mouse: cfg.mouse, write: (text) => io.output.write(text) });
2884
+ if (interactive) io2.input.setRawMode(true);
2885
+ const terminal = openTerminalSession({ interactive, mouse: cfg.mouse, write: (text) => io2.output.write(text) });
2844
2886
  const frameOutput = interactive ? createFrameOutput({
2845
- write: (text) => io.output.write(text),
2887
+ write: (text) => io2.output.write(text),
2846
2888
  onDrain: (ready) => {
2847
- io.output.once("drain", ready);
2889
+ io2.output.once("drain", ready);
2848
2890
  return () => {
2849
- io.output.off("drain", ready);
2891
+ io2.output.off("drain", ready);
2850
2892
  };
2851
2893
  }
2852
2894
  }) : void 0;
@@ -2877,9 +2919,9 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2877
2919
  const paint2 = () => {
2878
2920
  clearTimeout(paintTimer);
2879
2921
  paintTimer = void 0;
2880
- const cols = io.output.columns ?? FALLBACK_COLUMNS;
2922
+ const cols = io2.output.columns ?? FALLBACK_COLUMNS;
2881
2923
  const viewW = viewWidth();
2882
- panelContentRows = clampPanelRows(panelContentRows, io.output.rows ?? FALLBACK_ROWS, tabRows());
2924
+ panelContentRows = clampPanelRows(panelContentRows, io2.output.rows ?? FALLBACK_ROWS, tabRows());
2883
2925
  const viewH = viewHeight();
2884
2926
  const focus = view.hoverId ?? view.selectedId;
2885
2927
  let body;
@@ -2897,6 +2939,7 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2897
2939
  lastHits = [];
2898
2940
  } else {
2899
2941
  const windowed = rendered.value;
2942
+ if (map.nodes.length > 0) io2.onMapRendered?.();
2900
2943
  const maxX = Math.max(0, windowed.contentWidth - viewW);
2901
2944
  const maxY = Math.max(0, windowed.contentHeight - viewH);
2902
2945
  if (view.offsetX > maxX || view.offsetY > maxY || view.offsetX < 0 || view.offsetY < 0) {
@@ -2990,7 +3033,7 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2990
3033
  } else {
2991
3034
  const frame = HOME + rows.map((row) => row + ERASE_LINE_END).join("\n");
2992
3035
  if (frame !== lastFrame) {
2993
- io.output.write(frame);
3036
+ io2.output.write(frame);
2994
3037
  lastFrame = frame;
2995
3038
  }
2996
3039
  }
@@ -2999,16 +3042,16 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2999
3042
  paintTimer ??= setTimeout(paint2, 16);
3000
3043
  };
3001
3044
  const handleResize = () => {
3002
- lastCols = io.output.columns ?? lastCols;
3003
- lastRows = io.output.rows ?? lastRows;
3045
+ lastCols = io2.output.columns ?? lastCols;
3046
+ lastRows = io2.output.rows ?? lastRows;
3004
3047
  lastFrame = "";
3005
3048
  frameOutput?.invalidate();
3006
- io.output.write(CLEAR_ALL);
3049
+ io2.output.write(CLEAR_ALL);
3007
3050
  paint2();
3008
3051
  };
3009
3052
  const tick = () => {
3010
3053
  if (takeQuitRequest(cfg.file, process.pid)) quit();
3011
- if ((io.output.columns ?? lastCols) !== lastCols || (io.output.rows ?? lastRows) !== lastRows) {
3054
+ if ((io2.output.columns ?? lastCols) !== lastCols || (io2.output.rows ?? lastRows) !== lastRows) {
3012
3055
  handleResize();
3013
3056
  }
3014
3057
  const discovered = listPageFiles(cfg.file);
@@ -3077,9 +3120,9 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
3077
3120
  tick();
3078
3121
  };
3079
3122
  if (interactive) {
3080
- io.input.resume();
3081
- io.input.setEncoding("utf8");
3082
- io.input.on("data", (chunk) => {
3123
+ io2.input.resume();
3124
+ io2.input.setEncoding("utf8");
3125
+ io2.input.on("data", (chunk) => {
3083
3126
  const parsed2 = parseInput(pendingInput + chunk);
3084
3127
  pendingInput = parsed2.rest;
3085
3128
  let dirty = false;
@@ -3152,7 +3195,7 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
3152
3195
  break;
3153
3196
  case "mouse-drag":
3154
3197
  if (view.dividerDrag) {
3155
- const next = panelRowsFromDividerY(event.y, io.output.rows ?? FALLBACK_ROWS, tabRows());
3198
+ const next = panelRowsFromDividerY(event.y, io2.output.rows ?? FALLBACK_ROWS, tabRows());
3156
3199
  if (next !== panelContentRows) {
3157
3200
  panelContentRows = next;
3158
3201
  dirty = true;
@@ -3248,7 +3291,7 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
3248
3291
  else paint2();
3249
3292
  }
3250
3293
  });
3251
- io.output.on("resize", handleResize);
3294
+ io2.output.on("resize", handleResize);
3252
3295
  }
3253
3296
  tick();
3254
3297
  setInterval(tick, cfg.intervalMs);
@@ -3263,6 +3306,73 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
3263
3306
  }
3264
3307
  }
3265
3308
 
3309
+ // src/watch/cli.ts
3310
+ import { homedir as homedir2 } from "node:os";
3311
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
3312
+
3313
+ // src/support/star-reminder.ts
3314
+ import { existsSync as existsSync6, readFileSync as readFileSync4, realpathSync as realpathSync4 } from "node:fs";
3315
+ import { basename as basename3, dirname as dirname10, join as join9 } from "node:path";
3316
+ var STAR_URL = "https://github.com/GuangminJu/mellos-mapping";
3317
+ var DAY_MS = 864e5;
3318
+ function isNpmInstallation(entry) {
3319
+ try {
3320
+ const root = dirname10(dirname10(realpathSync4(entry)));
3321
+ if (basename3(dirname10(root)).toLowerCase() !== "node_modules" || existsSync6(join9(root, ".git")) || existsSync6(join9(root, ".codex-plugin")) || existsSync6(join9(root, ".claude-plugin"))) return false;
3322
+ const pkg = JSON.parse(readFileSync4(join9(root, "package.json"), "utf8"));
3323
+ return pkg.name === "mellos-mapping" && pkg.bin?.mmap === "dist/mmap.mjs";
3324
+ } catch {
3325
+ return false;
3326
+ }
3327
+ }
3328
+ function starReminderFile(userBase) {
3329
+ return join9(userBase, ".mellos", "support", "star-reminder.json");
3330
+ }
3331
+ function enabledFlag(value) {
3332
+ return !!value && value !== "0" && value.toLowerCase() !== "false";
3333
+ }
3334
+ function validState(value) {
3335
+ if (!value || typeof value !== "object") return false;
3336
+ const s = value;
3337
+ return s.version === 1 && Number.isFinite(s.firstSeen) && s.firstSeen >= 0 && Number.isSafeInteger(s.lastDay) && s.lastDay >= Math.floor(s.firstSeen / DAY_MS) && Number.isSafeInteger(s.days) && s.days >= 1 && s.days <= 3 && typeof s.notified === "boolean";
3338
+ }
3339
+ function createStarReminder(options) {
3340
+ const eligible = isNpmInstallation(options.entry) && !enabledFlag(options.env["CI"]) && !enabledFlag(options.env["MELLOS_MAPPING_NO_STAR"]);
3341
+ const file = starReminderFile(options.userBase);
3342
+ return {
3343
+ visit() {
3344
+ if (!eligible) return false;
3345
+ try {
3346
+ return withStoreLock(file, () => {
3347
+ const now = (options.now ?? Date.now)();
3348
+ if (!Number.isFinite(now) || now < 0) return false;
3349
+ const day = Math.floor(now / DAY_MS);
3350
+ let state;
3351
+ try {
3352
+ const saved2 = JSON.parse(readFileSync4(file, "utf8"));
3353
+ if (!validState(saved2)) return false;
3354
+ state = saved2;
3355
+ } catch (error) {
3356
+ if (error.code !== "ENOENT") return false;
3357
+ state = { version: 1, firstSeen: now, lastDay: day, days: 1, notified: false };
3358
+ }
3359
+ if (state.notified || now < state.firstSeen || day < state.lastDay) return false;
3360
+ if (day > state.lastDay) {
3361
+ state.days = Math.min(3, state.days + 1);
3362
+ state.lastDay = day;
3363
+ }
3364
+ const show = state.days >= 3 && now - state.firstSeen >= 7 * DAY_MS;
3365
+ if (show) state.notified = true;
3366
+ const saved = writeAtomic(file, JSON.stringify(state, null, 2) + "\n", 1);
3367
+ return saved.ok && show;
3368
+ });
3369
+ } catch {
3370
+ return false;
3371
+ }
3372
+ }
3373
+ };
3374
+ }
3375
+
3266
3376
  // src/watch/cli.ts
3267
3377
  var parsed = parseArgs(process.argv.slice(2), process.cwd());
3268
3378
  if (!parsed.ok) {
@@ -3270,4 +3380,17 @@ if (!parsed.ok) {
3270
3380
  ${USAGE}`);
3271
3381
  process.exit(1);
3272
3382
  }
3273
- runWatcher(parsed.value);
3383
+ var viewed = false;
3384
+ var io = nativeWatcherIO();
3385
+ var reminder = createStarReminder({ entry: fileURLToPath2(import.meta.url), userBase: homedir2(), env: process.env });
3386
+ runWatcher(parsed.value, { ...io, onMapRendered: () => {
3387
+ viewed = true;
3388
+ } });
3389
+ process.on("exit", (code) => {
3390
+ if (code === 0 && viewed && io.interactive && process.stderr.isTTY && process.env["TERM"] !== "dumb" && reminder.visit()) {
3391
+ process.stderr.write(`
3392
+ If Mellos Mapping has been useful, a GitHub Star is welcome: ${STAR_URL}
3393
+ This optional reminder will not appear again.
3394
+ `);
3395
+ }
3396
+ });