mellos-mapping 0.15.0 → 0.16.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 +5 -3
- package/README.zh-CN.md +4 -2
- package/dist/server.mjs +2 -2
- package/dist/watch.mjs +74 -8
- package/package.json +1 -1
- package/scripts/open-pane.mjs +41 -7
package/README.md
CHANGED
|
@@ -128,9 +128,11 @@ the script after updating the plugin.
|
|
|
128
128
|
To watch the live pane beside a Codex session on Windows, run
|
|
129
129
|
`node <plugin root>/scripts/open-pane.mjs <project dir>` — it splits the
|
|
130
130
|
terminal window hosting the session (or falls back to a dedicated
|
|
131
|
-
"mellos-mapping" window; `--window` picks that on purpose).
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
"mellos-mapping" window; `--window` picks that on purpose). Add
|
|
132
|
+
`--page <slug>` to open on a particular page — and with a pane already open,
|
|
133
|
+
rerunning with `--page` retargets it instead of opening another. Elsewhere
|
|
134
|
+
run `node <plugin root>/dist/watch.mjs` (same `--page` flag) from the
|
|
135
|
+
project directory in a second terminal (or any terminal split).
|
|
134
136
|
|
|
135
137
|
## Any MCP client
|
|
136
138
|
|
package/README.zh-CN.md
CHANGED
|
@@ -118,8 +118,10 @@ node ~/.codex/plugins/cache/mellos-mapping/mellos-mapping/<版本>/scripts/codex
|
|
|
118
118
|
要在 Codex 会话旁边看实况面板,Windows 上运行
|
|
119
119
|
`node <插件根>/scripts/open-pane.mjs <项目目录>`——它会在承载本会话的
|
|
120
120
|
终端窗口里分屏(识别不到就确定性地开到专属的 "mellos-mapping" 窗口;
|
|
121
|
-
`--window`
|
|
122
|
-
|
|
121
|
+
`--window` 则是主动选择专属窗口)。加 `--page <slug>` 指定打开哪一页;
|
|
122
|
+
面板已经开着时,带 `--page` 重跑一次不会再开新面板,而是让现有面板
|
|
123
|
+
切到那一页。其他环境在项目目录下的第二个终端(或任意分屏)运行
|
|
124
|
+
`node <插件根>/dist/watch.mjs`(同样支持 `--page`)。
|
|
123
125
|
|
|
124
126
|
## 任意 MCP 客户端
|
|
125
127
|
|
package/dist/server.mjs
CHANGED
|
@@ -22133,7 +22133,7 @@ function drawBox(canvas, box, opts, neutral, focused = false) {
|
|
|
22133
22133
|
}
|
|
22134
22134
|
|
|
22135
22135
|
// src/store/store.ts
|
|
22136
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
22136
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
22137
22137
|
import { basename, dirname, join } from "node:path";
|
|
22138
22138
|
var STATE_FILE_VERSION = 1;
|
|
22139
22139
|
var STATE_FILE_RELATIVE_PATH = join(".claude", "mellos-mapping.json");
|
|
@@ -22515,7 +22515,7 @@ function summarize(map) {
|
|
|
22515
22515
|
|
|
22516
22516
|
// src/server/server.ts
|
|
22517
22517
|
var SERVER_NAME = "mellos-mapping";
|
|
22518
|
-
var SERVER_VERSION = "0.
|
|
22518
|
+
var SERVER_VERSION = "0.16.0";
|
|
22519
22519
|
var ID = external_exports.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "lowercase letters, digits and dashes, 1-64 chars").describe("stable kebab-case identifier");
|
|
22520
22520
|
var PAGE = ID.optional().describe(
|
|
22521
22521
|
"page (parallel map) this call targets; omit for the default page. One effort = one page: start a NEW effort on its own page named after the effort, so concurrent sessions never write over each other and the pane can switch between pages."
|
package/dist/watch.mjs
CHANGED
|
@@ -967,11 +967,14 @@ function drawBox(canvas, box, opts, neutral, focused = false) {
|
|
|
967
967
|
}
|
|
968
968
|
|
|
969
969
|
// src/store/store.ts
|
|
970
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
970
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
971
971
|
import { basename, dirname, join } from "node:path";
|
|
972
972
|
var STATE_FILE_VERSION = 1;
|
|
973
973
|
var STATE_FILE_RELATIVE_PATH = join(".claude", "mellos-mapping.json");
|
|
974
974
|
var PAGES_DIR_NAME = "mellos-mapping.pages";
|
|
975
|
+
function makePageId(raw) {
|
|
976
|
+
return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
|
|
977
|
+
}
|
|
975
978
|
function pageFilePath(defaultFile, page) {
|
|
976
979
|
return page === void 0 ? defaultFile : join(dirname(defaultFile), PAGES_DIR_NAME, `${page}.json`);
|
|
977
980
|
}
|
|
@@ -993,6 +996,35 @@ function listPageFiles(defaultFile) {
|
|
|
993
996
|
}
|
|
994
997
|
return out;
|
|
995
998
|
}
|
|
999
|
+
var FOCUS_FILE_NAME = "mellos-mapping.focus";
|
|
1000
|
+
function focusFilePath(defaultFile) {
|
|
1001
|
+
return join(dirname(defaultFile), FOCUS_FILE_NAME);
|
|
1002
|
+
}
|
|
1003
|
+
function takeFocusRequest(defaultFile) {
|
|
1004
|
+
const path = focusFilePath(defaultFile);
|
|
1005
|
+
let raw;
|
|
1006
|
+
try {
|
|
1007
|
+
raw = readFileSync(path, "utf8");
|
|
1008
|
+
} catch {
|
|
1009
|
+
return void 0;
|
|
1010
|
+
}
|
|
1011
|
+
try {
|
|
1012
|
+
rmSync(path, { force: true });
|
|
1013
|
+
} catch {
|
|
1014
|
+
}
|
|
1015
|
+
let parsed;
|
|
1016
|
+
try {
|
|
1017
|
+
parsed = JSON.parse(raw);
|
|
1018
|
+
} catch {
|
|
1019
|
+
return void 0;
|
|
1020
|
+
}
|
|
1021
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
1022
|
+
const page = parsed.page;
|
|
1023
|
+
if (page === void 0 || page === null) return { page: void 0 };
|
|
1024
|
+
if (typeof page !== "string") return void 0;
|
|
1025
|
+
const id = makePageId(page);
|
|
1026
|
+
return id.ok ? { page: id.value } : void 0;
|
|
1027
|
+
}
|
|
996
1028
|
function describeStoreError(e) {
|
|
997
1029
|
switch (e.kind) {
|
|
998
1030
|
case "not-found":
|
|
@@ -1241,11 +1273,17 @@ function parseArgs(argv, cwd) {
|
|
|
1241
1273
|
let unicode = true;
|
|
1242
1274
|
let color = true;
|
|
1243
1275
|
let mouse = true;
|
|
1276
|
+
let page;
|
|
1244
1277
|
for (let i = 0; i < argv.length; i++) {
|
|
1245
1278
|
switch (argv[i]) {
|
|
1246
1279
|
case "--file":
|
|
1247
1280
|
file = argv[++i] ?? file;
|
|
1248
1281
|
break;
|
|
1282
|
+
case "--page": {
|
|
1283
|
+
const parsed = makePageId(argv[++i] ?? "");
|
|
1284
|
+
if (parsed.ok) page = parsed.value;
|
|
1285
|
+
break;
|
|
1286
|
+
}
|
|
1249
1287
|
case "--interval":
|
|
1250
1288
|
intervalMs = Math.max(50, Number(argv[++i]) || intervalMs);
|
|
1251
1289
|
break;
|
|
@@ -1262,7 +1300,19 @@ function parseArgs(argv, cwd) {
|
|
|
1262
1300
|
break;
|
|
1263
1301
|
}
|
|
1264
1302
|
}
|
|
1265
|
-
return { file, intervalMs, unicode, color, mouse };
|
|
1303
|
+
return { file, intervalMs, unicode, color, mouse, page };
|
|
1304
|
+
}
|
|
1305
|
+
function mostRecentPageFile(files, mtimeOf) {
|
|
1306
|
+
let best;
|
|
1307
|
+
let bestMtime = -Infinity;
|
|
1308
|
+
for (const file of files) {
|
|
1309
|
+
const mtime = mtimeOf(file);
|
|
1310
|
+
if (mtime !== void 0 && mtime > bestMtime) {
|
|
1311
|
+
best = file;
|
|
1312
|
+
bestMtime = mtime;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
return best ?? files[0];
|
|
1266
1316
|
}
|
|
1267
1317
|
var HIDE_CURSOR = "\x1B[?25l";
|
|
1268
1318
|
var SHOW_CURSOR = "\x1B[?25h";
|
|
@@ -1627,6 +1677,7 @@ function main() {
|
|
|
1627
1677
|
const pageViews = /* @__PURE__ */ new Map();
|
|
1628
1678
|
let activeFile;
|
|
1629
1679
|
let firstScan = true;
|
|
1680
|
+
let pendingFocusFile = cfg.page === void 0 ? void 0 : pageFilePath(cfg.file, cfg.page);
|
|
1630
1681
|
let lastTabSegments = [];
|
|
1631
1682
|
let tabScroll = 0;
|
|
1632
1683
|
let offsetX = 0;
|
|
@@ -1656,7 +1707,7 @@ function main() {
|
|
|
1656
1707
|
parent = diveOrigin(cfg.file, activeFile, pageFiles, maps())?.parent;
|
|
1657
1708
|
}
|
|
1658
1709
|
if (parent !== void 0 && parent !== activeFile) {
|
|
1659
|
-
|
|
1710
|
+
handSwitch(parent);
|
|
1660
1711
|
return true;
|
|
1661
1712
|
}
|
|
1662
1713
|
return false;
|
|
@@ -1691,6 +1742,10 @@ function main() {
|
|
|
1691
1742
|
const tabIndex = top.indexOf(file);
|
|
1692
1743
|
if (tabIndex >= 0) tabScroll = tabScrollFor(pageTabsOf(top), viewWidth(), cfg.unicode, tabScroll, tabIndex);
|
|
1693
1744
|
};
|
|
1745
|
+
const handSwitch = (file) => {
|
|
1746
|
+
pendingFocusFile = void 0;
|
|
1747
|
+
switchPage(file);
|
|
1748
|
+
};
|
|
1694
1749
|
const hitTest = (termX, termY) => {
|
|
1695
1750
|
const sx = termX - 1;
|
|
1696
1751
|
const sy = termY - 1 - tabRows();
|
|
@@ -1846,8 +1901,18 @@ function main() {
|
|
|
1846
1901
|
if (file === activeFile) notice = describeStoreError(loaded.error);
|
|
1847
1902
|
}
|
|
1848
1903
|
}
|
|
1904
|
+
const request = takeFocusRequest(cfg.file);
|
|
1905
|
+
if (request !== void 0 && !(firstScan && pendingFocusFile !== void 0)) {
|
|
1906
|
+
pendingFocusFile = pageFilePath(cfg.file, request.page);
|
|
1907
|
+
}
|
|
1908
|
+
if (pendingFocusFile !== void 0 && pageFiles.includes(pendingFocusFile)) {
|
|
1909
|
+
if (pendingFocusFile !== activeFile) switchPage(pendingFocusFile);
|
|
1910
|
+
pendingFocusFile = void 0;
|
|
1911
|
+
}
|
|
1849
1912
|
firstScan = false;
|
|
1850
|
-
if (activeFile === void 0 || !pageFiles.includes(activeFile))
|
|
1913
|
+
if (activeFile === void 0 || !pageFiles.includes(activeFile)) {
|
|
1914
|
+
switchPage(mostRecentPageFile(pageFiles, (f) => pageData.get(f)?.mtimeMs));
|
|
1915
|
+
}
|
|
1851
1916
|
if ([...pageData.values()].some((p) => p.map?.nodes.some((n) => n.status === "in-progress"))) spinnerFrame++;
|
|
1852
1917
|
if (flash !== void 0 && Date.now() > flash.until) flash = void 0;
|
|
1853
1918
|
paint();
|
|
@@ -1959,7 +2024,7 @@ function main() {
|
|
|
1959
2024
|
tabScroll = Math.max(0, Math.min(tabScroll + tabHit.action.delta, lastTabFiles.length - 1));
|
|
1960
2025
|
} else {
|
|
1961
2026
|
const target = lastTabFiles[tabHit.action.index];
|
|
1962
|
-
if (target !== void 0 && target !== activeFile)
|
|
2027
|
+
if (target !== void 0 && target !== activeFile) handSwitch(target);
|
|
1963
2028
|
}
|
|
1964
2029
|
} else {
|
|
1965
2030
|
const id = hitTest(event.x, event.y);
|
|
@@ -1970,7 +2035,7 @@ function main() {
|
|
|
1970
2035
|
const target = pageFilePath(cfg.file, submap);
|
|
1971
2036
|
if (pageFiles.includes(target) && target !== activeFile) {
|
|
1972
2037
|
diveStack.push(activeFile);
|
|
1973
|
-
|
|
2038
|
+
handSwitch(target);
|
|
1974
2039
|
} else if (!pageFiles.includes(target)) {
|
|
1975
2040
|
flash = { text: `submap "${submap}" has no page yet`, until: now + 2500 };
|
|
1976
2041
|
}
|
|
@@ -1994,7 +2059,7 @@ function main() {
|
|
|
1994
2059
|
const step = event.kind === "next-page" ? 1 : -1;
|
|
1995
2060
|
const target = top[(current + step + top.length) % top.length];
|
|
1996
2061
|
if (target !== activeFile) {
|
|
1997
|
-
|
|
2062
|
+
handSwitch(target);
|
|
1998
2063
|
dirty = true;
|
|
1999
2064
|
}
|
|
2000
2065
|
}
|
|
@@ -2003,7 +2068,7 @@ function main() {
|
|
|
2003
2068
|
case "page": {
|
|
2004
2069
|
const target = topFiles()[event.index];
|
|
2005
2070
|
if (target !== void 0 && target !== activeFile) {
|
|
2006
|
-
|
|
2071
|
+
handSwitch(target);
|
|
2007
2072
|
dirty = true;
|
|
2008
2073
|
}
|
|
2009
2074
|
break;
|
|
@@ -2049,6 +2114,7 @@ export {
|
|
|
2049
2114
|
launchedAsEntry,
|
|
2050
2115
|
liveRipples,
|
|
2051
2116
|
mapPanel,
|
|
2117
|
+
mostRecentPageFile,
|
|
2052
2118
|
nearestHit,
|
|
2053
2119
|
nodePanel,
|
|
2054
2120
|
pageTabRow,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mellos-mapping",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"mcpName": "io.github.GuangminJu/mellos-mapping",
|
|
5
5
|
"description": "A live layered dependency map for bottom-up development — MCP server + terminal pane. Ghost the design first, then light nodes up from the bottom as they are built and verified.",
|
|
6
6
|
"type": "module",
|
package/scripts/open-pane.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Open the live map pane in the RIGHT Windows Terminal window.
|
|
4
4
|
*
|
|
5
|
-
* node scripts/open-pane.mjs <project-dir> [--window] [--ascii] [--force]
|
|
5
|
+
* node scripts/open-pane.mjs <project-dir> [--page <slug>] [--window] [--ascii] [--force]
|
|
6
6
|
*
|
|
7
7
|
* Why this exists: the agent's shell runs on a hidden console (no WT_SESSION),
|
|
8
8
|
* so a bare `wt -w 0 sp` targets the MOST RECENTLY USED terminal window — with
|
|
@@ -33,18 +33,38 @@
|
|
|
33
33
|
* Known race, accepted: if the user focuses a DIFFERENT terminal window in
|
|
34
34
|
* the ~1s between our focus-verify and wt reading its MRU state, the split
|
|
35
35
|
* can still land there. The window is at least one the user is actively in.
|
|
36
|
+
*
|
|
37
|
+
* --page <slug> opens the map ON that page (the effort under discussion, not
|
|
38
|
+
* whatever page the store lists first). With a watcher already running it
|
|
39
|
+
* writes the one-shot focus file instead — the existing pane retargets within
|
|
40
|
+
* a poll tick — so re-running with --page is also how you steer an open pane.
|
|
36
41
|
*/
|
|
37
42
|
import { spawnSync } from 'node:child_process';
|
|
38
|
-
import { existsSync } from 'node:fs';
|
|
43
|
+
import { existsSync, mkdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
39
44
|
import { dirname, join, resolve } from 'node:path';
|
|
40
45
|
import { fileURLToPath } from 'node:url';
|
|
41
46
|
|
|
47
|
+
const USAGE = 'usage: node scripts/open-pane.mjs <project-dir> [--page <slug>] [--window] [--ascii] [--force]';
|
|
48
|
+
|
|
42
49
|
const argv = process.argv.slice(2);
|
|
43
|
-
const flags = new Set(
|
|
44
|
-
const positional =
|
|
50
|
+
const flags = new Set();
|
|
51
|
+
const positional = [];
|
|
52
|
+
let pageSlug;
|
|
53
|
+
for (let i = 0; i < argv.length; i++) {
|
|
54
|
+
const a = argv[i];
|
|
55
|
+
if (a === '--page') pageSlug = argv[++i];
|
|
56
|
+
else if (a.startsWith('--')) flags.add(a);
|
|
57
|
+
else positional.push(a);
|
|
58
|
+
}
|
|
45
59
|
|
|
46
60
|
if (positional.length !== 1) {
|
|
47
|
-
console.error(
|
|
61
|
+
console.error(USAGE);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
// Mirrors ID_RULE in src/domain/types.ts — this script runs standalone and
|
|
65
|
+
// cannot import the TypeScript sources.
|
|
66
|
+
if (pageSlug !== undefined && !/^[a-z0-9][a-z0-9-]{0,63}$/.test(pageSlug)) {
|
|
67
|
+
console.error(`--page needs a kebab-case slug (got "${pageSlug}")\n${USAGE}`);
|
|
48
68
|
process.exit(1);
|
|
49
69
|
}
|
|
50
70
|
if (process.platform !== 'win32') {
|
|
@@ -203,6 +223,7 @@ Write-Output "FOCUS=$(if ($focused) { 1 } else { 0 })"
|
|
|
203
223
|
function paneCommand() {
|
|
204
224
|
const cmd = ['--title', 'mellos map', '-d', projectDir, 'node', watchPath, '--file', mapFile];
|
|
205
225
|
if (flags.has('--ascii')) cmd.push('--ascii');
|
|
226
|
+
if (pageSlug !== undefined) cmd.push('--page', pageSlug);
|
|
206
227
|
return cmd;
|
|
207
228
|
}
|
|
208
229
|
|
|
@@ -221,8 +242,21 @@ function openDedicatedWindow(reason) {
|
|
|
221
242
|
}
|
|
222
243
|
|
|
223
244
|
if (!flags.has('--force') && watcherAlreadyRunning()) {
|
|
224
|
-
|
|
225
|
-
|
|
245
|
+
if (pageSlug !== undefined) {
|
|
246
|
+
// One-shot focus request (see takeFocusRequest in src/store/store.ts):
|
|
247
|
+
// the running watcher consumes and deletes it within a poll tick. Temp +
|
|
248
|
+
// rename because the watcher polls: a torn read would be swept as junk,
|
|
249
|
+
// silently losing the request.
|
|
250
|
+
mkdirSync(dirname(mapFile), { recursive: true });
|
|
251
|
+
const focusFile = join(dirname(mapFile), 'mellos-mapping.focus');
|
|
252
|
+
writeFileSync(`${focusFile}.tmp`, JSON.stringify({ page: pageSlug }));
|
|
253
|
+
renameSync(`${focusFile}.tmp`, focusFile);
|
|
254
|
+
console.log(`MMAP_PANE already-open refocused=${pageSlug}`);
|
|
255
|
+
console.log(`A watcher for ${mapFile} is already running — asked it to show page "${pageSlug}".`);
|
|
256
|
+
} else {
|
|
257
|
+
console.log('MMAP_PANE already-open');
|
|
258
|
+
console.log(`A watcher for ${mapFile} is already running — not opening another pane (use --force to override).`);
|
|
259
|
+
}
|
|
226
260
|
process.exit(0);
|
|
227
261
|
}
|
|
228
262
|
|