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/README.md +48 -10
- package/README.zh-CN.md +38 -9
- package/dist/hook-session-start.mjs +1 -1
- package/dist/native/LICENSES.txt +443 -0
- package/dist/native/darwin-arm64/fs-native-extensions.node +0 -0
- package/dist/native/darwin-x64/fs-native-extensions.node +0 -0
- package/dist/native/linux-arm64/fs-native-extensions.node +0 -0
- package/dist/native/linux-x64/fs-native-extensions.node +0 -0
- package/dist/native/win32-arm64/fs-native-extensions.node +0 -0
- package/dist/native/win32-x64/fs-native-extensions.node +0 -0
- package/dist/native-lock.cjs +346 -0
- package/dist/server.mjs +198 -138
- package/dist/terminal-worker.mjs +117 -74
- package/dist/watch.mjs +223 -100
- package/dist/web/app.css +106 -2
- package/dist/web/app.js +78 -0
- package/dist/web/index.html +1 -1
- package/dist/web/terminal.css +103 -0
- package/dist/web/terminal.html +1 -1
- package/dist/web/terminal.js +78 -0
- package/dist/web.mjs +305 -112
- package/docs/codex.md +4 -1
- package/docs/locking.md +115 -0
- package/docs/map-api.md +29 -13
- package/lib/store/migration.js +19 -12
- package/lib/store/native-lock.d.ts +5 -0
- package/lib/store/native-lock.js +27 -0
- package/lib/store/transaction.d.ts +9 -2
- package/lib/store/transaction.js +79 -50
- package/package.json +6 -3
package/dist/watch.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createRequire } from 'node:module'; const require =
|
|
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
|
|
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 {
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
function deadOwner(lock) {
|
|
590
|
+
if (basename(dir).toLowerCase() === "pages") return dirname2(dir);
|
|
591
|
+
let canonical = dir;
|
|
570
592
|
try {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
588
|
-
|
|
622
|
+
checkLockPath(lock);
|
|
623
|
+
let fd;
|
|
589
624
|
try {
|
|
590
|
-
|
|
625
|
+
fd = openSync(lock, "a+", 384);
|
|
591
626
|
} catch (error) {
|
|
592
|
-
|
|
593
|
-
|
|
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
|
|
630
|
+
let acquired = false;
|
|
611
631
|
try {
|
|
612
|
-
|
|
613
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 &&
|
|
1917
|
+
const path = targeted !== void 0 && existsSync4(targeted) ? targeted : focusFilePath(defaultFile);
|
|
1878
1918
|
let raw;
|
|
1879
1919
|
try {
|
|
1880
|
-
raw =
|
|
1920
|
+
raw = readFileSync2(path, "utf8");
|
|
1881
1921
|
} catch {
|
|
1882
1922
|
return void 0;
|
|
1883
1923
|
}
|
|
1884
1924
|
try {
|
|
1885
|
-
|
|
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 &&
|
|
1947
|
+
const path = targeted !== void 0 && existsSync4(targeted) ? targeted : quitFilePath(defaultFile);
|
|
1908
1948
|
let raw;
|
|
1909
1949
|
try {
|
|
1910
|
-
raw =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
1949
|
-
const hasCurrent =
|
|
1950
|
-
if (!hasLegacy || hasCurrent) return false;
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
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
|
|
2001
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1960
2002
|
function loadMapFile(path) {
|
|
1961
2003
|
let text;
|
|
1962
2004
|
try {
|
|
1963
|
-
text =
|
|
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
|
|
2349
|
-
if (
|
|
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:
|
|
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:
|
|
2402
|
+
fault: loaded2.error,
|
|
2361
2403
|
lastGood: mapOf(held),
|
|
2362
2404
|
mtimeMs,
|
|
2363
|
-
transient: isTransient(
|
|
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,
|
|
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 =
|
|
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 =
|
|
2753
|
-
let lastRows =
|
|
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(
|
|
2781
|
-
const viewHeight = () => Math.max(1, (
|
|
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
|
-
|
|
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)
|
|
2843
|
-
const terminal = openTerminalSession({ interactive, mouse: cfg.mouse, 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) =>
|
|
2887
|
+
write: (text) => io2.output.write(text),
|
|
2846
2888
|
onDrain: (ready) => {
|
|
2847
|
-
|
|
2889
|
+
io2.output.once("drain", ready);
|
|
2848
2890
|
return () => {
|
|
2849
|
-
|
|
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 =
|
|
2922
|
+
const cols = io2.output.columns ?? FALLBACK_COLUMNS;
|
|
2881
2923
|
const viewW = viewWidth();
|
|
2882
|
-
panelContentRows = clampPanelRows(panelContentRows,
|
|
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
|
-
|
|
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 =
|
|
3003
|
-
lastRows =
|
|
3045
|
+
lastCols = io2.output.columns ?? lastCols;
|
|
3046
|
+
lastRows = io2.output.rows ?? lastRows;
|
|
3004
3047
|
lastFrame = "";
|
|
3005
3048
|
frameOutput?.invalidate();
|
|
3006
|
-
|
|
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 ((
|
|
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
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|