mellos-mapping 0.23.0 → 0.24.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 +20 -0
- package/README.zh-CN.md +16 -0
- package/dist/server.mjs +1 -1
- package/dist/terminal-worker.mjs +1 -0
- package/dist/watch.mjs +106 -25
- 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 +150 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -661,6 +661,26 @@ by a test, so a graphical client (a web panel, an editor view) can parse state
|
|
|
661
661
|
files and reuse the exact aggregation, zoom and glyph semantics the terminal
|
|
662
662
|
pane draws with.
|
|
663
663
|
|
|
664
|
+
## Optional GitHub Star reminder
|
|
665
|
+
|
|
666
|
+
npm/npx installations may show one small, dismissible reminder after at least
|
|
667
|
+
7 days and successful map viewing on 3 different UTC dates. Web viewers show
|
|
668
|
+
it inline without taking focus; a native interactive terminal shows it only
|
|
669
|
+
after a successful viewing session exits. It is never added to MCP replies,
|
|
670
|
+
AI prompts, install logs, CI or piped output. Plugin editions and source
|
|
671
|
+
checkouts are excluded.
|
|
672
|
+
|
|
673
|
+
The reminder is claimed once per user, across projects, versions and installs.
|
|
674
|
+
The Star button opens GitHub in the system's default browser, keeping its
|
|
675
|
+
existing sign-in session. A failed launch leaves the notice open with a copy-address option.
|
|
676
|
+
Closing it, following its Star link, or simply leaving the session means it
|
|
677
|
+
will not appear again. To suppress it before it appears, set
|
|
678
|
+
`MELLOS_MAPPING_NO_STAR=1` in the environment of the viewer/MCP process.
|
|
679
|
+
Only the first-use time, last UTC day, a capped day count and the one-time flag
|
|
680
|
+
are stored in `~/.mellos/support/star-reminder.json`; nothing is uploaded and
|
|
681
|
+
GitHub is contacted only when the user follows the link. Unreadable settings
|
|
682
|
+
or a busy store silently skip the reminder.
|
|
683
|
+
|
|
664
684
|
## License
|
|
665
685
|
|
|
666
686
|
MIT
|
package/README.zh-CN.md
CHANGED
|
@@ -596,6 +596,22 @@ npm run verify
|
|
|
596
596
|
守护——图形客户端(web 面板、编辑器视图)可以直接解析状态文件,并复用与
|
|
597
597
|
终端面板完全一致的聚合、缩放与字形语义。
|
|
598
598
|
|
|
599
|
+
## 可选的 GitHub Star 提醒
|
|
600
|
+
|
|
601
|
+
仅 npm/npx 安装会在首次成功查看地图满 7 天、且已在至少 3 个不同 UTC 日期
|
|
602
|
+
成功查看地图后,显示一次轻提示。网页中是一行可关闭的文字,不抢焦点;
|
|
603
|
+
原生交互终端只在正常退出地图后提示。MCP 返回、AI 提示词、安装日志、CI
|
|
604
|
+
及管道输出中都不会加入提醒,插件版和源码运行也不触发。
|
|
605
|
+
|
|
606
|
+
Star 按钮会调用系统默认浏览器打开 GitHub,方便沿用已有登录状态。
|
|
607
|
+
若未能打开,会保留提示并提供复制地址按钮。
|
|
608
|
+
提醒按用户跨项目、版本及安装去重,一生效就记为已提醒。关闭、点击 Star
|
|
609
|
+
或直接离开后,都不会再次出现。想提前关闭,可在启动地图或 MCP 的环境中
|
|
610
|
+
设置 `MELLOS_MAPPING_NO_STAR=1`。
|
|
611
|
+
仅在 `~/.mellos/support/star-reminder.json` 本地记录首次使用时间、最后一个
|
|
612
|
+
UTC 日期、最多 3 天的计数和已提醒标记,不上传数据,不查询是否点过 Star;
|
|
613
|
+
只有用户主动点击链接时才访问 GitHub。设置损坏、不可写或锁忙时静默跳过。
|
|
614
|
+
|
|
599
615
|
## 许可证
|
|
600
616
|
|
|
601
617
|
MIT
|
package/dist/server.mjs
CHANGED
|
@@ -24590,7 +24590,7 @@ function launcherViewerPid(run) {
|
|
|
24590
24590
|
|
|
24591
24591
|
// src/server/server.ts
|
|
24592
24592
|
var SERVER_NAME = "mellos-mapping";
|
|
24593
|
-
var SERVER_VERSION = "0.
|
|
24593
|
+
var SERVER_VERSION = "0.24.0";
|
|
24594
24594
|
function text(s, isError = false) {
|
|
24595
24595
|
return { content: [{ type: "text", text: s }], ...isError ? { isError: true } : {} };
|
|
24596
24596
|
}
|
package/dist/terminal-worker.mjs
CHANGED
|
@@ -2896,6 +2896,7 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2896
2896
|
lastHits = [];
|
|
2897
2897
|
} else {
|
|
2898
2898
|
const windowed = rendered.value;
|
|
2899
|
+
if (map.nodes.length > 0) io.onMapRendered?.();
|
|
2899
2900
|
const maxX = Math.max(0, windowed.contentWidth - viewW);
|
|
2900
2901
|
const maxY = Math.max(0, windowed.contentHeight - viewH);
|
|
2901
2902
|
if (view.offsetX > maxX || view.offsetY > maxY || view.offsetX < 0 || view.offsetY < 0) {
|
package/dist/watch.mjs
CHANGED
|
@@ -2735,12 +2735,12 @@ function viewerReportOf(pane, defaultFile) {
|
|
|
2735
2735
|
const shown = pane.activeFile ?? pane.pendingFocusFile ?? defaultFile;
|
|
2736
2736
|
return { page: pageIdOfFile(defaultFile, shown), follow: pane.follow };
|
|
2737
2737
|
}
|
|
2738
|
-
function runWatcher(cfg,
|
|
2738
|
+
function runWatcher(cfg, io2 = nativeWatcherIO()) {
|
|
2739
2739
|
const renderScene = createWindowRenderer();
|
|
2740
2740
|
if (migrateLegacyStore(cfg.file)) console.error("mellos-mapping: moved the legacy .claude map store to .mellos/ \u2014 commit the move.");
|
|
2741
2741
|
sweepQuitRequest(cfg.file);
|
|
2742
2742
|
sweepQuitRequest(cfg.file, process.pid);
|
|
2743
|
-
const interactive =
|
|
2743
|
+
const interactive = io2.interactive;
|
|
2744
2744
|
const mouseActive = interactive && cfg.mouse;
|
|
2745
2745
|
let lastFrame = "";
|
|
2746
2746
|
let spinnerFrame = 0;
|
|
@@ -2749,8 +2749,8 @@ function runWatcher(cfg, io = nativeWatcherIO()) {
|
|
|
2749
2749
|
const standbyNotice = "waiting for the first mmap_declare ...";
|
|
2750
2750
|
let map;
|
|
2751
2751
|
let notice = standbyNotice;
|
|
2752
|
-
let lastCols =
|
|
2753
|
-
let lastRows =
|
|
2752
|
+
let lastCols = io2.output.columns ?? 0;
|
|
2753
|
+
let lastRows = io2.output.rows ?? 0;
|
|
2754
2754
|
let pane = initialPaneState(
|
|
2755
2755
|
cfg.follow,
|
|
2756
2756
|
cfg.page === void 0 ? void 0 : pageFilePath(cfg.file, cfg.page)
|
|
@@ -2777,8 +2777,8 @@ function runWatcher(cfg, io = nativeWatcherIO()) {
|
|
|
2777
2777
|
}
|
|
2778
2778
|
return false;
|
|
2779
2779
|
};
|
|
2780
|
-
const viewWidth = () => usableColumns(
|
|
2781
|
-
const viewHeight = () => Math.max(1, (
|
|
2780
|
+
const viewWidth = () => usableColumns(io2.output.columns ?? FALLBACK_COLUMNS);
|
|
2781
|
+
const viewHeight = () => Math.max(1, (io2.output.rows ?? FALLBACK_ROWS) - (1 + panelContentRows) - 1 - tabRows());
|
|
2782
2782
|
const dividerY = () => tabRows() + viewHeight() + 1;
|
|
2783
2783
|
const pageTabsOf = (files) => files.map((f) => {
|
|
2784
2784
|
const entry = entryOf(pane, f);
|
|
@@ -2833,20 +2833,20 @@ function runWatcher(cfg, io = nativeWatcherIO()) {
|
|
|
2833
2833
|
return lastHits.find((h) => cx >= h.x && cx < h.x + h.w && cy >= h.y && cy < h.y + h.h)?.id;
|
|
2834
2834
|
};
|
|
2835
2835
|
const publishPresence = () => {
|
|
2836
|
-
|
|
2836
|
+
io2.report?.(viewerReportOf(pane, cfg.file));
|
|
2837
2837
|
publishViewer(cfg.file, process.pid, {
|
|
2838
2838
|
...viewerReportOf(pane, cfg.file),
|
|
2839
2839
|
...cfg.owner === void 0 ? {} : { owner: cfg.owner }
|
|
2840
2840
|
});
|
|
2841
2841
|
};
|
|
2842
|
-
if (interactive)
|
|
2843
|
-
const terminal = openTerminalSession({ interactive, mouse: cfg.mouse, write: (text) =>
|
|
2842
|
+
if (interactive) io2.input.setRawMode(true);
|
|
2843
|
+
const terminal = openTerminalSession({ interactive, mouse: cfg.mouse, write: (text) => io2.output.write(text) });
|
|
2844
2844
|
const frameOutput = interactive ? createFrameOutput({
|
|
2845
|
-
write: (text) =>
|
|
2845
|
+
write: (text) => io2.output.write(text),
|
|
2846
2846
|
onDrain: (ready) => {
|
|
2847
|
-
|
|
2847
|
+
io2.output.once("drain", ready);
|
|
2848
2848
|
return () => {
|
|
2849
|
-
|
|
2849
|
+
io2.output.off("drain", ready);
|
|
2850
2850
|
};
|
|
2851
2851
|
}
|
|
2852
2852
|
}) : void 0;
|
|
@@ -2877,9 +2877,9 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2877
2877
|
const paint2 = () => {
|
|
2878
2878
|
clearTimeout(paintTimer);
|
|
2879
2879
|
paintTimer = void 0;
|
|
2880
|
-
const cols =
|
|
2880
|
+
const cols = io2.output.columns ?? FALLBACK_COLUMNS;
|
|
2881
2881
|
const viewW = viewWidth();
|
|
2882
|
-
panelContentRows = clampPanelRows(panelContentRows,
|
|
2882
|
+
panelContentRows = clampPanelRows(panelContentRows, io2.output.rows ?? FALLBACK_ROWS, tabRows());
|
|
2883
2883
|
const viewH = viewHeight();
|
|
2884
2884
|
const focus = view.hoverId ?? view.selectedId;
|
|
2885
2885
|
let body;
|
|
@@ -2897,6 +2897,7 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2897
2897
|
lastHits = [];
|
|
2898
2898
|
} else {
|
|
2899
2899
|
const windowed = rendered.value;
|
|
2900
|
+
if (map.nodes.length > 0) io2.onMapRendered?.();
|
|
2900
2901
|
const maxX = Math.max(0, windowed.contentWidth - viewW);
|
|
2901
2902
|
const maxY = Math.max(0, windowed.contentHeight - viewH);
|
|
2902
2903
|
if (view.offsetX > maxX || view.offsetY > maxY || view.offsetX < 0 || view.offsetY < 0) {
|
|
@@ -2990,7 +2991,7 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2990
2991
|
} else {
|
|
2991
2992
|
const frame = HOME + rows.map((row) => row + ERASE_LINE_END).join("\n");
|
|
2992
2993
|
if (frame !== lastFrame) {
|
|
2993
|
-
|
|
2994
|
+
io2.output.write(frame);
|
|
2994
2995
|
lastFrame = frame;
|
|
2995
2996
|
}
|
|
2996
2997
|
}
|
|
@@ -2999,16 +3000,16 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
2999
3000
|
paintTimer ??= setTimeout(paint2, 16);
|
|
3000
3001
|
};
|
|
3001
3002
|
const handleResize = () => {
|
|
3002
|
-
lastCols =
|
|
3003
|
-
lastRows =
|
|
3003
|
+
lastCols = io2.output.columns ?? lastCols;
|
|
3004
|
+
lastRows = io2.output.rows ?? lastRows;
|
|
3004
3005
|
lastFrame = "";
|
|
3005
3006
|
frameOutput?.invalidate();
|
|
3006
|
-
|
|
3007
|
+
io2.output.write(CLEAR_ALL);
|
|
3007
3008
|
paint2();
|
|
3008
3009
|
};
|
|
3009
3010
|
const tick = () => {
|
|
3010
3011
|
if (takeQuitRequest(cfg.file, process.pid)) quit();
|
|
3011
|
-
if ((
|
|
3012
|
+
if ((io2.output.columns ?? lastCols) !== lastCols || (io2.output.rows ?? lastRows) !== lastRows) {
|
|
3012
3013
|
handleResize();
|
|
3013
3014
|
}
|
|
3014
3015
|
const discovered = listPageFiles(cfg.file);
|
|
@@ -3077,9 +3078,9 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
3077
3078
|
tick();
|
|
3078
3079
|
};
|
|
3079
3080
|
if (interactive) {
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3081
|
+
io2.input.resume();
|
|
3082
|
+
io2.input.setEncoding("utf8");
|
|
3083
|
+
io2.input.on("data", (chunk) => {
|
|
3083
3084
|
const parsed2 = parseInput(pendingInput + chunk);
|
|
3084
3085
|
pendingInput = parsed2.rest;
|
|
3085
3086
|
let dirty = false;
|
|
@@ -3152,7 +3153,7 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
3152
3153
|
break;
|
|
3153
3154
|
case "mouse-drag":
|
|
3154
3155
|
if (view.dividerDrag) {
|
|
3155
|
-
const next = panelRowsFromDividerY(event.y,
|
|
3156
|
+
const next = panelRowsFromDividerY(event.y, io2.output.rows ?? FALLBACK_ROWS, tabRows());
|
|
3156
3157
|
if (next !== panelContentRows) {
|
|
3157
3158
|
panelContentRows = next;
|
|
3158
3159
|
dirty = true;
|
|
@@ -3248,7 +3249,7 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
3248
3249
|
else paint2();
|
|
3249
3250
|
}
|
|
3250
3251
|
});
|
|
3251
|
-
|
|
3252
|
+
io2.output.on("resize", handleResize);
|
|
3252
3253
|
}
|
|
3253
3254
|
tick();
|
|
3254
3255
|
setInterval(tick, cfg.intervalMs);
|
|
@@ -3263,6 +3264,73 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|
|
|
3263
3264
|
}
|
|
3264
3265
|
}
|
|
3265
3266
|
|
|
3267
|
+
// src/watch/cli.ts
|
|
3268
|
+
import { homedir as homedir2 } from "node:os";
|
|
3269
|
+
import { fileURLToPath } from "node:url";
|
|
3270
|
+
|
|
3271
|
+
// src/support/star-reminder.ts
|
|
3272
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, realpathSync as realpathSync3 } from "node:fs";
|
|
3273
|
+
import { basename as basename2, dirname as dirname10, join as join9 } from "node:path";
|
|
3274
|
+
var STAR_URL = "https://github.com/GuangminJu/mellos-mapping";
|
|
3275
|
+
var DAY_MS = 864e5;
|
|
3276
|
+
function isNpmInstallation(entry) {
|
|
3277
|
+
try {
|
|
3278
|
+
const root = dirname10(dirname10(realpathSync3(entry)));
|
|
3279
|
+
if (basename2(dirname10(root)).toLowerCase() !== "node_modules" || existsSync5(join9(root, ".git")) || existsSync5(join9(root, ".codex-plugin")) || existsSync5(join9(root, ".claude-plugin"))) return false;
|
|
3280
|
+
const pkg = JSON.parse(readFileSync5(join9(root, "package.json"), "utf8"));
|
|
3281
|
+
return pkg.name === "mellos-mapping" && pkg.bin?.mmap === "dist/mmap.mjs";
|
|
3282
|
+
} catch {
|
|
3283
|
+
return false;
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
function starReminderFile(userBase) {
|
|
3287
|
+
return join9(userBase, ".mellos", "support", "star-reminder.json");
|
|
3288
|
+
}
|
|
3289
|
+
function enabledFlag(value) {
|
|
3290
|
+
return !!value && value !== "0" && value.toLowerCase() !== "false";
|
|
3291
|
+
}
|
|
3292
|
+
function validState(value) {
|
|
3293
|
+
if (!value || typeof value !== "object") return false;
|
|
3294
|
+
const s = value;
|
|
3295
|
+
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";
|
|
3296
|
+
}
|
|
3297
|
+
function createStarReminder(options) {
|
|
3298
|
+
const eligible = isNpmInstallation(options.entry) && !enabledFlag(options.env["CI"]) && !enabledFlag(options.env["MELLOS_MAPPING_NO_STAR"]);
|
|
3299
|
+
const file = starReminderFile(options.userBase);
|
|
3300
|
+
return {
|
|
3301
|
+
visit() {
|
|
3302
|
+
if (!eligible) return false;
|
|
3303
|
+
try {
|
|
3304
|
+
return withStoreLock(file, () => {
|
|
3305
|
+
const now = (options.now ?? Date.now)();
|
|
3306
|
+
if (!Number.isFinite(now) || now < 0) return false;
|
|
3307
|
+
const day = Math.floor(now / DAY_MS);
|
|
3308
|
+
let state;
|
|
3309
|
+
try {
|
|
3310
|
+
const saved2 = JSON.parse(readFileSync5(file, "utf8"));
|
|
3311
|
+
if (!validState(saved2)) return false;
|
|
3312
|
+
state = saved2;
|
|
3313
|
+
} catch (error) {
|
|
3314
|
+
if (error.code !== "ENOENT") return false;
|
|
3315
|
+
state = { version: 1, firstSeen: now, lastDay: day, days: 1, notified: false };
|
|
3316
|
+
}
|
|
3317
|
+
if (state.notified || now < state.firstSeen || day < state.lastDay) return false;
|
|
3318
|
+
if (day > state.lastDay) {
|
|
3319
|
+
state.days = Math.min(3, state.days + 1);
|
|
3320
|
+
state.lastDay = day;
|
|
3321
|
+
}
|
|
3322
|
+
const show = state.days >= 3 && now - state.firstSeen >= 7 * DAY_MS;
|
|
3323
|
+
if (show) state.notified = true;
|
|
3324
|
+
const saved = writeAtomic(file, JSON.stringify(state, null, 2) + "\n", 1);
|
|
3325
|
+
return saved.ok && show;
|
|
3326
|
+
});
|
|
3327
|
+
} catch {
|
|
3328
|
+
return false;
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3266
3334
|
// src/watch/cli.ts
|
|
3267
3335
|
var parsed = parseArgs(process.argv.slice(2), process.cwd());
|
|
3268
3336
|
if (!parsed.ok) {
|
|
@@ -3270,4 +3338,17 @@ if (!parsed.ok) {
|
|
|
3270
3338
|
${USAGE}`);
|
|
3271
3339
|
process.exit(1);
|
|
3272
3340
|
}
|
|
3273
|
-
|
|
3341
|
+
var viewed = false;
|
|
3342
|
+
var io = nativeWatcherIO();
|
|
3343
|
+
var reminder = createStarReminder({ entry: fileURLToPath(import.meta.url), userBase: homedir2(), env: process.env });
|
|
3344
|
+
runWatcher(parsed.value, { ...io, onMapRendered: () => {
|
|
3345
|
+
viewed = true;
|
|
3346
|
+
} });
|
|
3347
|
+
process.on("exit", (code) => {
|
|
3348
|
+
if (code === 0 && viewed && io.interactive && process.stderr.isTTY && process.env["TERM"] !== "dumb" && reminder.visit()) {
|
|
3349
|
+
process.stderr.write(`
|
|
3350
|
+
If Mellos Mapping has been useful, a GitHub Star is welcome: ${STAR_URL}
|
|
3351
|
+
This optional reminder will not appear again.
|
|
3352
|
+
`);
|
|
3353
|
+
}
|
|
3354
|
+
});
|
package/dist/web/app.css
CHANGED
|
@@ -724,7 +724,8 @@ h2 {
|
|
|
724
724
|
margin-left: 20px;
|
|
725
725
|
}
|
|
726
726
|
footer {
|
|
727
|
-
height: 29px;
|
|
727
|
+
min-height: 29px;
|
|
728
|
+
flex-wrap: wrap;
|
|
728
729
|
flex-shrink: 0;
|
|
729
730
|
border-top: 1px solid var(--line);
|
|
730
731
|
display: flex;
|
|
@@ -919,7 +920,7 @@ dialog p {
|
|
|
919
920
|
#app {
|
|
920
921
|
display: grid;
|
|
921
922
|
grid-template-columns: minmax(0, 1fr) 300px;
|
|
922
|
-
grid-template-rows: 58px auto auto minmax(0, 1fr)
|
|
923
|
+
grid-template-rows: 58px auto auto minmax(0, 1fr) auto;
|
|
923
924
|
}
|
|
924
925
|
header {
|
|
925
926
|
grid-column: 1 / -1;
|
|
@@ -965,3 +966,106 @@ dialog p {
|
|
|
965
966
|
grid-column: 1 / -1;
|
|
966
967
|
}
|
|
967
968
|
}
|
|
969
|
+
|
|
970
|
+
#star-reminder {
|
|
971
|
+
--star-surface: linear-gradient(115deg, #15302e, #202341 48%, #36243b);
|
|
972
|
+
--star-text: #f2f5ff;
|
|
973
|
+
--star-muted: #c9d2e5;
|
|
974
|
+
width: 100%;
|
|
975
|
+
display: flex;
|
|
976
|
+
flex-wrap: wrap;
|
|
977
|
+
align-items: center;
|
|
978
|
+
gap: 10px 14px;
|
|
979
|
+
margin: 4px 0 8px;
|
|
980
|
+
padding: 12px 16px;
|
|
981
|
+
border: 1px solid transparent;
|
|
982
|
+
border-radius: 12px;
|
|
983
|
+
background: var(--star-surface) padding-box,
|
|
984
|
+
linear-gradient(110deg, #69ebc9, #8299ff, #d897ff, #ffc17d, #69ebc9) border-box;
|
|
985
|
+
background-size: 200% 100%;
|
|
986
|
+
color: var(--star-text);
|
|
987
|
+
font-size: 14px;
|
|
988
|
+
line-height: 1.5;
|
|
989
|
+
animation: star-color-flow 6s ease-in-out 2, star-glow 3s ease-in-out 4;
|
|
990
|
+
}
|
|
991
|
+
#star-reminder::before {
|
|
992
|
+
content: "★";
|
|
993
|
+
display: grid;
|
|
994
|
+
place-items: center;
|
|
995
|
+
flex: 0 0 36px;
|
|
996
|
+
height: 36px;
|
|
997
|
+
border: 1px solid #ffe49950;
|
|
998
|
+
border-radius: 10px;
|
|
999
|
+
background: #f9c76916;
|
|
1000
|
+
color: #ffe49b;
|
|
1001
|
+
font-size: 26px;
|
|
1002
|
+
line-height: 1;
|
|
1003
|
+
text-shadow: 0 0 14px #ffd47870;
|
|
1004
|
+
animation: star-beat 3s ease-in-out 4;
|
|
1005
|
+
}
|
|
1006
|
+
#star-reminder > span { flex: 1 1 230px; font-weight: 550; }
|
|
1007
|
+
#star-reminder a {
|
|
1008
|
+
display: inline-flex;
|
|
1009
|
+
align-items: center;
|
|
1010
|
+
justify-content: center;
|
|
1011
|
+
min-height: 36px;
|
|
1012
|
+
padding: 7px 16px;
|
|
1013
|
+
border: 1px solid #ffffff50;
|
|
1014
|
+
border-radius: 8px;
|
|
1015
|
+
background: linear-gradient(110deg, #8bf0d0, #babaff 55%, #ffd0aa);
|
|
1016
|
+
color: #17243a;
|
|
1017
|
+
font-size: 13px;
|
|
1018
|
+
font-weight: 750;
|
|
1019
|
+
line-height: 1.4;
|
|
1020
|
+
text-decoration: none;
|
|
1021
|
+
white-space: nowrap;
|
|
1022
|
+
box-shadow: 0 2px 12px #8b96ff25;
|
|
1023
|
+
}
|
|
1024
|
+
#star-reminder a:hover { filter: brightness(1.1); }
|
|
1025
|
+
#star-reminder a[aria-disabled=true] { cursor: progress; opacity: .8; }
|
|
1026
|
+
#star-reminder .star-open-error { flex: 1 1 100%; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 12px; color: var(--star-muted); }
|
|
1027
|
+
#star-reminder .star-open-error input { min-width: 0; max-width: 100%; width: 340px; padding: 6px; color: var(--star-text); background: transparent; border: 1px solid #b8c5e470; border-radius: 6px; }
|
|
1028
|
+
#star-reminder button {
|
|
1029
|
+
min-height: 34px;
|
|
1030
|
+
padding: 6px 10px;
|
|
1031
|
+
border: 1px solid #b8c5e42e;
|
|
1032
|
+
border-radius: 8px;
|
|
1033
|
+
background: transparent;
|
|
1034
|
+
color: var(--star-muted);
|
|
1035
|
+
font-size: 12px;
|
|
1036
|
+
line-height: 1.4;
|
|
1037
|
+
white-space: nowrap;
|
|
1038
|
+
}
|
|
1039
|
+
#star-reminder button:hover { background: #a0b5f014; border-color: #b8c5e470; }
|
|
1040
|
+
#star-reminder a:focus-visible, #star-reminder button:focus-visible {
|
|
1041
|
+
outline: 2px solid #b0b9ff;
|
|
1042
|
+
outline-offset: 3px;
|
|
1043
|
+
}
|
|
1044
|
+
#star-reminder:hover, #star-reminder:focus-within,
|
|
1045
|
+
#star-reminder:hover::before, #star-reminder:focus-within::before { animation-play-state: paused; }
|
|
1046
|
+
[data-theme=light] #star-reminder {
|
|
1047
|
+
--star-surface: linear-gradient(115deg, #e4fbf5, #eeedff 48%, #fff0e5);
|
|
1048
|
+
--star-text: #263451;
|
|
1049
|
+
--star-muted: #53607b;
|
|
1050
|
+
}
|
|
1051
|
+
[data-theme=light] #star-reminder::before { color: #a5660c; background: #ffe6aa80; border-color: #dbae6340; text-shadow: none; }
|
|
1052
|
+
@keyframes star-color-flow {
|
|
1053
|
+
0%, 100% { background-position: 0% 50%; }
|
|
1054
|
+
50% { background-position: 100% 50%; }
|
|
1055
|
+
}
|
|
1056
|
+
@keyframes star-glow {
|
|
1057
|
+
0%, 100% { box-shadow: 0 0 0 #8299ff00; }
|
|
1058
|
+
50% { box-shadow: 0 0 16px #8299ff30, inset 0 0 18px #b897ff0c; }
|
|
1059
|
+
}
|
|
1060
|
+
@keyframes star-beat {
|
|
1061
|
+
0%, 100% { transform: scale(1) rotate(0deg); }
|
|
1062
|
+
50% { transform: scale(1.1) rotate(-7deg); }
|
|
1063
|
+
}
|
|
1064
|
+
@media (max-width: 520px) {
|
|
1065
|
+
#star-reminder { padding: 12px; gap: 10px; }
|
|
1066
|
+
#star-reminder > span { flex-basis: calc(100% - 50px); font-size: 13px; }
|
|
1067
|
+
#star-reminder a { margin-left: 46px; }
|
|
1068
|
+
}
|
|
1069
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1070
|
+
#star-reminder, #star-reminder::before { animation: none; }
|
|
1071
|
+
}
|
package/dist/web/app.js
CHANGED
|
@@ -904,6 +904,82 @@ function zoomScene(state, factor, x, y) {
|
|
|
904
904
|
};
|
|
905
905
|
}
|
|
906
906
|
|
|
907
|
+
// src/web/star-reminder.ts
|
|
908
|
+
function createStarNotice(base2) {
|
|
909
|
+
let attempted = false;
|
|
910
|
+
const notice = document.getElementById("star-reminder");
|
|
911
|
+
const link = notice.querySelector("a");
|
|
912
|
+
let opening = false;
|
|
913
|
+
link.title = "\u5728\u7CFB\u7EDF\u9ED8\u8BA4\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00 GitHub";
|
|
914
|
+
const dismiss = () => {
|
|
915
|
+
notice.hidden = true;
|
|
916
|
+
};
|
|
917
|
+
notice.querySelector("button").addEventListener("click", dismiss);
|
|
918
|
+
const launch = async (event) => {
|
|
919
|
+
event.preventDefault();
|
|
920
|
+
if (opening) return;
|
|
921
|
+
opening = true;
|
|
922
|
+
link.setAttribute("aria-disabled", "true");
|
|
923
|
+
link.textContent = "\u6B63\u5728\u6253\u5F00\u2026";
|
|
924
|
+
notice.querySelector(".star-open-error")?.remove();
|
|
925
|
+
try {
|
|
926
|
+
const response = await fetch(new URL("api/star-reminder/open", base2), { method: "POST", signal: AbortSignal.timeout(8e3) });
|
|
927
|
+
const result = await response.json();
|
|
928
|
+
if (!response.ok || result.opened !== true) throw new Error("Not opened");
|
|
929
|
+
dismiss();
|
|
930
|
+
} catch {
|
|
931
|
+
const error = document.createElement("div");
|
|
932
|
+
error.className = "star-open-error";
|
|
933
|
+
error.setAttribute("role", "status");
|
|
934
|
+
const message = document.createElement("span");
|
|
935
|
+
message.textContent = "\u9ED8\u8BA4\u6D4F\u89C8\u5668\u672A\u80FD\u6253\u5F00\uFF0C\u53EF\u91CD\u8BD5\u6216\u590D\u5236\u5730\u5740\u3002";
|
|
936
|
+
const copy = document.createElement("button");
|
|
937
|
+
copy.type = "button";
|
|
938
|
+
copy.textContent = "\u590D\u5236 GitHub \u5730\u5740";
|
|
939
|
+
copy.addEventListener("click", () => {
|
|
940
|
+
void (async () => {
|
|
941
|
+
try {
|
|
942
|
+
await navigator.clipboard.writeText(link.href);
|
|
943
|
+
copy.textContent = "\u5DF2\u590D\u5236";
|
|
944
|
+
} catch {
|
|
945
|
+
const address = document.createElement("input");
|
|
946
|
+
address.readOnly = true;
|
|
947
|
+
address.value = link.href;
|
|
948
|
+
address.setAttribute("aria-label", "GitHub \u5730\u5740");
|
|
949
|
+
copy.replaceWith(address);
|
|
950
|
+
address.focus();
|
|
951
|
+
address.select();
|
|
952
|
+
}
|
|
953
|
+
})();
|
|
954
|
+
});
|
|
955
|
+
error.append(message, copy);
|
|
956
|
+
notice.append(error);
|
|
957
|
+
} finally {
|
|
958
|
+
opening = false;
|
|
959
|
+
link.removeAttribute("aria-disabled");
|
|
960
|
+
link.textContent = "\u53BB\u70B9 Star \u2197";
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
link.addEventListener("click", (event) => {
|
|
964
|
+
void launch(event);
|
|
965
|
+
});
|
|
966
|
+
link.addEventListener("auxclick", (event) => {
|
|
967
|
+
if (event.button === 1) void launch(event);
|
|
968
|
+
});
|
|
969
|
+
return (page) => {
|
|
970
|
+
if (attempted || document.visibilityState !== "visible") return;
|
|
971
|
+
attempted = true;
|
|
972
|
+
const url = new URL("api/star-reminder/visit", base2);
|
|
973
|
+
if (page) url.searchParams.set("page", page);
|
|
974
|
+
void fetch(url, { method: "POST", signal: AbortSignal.timeout(3e3) }).then(async (response) => {
|
|
975
|
+
if (!response.ok) return;
|
|
976
|
+
const result = await response.json();
|
|
977
|
+
if (result.show === true && document.visibilityState === "visible") notice.hidden = false;
|
|
978
|
+
}).catch(() => {
|
|
979
|
+
});
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
|
|
907
983
|
// src/web/app.ts
|
|
908
984
|
var element = (id) => document.getElementById(id);
|
|
909
985
|
var viewport = element("viewport");
|
|
@@ -913,6 +989,7 @@ var pages = element("pages");
|
|
|
913
989
|
var search = element("search");
|
|
914
990
|
var dialog = element("dialog");
|
|
915
991
|
var base = new URL(".", window.location.href);
|
|
992
|
+
var starNotice = createStarNotice(base);
|
|
916
993
|
var kinds = { dev: "\u5F00\u53D1\u5730\u56FE", architecture: "\u67B6\u6784\u5730\u56FE", dataflow: "\u6570\u636E\u6D41", "behavior-tree": "\u884C\u4E3A\u6811", sequence: "\u65F6\u5E8F\u5730\u56FE" };
|
|
917
994
|
var snapshot = { project: "", pages: [] };
|
|
918
995
|
var revision = "";
|
|
@@ -1340,6 +1417,7 @@ async function poll() {
|
|
|
1340
1417
|
}
|
|
1341
1418
|
if (initial || JSON.stringify(priorPage) !== JSON.stringify(current())) render();
|
|
1342
1419
|
else renderHeading();
|
|
1420
|
+
if ((current()?.map?.nodes.length ?? 0) > 0 && !current()?.error) starNotice(currentId);
|
|
1343
1421
|
element("updated").textContent = `\u5DF2\u540C\u6B65 ${(/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false })}`;
|
|
1344
1422
|
}
|
|
1345
1423
|
}
|
package/dist/web/index.html
CHANGED
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
<section class="map-heading"><div class="project-line"><span id="project">本地项目</span><span>/</span><span id="kind">开发地图</span></div><div class="title-row"><h1 id="title">正在读取地图…</h1><select id="pages" aria-label="切换地图页面"></select></div><div id="breadcrumbs"></div><div class="progress-row"><span id="summary"></span><div id="progress"><i></i></div><button id="follow" class="follow active" aria-pressed="true">● 自动跟随</button></div></section>
|
|
6
6
|
<section class="toolbar"><label class="search"><svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="8" cy="8" r="5.5"/><path d="m12 12 5 5"/></svg><input id="search" placeholder="搜索模块、说明…" aria-label="搜索模块"><kbd>/</kbd></label><button id="overview" class="tool-button" aria-pressed="false">分组概览</button><button id="export" class="icon-button" title="导出 SVG" aria-label="导出 SVG">↓</button><button id="delete" class="icon-button" title="删除当前地图" aria-label="删除当前地图">×</button></section>
|
|
7
7
|
<section class="canvas-shell"><div id="viewport" tabindex="0" aria-label="交互地图画布"><div id="stage"></div><div id="empty" hidden></div></div><div id="filters" class="filters" aria-label="按状态筛选"><button data-status="all" class="active">全部</button><button data-status="planned"><i class="planned"></i>待开发</button><button data-status="in-progress"><i class="in-progress"></i>进行中</button><button data-status="done"><i class="done"></i>已验证</button><button data-status="regressed"><i class="regressed"></i>回归</button><button data-status="unverified"><i class="unverified"></i>待验证</button></div><div class="zoom-controls"><button id="zoom-out" aria-label="缩小">−</button><output id="zoom-label">100%</output><button id="zoom-in" aria-label="放大">+</button><span></span><button id="fit" title="适应窗口 · 0" aria-label="适应窗口">⛶</button></div><div id="minimap" title="点击移动视口"></div><div id="match-count" role="status"></div></section>
|
|
8
|
-
<div id="divider" role="separator" aria-label="调整详情高度" aria-orientation="horizontal" aria-valuemin="100" aria-valuemax="500" aria-valuenow="210" tabindex="0"><i></i></div><aside id="inspector"></aside><footer><span>滚轮缩放 · 拖动画布 · 双击进入子地图</span><span id="updated">等待同步</span></footer>
|
|
8
|
+
<div id="divider" role="separator" aria-label="调整详情高度" aria-orientation="horizontal" aria-valuemin="100" aria-valuemax="500" aria-valuenow="210" tabindex="0"><i></i></div><aside id="inspector"></aside><footer><span>滚轮缩放 · 拖动画布 · 双击进入子地图</span><span id="updated">等待同步</span><aside id="star-reminder" aria-label="支持 Mellos Mapping" hidden><span>地图有帮到你?欢迎在 GitHub 点个 Star。</span><a href="https://github.com/GuangminJu/mellos-mapping" target="_blank" rel="noopener noreferrer">去点 Star ↗</a><button type="button">不再提醒</button></aside></footer>
|
|
9
9
|
</main><div id="toast" role="status" hidden></div><dialog id="dialog"><h2 id="dialog-title"></h2><p id="dialog-body"></p><div class="dialog-actions"><button id="dialog-cancel">关闭</button><button id="dialog-confirm" class="danger" hidden>删除此地图</button></div></dialog></body></html>
|
package/dist/web/terminal.css
CHANGED
|
@@ -7,3 +7,106 @@ header{display:flex;align-items:center;justify-content:space-between;gap:12px;pa
|
|
|
7
7
|
footer{min-height:32px;display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;padding:5px 14px;border-top:1px solid #283343;color:#8c9eb5;font-size:11px}#connection{overflow-wrap:anywhere}#connection[data-state="connected"]{color:#81dbae}#connection[data-state="error"]{color:#ffaf87}.hint{margin-left:auto}[hidden]{display:none!important}
|
|
8
8
|
@media(max-width:440px){.brand span{margin-left:6px}.hint{margin-left:0;width:100%}header{padding:0 10px}#terminal .xterm{padding-left:4px;padding-right:4px}}
|
|
9
9
|
dialog{max-width:min(420px,90vw);border:1px solid #3b4b60;border-radius:10px;background:#151c25;color:#d9e2ed;padding:24px}dialog::backdrop{background:#0009}dialog h2{font-size:18px;margin-top:0}dialog p{line-height:1.9;font-size:13px}dialog form{text-align:right}
|
|
10
|
+
|
|
11
|
+
#star-reminder {
|
|
12
|
+
--star-surface: linear-gradient(115deg, #15302e, #202341 48%, #36243b);
|
|
13
|
+
--star-text: #f2f5ff;
|
|
14
|
+
--star-muted: #c9d2e5;
|
|
15
|
+
width: 100%;
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-wrap: wrap;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 10px 14px;
|
|
20
|
+
margin: 4px 0 8px;
|
|
21
|
+
padding: 12px 16px;
|
|
22
|
+
border: 1px solid transparent;
|
|
23
|
+
border-radius: 12px;
|
|
24
|
+
background: var(--star-surface) padding-box,
|
|
25
|
+
linear-gradient(110deg, #69ebc9, #8299ff, #d897ff, #ffc17d, #69ebc9) border-box;
|
|
26
|
+
background-size: 200% 100%;
|
|
27
|
+
color: var(--star-text);
|
|
28
|
+
font-size: 14px;
|
|
29
|
+
line-height: 1.5;
|
|
30
|
+
animation: star-color-flow 6s ease-in-out 2, star-glow 3s ease-in-out 4;
|
|
31
|
+
}
|
|
32
|
+
#star-reminder::before {
|
|
33
|
+
content: "★";
|
|
34
|
+
display: grid;
|
|
35
|
+
place-items: center;
|
|
36
|
+
flex: 0 0 36px;
|
|
37
|
+
height: 36px;
|
|
38
|
+
border: 1px solid #ffe49950;
|
|
39
|
+
border-radius: 10px;
|
|
40
|
+
background: #f9c76916;
|
|
41
|
+
color: #ffe49b;
|
|
42
|
+
font-size: 26px;
|
|
43
|
+
line-height: 1;
|
|
44
|
+
text-shadow: 0 0 14px #ffd47870;
|
|
45
|
+
animation: star-beat 3s ease-in-out 4;
|
|
46
|
+
}
|
|
47
|
+
#star-reminder > span { flex: 1 1 230px; font-weight: 550; }
|
|
48
|
+
#star-reminder a {
|
|
49
|
+
display: inline-flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
min-height: 36px;
|
|
53
|
+
padding: 7px 16px;
|
|
54
|
+
border: 1px solid #ffffff50;
|
|
55
|
+
border-radius: 8px;
|
|
56
|
+
background: linear-gradient(110deg, #8bf0d0, #babaff 55%, #ffd0aa);
|
|
57
|
+
color: #17243a;
|
|
58
|
+
font-size: 13px;
|
|
59
|
+
font-weight: 750;
|
|
60
|
+
line-height: 1.4;
|
|
61
|
+
text-decoration: none;
|
|
62
|
+
white-space: nowrap;
|
|
63
|
+
box-shadow: 0 2px 12px #8b96ff25;
|
|
64
|
+
}
|
|
65
|
+
#star-reminder a:hover { filter: brightness(1.1); }
|
|
66
|
+
#star-reminder a[aria-disabled=true] { cursor: progress; opacity: .8; }
|
|
67
|
+
#star-reminder .star-open-error { flex: 1 1 100%; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 12px; color: var(--star-muted); }
|
|
68
|
+
#star-reminder .star-open-error input { min-width: 0; max-width: 100%; width: 340px; padding: 6px; color: var(--star-text); background: transparent; border: 1px solid #b8c5e470; border-radius: 6px; }
|
|
69
|
+
#star-reminder button {
|
|
70
|
+
min-height: 34px;
|
|
71
|
+
padding: 6px 10px;
|
|
72
|
+
border: 1px solid #b8c5e42e;
|
|
73
|
+
border-radius: 8px;
|
|
74
|
+
background: transparent;
|
|
75
|
+
color: var(--star-muted);
|
|
76
|
+
font-size: 12px;
|
|
77
|
+
line-height: 1.4;
|
|
78
|
+
white-space: nowrap;
|
|
79
|
+
}
|
|
80
|
+
#star-reminder button:hover { background: #a0b5f014; border-color: #b8c5e470; }
|
|
81
|
+
#star-reminder a:focus-visible, #star-reminder button:focus-visible {
|
|
82
|
+
outline: 2px solid #b0b9ff;
|
|
83
|
+
outline-offset: 3px;
|
|
84
|
+
}
|
|
85
|
+
#star-reminder:hover, #star-reminder:focus-within,
|
|
86
|
+
#star-reminder:hover::before, #star-reminder:focus-within::before { animation-play-state: paused; }
|
|
87
|
+
[data-theme=light] #star-reminder {
|
|
88
|
+
--star-surface: linear-gradient(115deg, #e4fbf5, #eeedff 48%, #fff0e5);
|
|
89
|
+
--star-text: #263451;
|
|
90
|
+
--star-muted: #53607b;
|
|
91
|
+
}
|
|
92
|
+
[data-theme=light] #star-reminder::before { color: #a5660c; background: #ffe6aa80; border-color: #dbae6340; text-shadow: none; }
|
|
93
|
+
@keyframes star-color-flow {
|
|
94
|
+
0%, 100% { background-position: 0% 50%; }
|
|
95
|
+
50% { background-position: 100% 50%; }
|
|
96
|
+
}
|
|
97
|
+
@keyframes star-glow {
|
|
98
|
+
0%, 100% { box-shadow: 0 0 0 #8299ff00; }
|
|
99
|
+
50% { box-shadow: 0 0 16px #8299ff30, inset 0 0 18px #b897ff0c; }
|
|
100
|
+
}
|
|
101
|
+
@keyframes star-beat {
|
|
102
|
+
0%, 100% { transform: scale(1) rotate(0deg); }
|
|
103
|
+
50% { transform: scale(1.1) rotate(-7deg); }
|
|
104
|
+
}
|
|
105
|
+
@media (max-width: 520px) {
|
|
106
|
+
#star-reminder { padding: 12px; gap: 10px; }
|
|
107
|
+
#star-reminder > span { flex-basis: calc(100% - 50px); font-size: 13px; }
|
|
108
|
+
#star-reminder a { margin-left: 46px; }
|
|
109
|
+
}
|
|
110
|
+
@media (prefers-reduced-motion: reduce) {
|
|
111
|
+
#star-reminder, #star-reminder::before { animation: none; }
|
|
112
|
+
}
|
package/dist/web/terminal.html
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Mellos · 终端地图</title><link rel="icon" href="data:,"><link rel="stylesheet" href="./xterm.css"><link rel="stylesheet" href="./terminal.css"><script type="module" src="./terminal.js"></script></head>
|
|
3
3
|
<body><header><a class="brand" id="graphic" href="./" title="切换到图形地图">Mellos <span>图形地图 ↗</span></a><div class="tools"><label for="font-size">字号</label><select id="font-size" aria-label="终端字号"><option>10</option><option>11</option><option>12</option><option selected>13</option><option>14</option><option>15</option><option>16</option><option>18</option><option>20</option><option>24</option><option>28</option></select><button id="help" aria-label="操作帮助">?</button></div></header>
|
|
4
4
|
<main id="terminal" aria-label="Mellos 终端地图"></main>
|
|
5
|
-
<footer><span id="connection" role="status" aria-live="polite">正在连接…</span><button id="restart" hidden>重新连接</button><span class="hint">滚轮缩放 · 拖动平移 · 点击选择 · ? 帮助</span></footer>
|
|
5
|
+
<footer><span id="connection" role="status" aria-live="polite">正在连接…</span><button id="restart" hidden>重新连接</button><span class="hint">滚轮缩放 · 拖动平移 · 点击选择 · ? 帮助</span><aside id="star-reminder" aria-label="支持 Mellos Mapping" hidden><span>地图有帮到你?欢迎在 GitHub 点个 Star。</span><a href="https://github.com/GuangminJu/mellos-mapping" target="_blank" rel="noopener noreferrer">去点 Star ↗</a><button type="button">不再提醒</button></aside></footer>
|
|
6
6
|
<dialog id="help-dialog"><h2>终端地图操作</h2><p>滚轮 / + / −:总览 → 模块 → 细节<br>拖动:平移 · 单击:固定详情<br>双击带 ⊞ 的节点:进入子地图<br>Backspace:返回父地图<br>Tab / 数字 1–9:切换页面<br>f:自动跟随 · 0:重置视图<br>x 按两次:确认删除当前页<br>q:关闭地图,可用「重新连接」再次打开</p><p>右上角字号只影响此网页终端。</p><form method="dialog"><button>关闭帮助</button></form></dialog>
|
|
7
7
|
</body></html>
|
package/dist/web/terminal.js
CHANGED
|
@@ -9137,6 +9137,82 @@ var o = class {
|
|
|
9137
9137
|
}
|
|
9138
9138
|
};
|
|
9139
9139
|
|
|
9140
|
+
// src/web/star-reminder.ts
|
|
9141
|
+
function createStarNotice(base2) {
|
|
9142
|
+
let attempted = false;
|
|
9143
|
+
const notice = document.getElementById("star-reminder");
|
|
9144
|
+
const link = notice.querySelector("a");
|
|
9145
|
+
let opening = false;
|
|
9146
|
+
link.title = "\u5728\u7CFB\u7EDF\u9ED8\u8BA4\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00 GitHub";
|
|
9147
|
+
const dismiss = () => {
|
|
9148
|
+
notice.hidden = true;
|
|
9149
|
+
};
|
|
9150
|
+
notice.querySelector("button").addEventListener("click", dismiss);
|
|
9151
|
+
const launch = async (event) => {
|
|
9152
|
+
event.preventDefault();
|
|
9153
|
+
if (opening) return;
|
|
9154
|
+
opening = true;
|
|
9155
|
+
link.setAttribute("aria-disabled", "true");
|
|
9156
|
+
link.textContent = "\u6B63\u5728\u6253\u5F00\u2026";
|
|
9157
|
+
notice.querySelector(".star-open-error")?.remove();
|
|
9158
|
+
try {
|
|
9159
|
+
const response = await fetch(new URL("api/star-reminder/open", base2), { method: "POST", signal: AbortSignal.timeout(8e3) });
|
|
9160
|
+
const result = await response.json();
|
|
9161
|
+
if (!response.ok || result.opened !== true) throw new Error("Not opened");
|
|
9162
|
+
dismiss();
|
|
9163
|
+
} catch {
|
|
9164
|
+
const error = document.createElement("div");
|
|
9165
|
+
error.className = "star-open-error";
|
|
9166
|
+
error.setAttribute("role", "status");
|
|
9167
|
+
const message = document.createElement("span");
|
|
9168
|
+
message.textContent = "\u9ED8\u8BA4\u6D4F\u89C8\u5668\u672A\u80FD\u6253\u5F00\uFF0C\u53EF\u91CD\u8BD5\u6216\u590D\u5236\u5730\u5740\u3002";
|
|
9169
|
+
const copy = document.createElement("button");
|
|
9170
|
+
copy.type = "button";
|
|
9171
|
+
copy.textContent = "\u590D\u5236 GitHub \u5730\u5740";
|
|
9172
|
+
copy.addEventListener("click", () => {
|
|
9173
|
+
void (async () => {
|
|
9174
|
+
try {
|
|
9175
|
+
await navigator.clipboard.writeText(link.href);
|
|
9176
|
+
copy.textContent = "\u5DF2\u590D\u5236";
|
|
9177
|
+
} catch {
|
|
9178
|
+
const address = document.createElement("input");
|
|
9179
|
+
address.readOnly = true;
|
|
9180
|
+
address.value = link.href;
|
|
9181
|
+
address.setAttribute("aria-label", "GitHub \u5730\u5740");
|
|
9182
|
+
copy.replaceWith(address);
|
|
9183
|
+
address.focus();
|
|
9184
|
+
address.select();
|
|
9185
|
+
}
|
|
9186
|
+
})();
|
|
9187
|
+
});
|
|
9188
|
+
error.append(message, copy);
|
|
9189
|
+
notice.append(error);
|
|
9190
|
+
} finally {
|
|
9191
|
+
opening = false;
|
|
9192
|
+
link.removeAttribute("aria-disabled");
|
|
9193
|
+
link.textContent = "\u53BB\u70B9 Star \u2197";
|
|
9194
|
+
}
|
|
9195
|
+
};
|
|
9196
|
+
link.addEventListener("click", (event) => {
|
|
9197
|
+
void launch(event);
|
|
9198
|
+
});
|
|
9199
|
+
link.addEventListener("auxclick", (event) => {
|
|
9200
|
+
if (event.button === 1) void launch(event);
|
|
9201
|
+
});
|
|
9202
|
+
return (page2) => {
|
|
9203
|
+
if (attempted || document.visibilityState !== "visible") return;
|
|
9204
|
+
attempted = true;
|
|
9205
|
+
const url = new URL("api/star-reminder/visit", base2);
|
|
9206
|
+
if (page2) url.searchParams.set("page", page2);
|
|
9207
|
+
void fetch(url, { method: "POST", signal: AbortSignal.timeout(3e3) }).then(async (response) => {
|
|
9208
|
+
if (!response.ok) return;
|
|
9209
|
+
const result = await response.json();
|
|
9210
|
+
if (result.show === true && document.visibilityState === "visible") notice.hidden = false;
|
|
9211
|
+
}).catch(() => {
|
|
9212
|
+
});
|
|
9213
|
+
};
|
|
9214
|
+
}
|
|
9215
|
+
|
|
9140
9216
|
// src/web/terminal-app.ts
|
|
9141
9217
|
var element = (id) => document.getElementById(id);
|
|
9142
9218
|
var container = element("terminal");
|
|
@@ -9145,6 +9221,7 @@ var restart = element("restart");
|
|
|
9145
9221
|
var font = element("font-size");
|
|
9146
9222
|
var graphic = element("graphic");
|
|
9147
9223
|
var base = new URL(".", location.href);
|
|
9224
|
+
var starNotice = createStarNotice(base);
|
|
9148
9225
|
var page = new URL(location.href).searchParams.get("page") ?? void 0;
|
|
9149
9226
|
var socket;
|
|
9150
9227
|
var retryTimer;
|
|
@@ -9264,6 +9341,7 @@ function connect() {
|
|
|
9264
9341
|
} else if (message.type === "view") {
|
|
9265
9342
|
page = message.page;
|
|
9266
9343
|
syncPage();
|
|
9344
|
+
starNotice(page);
|
|
9267
9345
|
} else if (message.type === "error") setStatus(message.message, "error");
|
|
9268
9346
|
});
|
|
9269
9347
|
connection.addEventListener("close", (event) => {
|
package/dist/web.mjs
CHANGED
|
@@ -3703,9 +3703,10 @@ var require_websocket_server = __commonJS({
|
|
|
3703
3703
|
});
|
|
3704
3704
|
|
|
3705
3705
|
// src/web/cli.ts
|
|
3706
|
-
import { existsSync as
|
|
3707
|
-
import { dirname as
|
|
3706
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync3, rmSync as rmSync4, statSync as statSync2 } from "node:fs";
|
|
3707
|
+
import { dirname as dirname9, join as join7, resolve as resolve2 } from "node:path";
|
|
3708
3708
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3709
|
+
import { homedir } from "node:os";
|
|
3709
3710
|
|
|
3710
3711
|
// src/domain/types.ts
|
|
3711
3712
|
var ok = (value) => ({ ok: true, value });
|
|
@@ -5435,12 +5436,117 @@ function attachTerminalService(server, options) {
|
|
|
5435
5436
|
};
|
|
5436
5437
|
}
|
|
5437
5438
|
|
|
5439
|
+
// src/support/open-star.ts
|
|
5440
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
5441
|
+
import { win32 } from "node:path";
|
|
5442
|
+
|
|
5443
|
+
// src/support/star-reminder.ts
|
|
5444
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, realpathSync as realpathSync2 } from "node:fs";
|
|
5445
|
+
import { basename as basename3, dirname as dirname8, join as join6 } from "node:path";
|
|
5446
|
+
var STAR_URL = "https://github.com/GuangminJu/mellos-mapping";
|
|
5447
|
+
var DAY_MS = 864e5;
|
|
5448
|
+
function isNpmInstallation(entry) {
|
|
5449
|
+
try {
|
|
5450
|
+
const root = dirname8(dirname8(realpathSync2(entry)));
|
|
5451
|
+
if (basename3(dirname8(root)).toLowerCase() !== "node_modules" || existsSync4(join6(root, ".git")) || existsSync4(join6(root, ".codex-plugin")) || existsSync4(join6(root, ".claude-plugin"))) return false;
|
|
5452
|
+
const pkg = JSON.parse(readFileSync5(join6(root, "package.json"), "utf8"));
|
|
5453
|
+
return pkg.name === "mellos-mapping" && pkg.bin?.mmap === "dist/mmap.mjs";
|
|
5454
|
+
} catch {
|
|
5455
|
+
return false;
|
|
5456
|
+
}
|
|
5457
|
+
}
|
|
5458
|
+
function starReminderFile(userBase) {
|
|
5459
|
+
return join6(userBase, ".mellos", "support", "star-reminder.json");
|
|
5460
|
+
}
|
|
5461
|
+
function enabledFlag(value) {
|
|
5462
|
+
return !!value && value !== "0" && value.toLowerCase() !== "false";
|
|
5463
|
+
}
|
|
5464
|
+
function validState(value) {
|
|
5465
|
+
if (!value || typeof value !== "object") return false;
|
|
5466
|
+
const s = value;
|
|
5467
|
+
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";
|
|
5468
|
+
}
|
|
5469
|
+
function createStarReminder(options) {
|
|
5470
|
+
const eligible = isNpmInstallation(options.entry) && !enabledFlag(options.env["CI"]) && !enabledFlag(options.env["MELLOS_MAPPING_NO_STAR"]);
|
|
5471
|
+
const file = starReminderFile(options.userBase);
|
|
5472
|
+
return {
|
|
5473
|
+
visit() {
|
|
5474
|
+
if (!eligible) return false;
|
|
5475
|
+
try {
|
|
5476
|
+
return withStoreLock(file, () => {
|
|
5477
|
+
const now = (options.now ?? Date.now)();
|
|
5478
|
+
if (!Number.isFinite(now) || now < 0) return false;
|
|
5479
|
+
const day = Math.floor(now / DAY_MS);
|
|
5480
|
+
let state;
|
|
5481
|
+
try {
|
|
5482
|
+
const saved2 = JSON.parse(readFileSync5(file, "utf8"));
|
|
5483
|
+
if (!validState(saved2)) return false;
|
|
5484
|
+
state = saved2;
|
|
5485
|
+
} catch (error) {
|
|
5486
|
+
if (error.code !== "ENOENT") return false;
|
|
5487
|
+
state = { version: 1, firstSeen: now, lastDay: day, days: 1, notified: false };
|
|
5488
|
+
}
|
|
5489
|
+
if (state.notified || now < state.firstSeen || day < state.lastDay) return false;
|
|
5490
|
+
if (day > state.lastDay) {
|
|
5491
|
+
state.days = Math.min(3, state.days + 1);
|
|
5492
|
+
state.lastDay = day;
|
|
5493
|
+
}
|
|
5494
|
+
const show = state.days >= 3 && now - state.firstSeen >= 7 * DAY_MS;
|
|
5495
|
+
if (show) state.notified = true;
|
|
5496
|
+
const saved = writeAtomic(file, JSON.stringify(state, null, 2) + "\n", 1);
|
|
5497
|
+
return saved.ok && show;
|
|
5498
|
+
});
|
|
5499
|
+
} catch {
|
|
5500
|
+
return false;
|
|
5501
|
+
}
|
|
5502
|
+
}
|
|
5503
|
+
};
|
|
5504
|
+
}
|
|
5505
|
+
|
|
5506
|
+
// src/support/open-star.ts
|
|
5507
|
+
function starBrowserCommand(platform, windowsRoot = "C:\\Windows") {
|
|
5508
|
+
if (platform === "win32") return {
|
|
5509
|
+
file: win32.join(windowsRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
|
|
5510
|
+
args: [
|
|
5511
|
+
"-NoLogo",
|
|
5512
|
+
"-NoProfile",
|
|
5513
|
+
"-NonInteractive",
|
|
5514
|
+
"-WindowStyle",
|
|
5515
|
+
"Hidden",
|
|
5516
|
+
"-Command",
|
|
5517
|
+
`$ErrorActionPreference = 'Stop'; Start-Process -FilePath '${STAR_URL}'`
|
|
5518
|
+
]
|
|
5519
|
+
};
|
|
5520
|
+
if (platform === "darwin") return { file: "/usr/bin/open", args: [STAR_URL] };
|
|
5521
|
+
if (platform === "linux") return { file: "xdg-open", args: [STAR_URL] };
|
|
5522
|
+
throw new Error("No supported desktop browser launcher");
|
|
5523
|
+
}
|
|
5524
|
+
function openStarInDefaultBrowser() {
|
|
5525
|
+
const command = starBrowserCommand(process.platform, process.env["SystemRoot"]);
|
|
5526
|
+
return new Promise((resolve3, reject) => {
|
|
5527
|
+
const child = spawn3(command.file, command.args, { windowsHide: true, stdio: "ignore" });
|
|
5528
|
+
let finished = false;
|
|
5529
|
+
const finish = (error) => {
|
|
5530
|
+
if (finished) return;
|
|
5531
|
+
finished = true;
|
|
5532
|
+
clearTimeout(deadline);
|
|
5533
|
+
child.unref();
|
|
5534
|
+
if (error) reject(error);
|
|
5535
|
+
else resolve3();
|
|
5536
|
+
};
|
|
5537
|
+
const deadline = setTimeout(() => finish(), 5e3);
|
|
5538
|
+
child.once("error", (error) => finish(error));
|
|
5539
|
+
child.once("exit", (code) => finish(code === 0 ? void 0 : new Error("Default browser launch failed")));
|
|
5540
|
+
});
|
|
5541
|
+
}
|
|
5542
|
+
|
|
5438
5543
|
// src/web/service.ts
|
|
5439
5544
|
async function startWebService(defaultFile, assets, options = {}) {
|
|
5440
5545
|
const token = options.token ?? randomBytes(24).toString("hex");
|
|
5441
5546
|
if (!/^[a-f0-9]{48}$/.test(token)) throw new Error("Invalid web token");
|
|
5442
5547
|
const prefix = `/${token}/`;
|
|
5443
5548
|
let origin = "", lastRequest = Date.now(), closed = false;
|
|
5549
|
+
let openingStar = false;
|
|
5444
5550
|
const send = (res, code, body, type = "application/json; charset=utf-8") => {
|
|
5445
5551
|
res.writeHead(code, {
|
|
5446
5552
|
"Content-Type": type,
|
|
@@ -5492,6 +5598,36 @@ async function startWebService(defaultFile, assets, options = {}) {
|
|
|
5492
5598
|
send(res, 200, JSON.stringify({ file: defaultFile, pid: process.pid, formats: [1, 2], surfaces: terminalService ? ["web", "web-terminal"] : ["web"] }));
|
|
5493
5599
|
return;
|
|
5494
5600
|
}
|
|
5601
|
+
if (req.method === "POST" && route === "api/star-reminder/open") {
|
|
5602
|
+
if (req.headers.origin !== origin) {
|
|
5603
|
+
send(res, 403, '{"error":"Origin refused"}');
|
|
5604
|
+
return;
|
|
5605
|
+
}
|
|
5606
|
+
if (req.url !== `${prefix}api/star-reminder/open` || req.headers["transfer-encoding"] || Number(req.headers["content-length"] ?? 0) !== 0) {
|
|
5607
|
+
send(res, 400, '{"error":"No parameters accepted"}');
|
|
5608
|
+
return;
|
|
5609
|
+
}
|
|
5610
|
+
if (openingStar) {
|
|
5611
|
+
send(res, 409, '{"error":"Browser is already opening"}');
|
|
5612
|
+
return;
|
|
5613
|
+
}
|
|
5614
|
+
openingStar = true;
|
|
5615
|
+
void Promise.resolve().then(options.openStar ?? openStarInDefaultBrowser).then(() => {
|
|
5616
|
+
send(res, 200, '{"opened":true}');
|
|
5617
|
+
}, () => {
|
|
5618
|
+
send(res, 503, '{"error":"\u65E0\u6CD5\u6253\u5F00\u9ED8\u8BA4\u6D4F\u89C8\u5668"}');
|
|
5619
|
+
}).finally(() => {
|
|
5620
|
+
openingStar = false;
|
|
5621
|
+
});
|
|
5622
|
+
return;
|
|
5623
|
+
}
|
|
5624
|
+
if (req.method === "POST" && route === "api/star-reminder/visit") {
|
|
5625
|
+
const requested = new URL(req.url, origin).searchParams.get("page");
|
|
5626
|
+
const id = !requested || requested === "_default" ? "" : requested;
|
|
5627
|
+
const hasMap = options.starReminder && readWebSnapshot(defaultFile).value.pages.some((page) => page.id === id && !page.error && (page.map?.nodes.length ?? 0) > 0);
|
|
5628
|
+
send(res, 200, JSON.stringify({ show: !!hasMap && options.starReminder.visit() }));
|
|
5629
|
+
return;
|
|
5630
|
+
}
|
|
5495
5631
|
if (req.method === "GET" && route === "api/state") {
|
|
5496
5632
|
const snapshot = readWebSnapshot(defaultFile);
|
|
5497
5633
|
const etag = `"${snapshot.revision}"`;
|
|
@@ -5582,19 +5718,19 @@ async function runWeb(args) {
|
|
|
5582
5718
|
if (args[0] === "--serve" && args.length === 2) {
|
|
5583
5719
|
const file2 = resolve2(args[1]);
|
|
5584
5720
|
if (await runningWebUrl(file2)) return;
|
|
5585
|
-
const directory =
|
|
5721
|
+
const directory = dirname9(webRuntimeFile(file2));
|
|
5586
5722
|
mkdirSync4(directory, { recursive: true });
|
|
5587
|
-
if (
|
|
5588
|
-
const assets =
|
|
5723
|
+
if (realpathSync3(directory).toLowerCase() !== join7(realpathSync3(dirname9(directory)), "web").toLowerCase()) throw new Error("Redirected web runtime directory");
|
|
5724
|
+
const assets = join7(dirname9(entry), "web");
|
|
5589
5725
|
let service;
|
|
5590
5726
|
service = await startWebService(file2, {
|
|
5591
|
-
html:
|
|
5592
|
-
javascript:
|
|
5593
|
-
css:
|
|
5594
|
-
terminal: { html:
|
|
5595
|
-
}, { terminalWorker:
|
|
5727
|
+
html: readFileSync6(join7(assets, "index.html"), "utf8"),
|
|
5728
|
+
javascript: readFileSync6(join7(assets, "app.js"), "utf8"),
|
|
5729
|
+
css: readFileSync6(join7(assets, "app.css"), "utf8"),
|
|
5730
|
+
terminal: { html: readFileSync6(join7(assets, "terminal.html"), "utf8"), javascript: readFileSync6(join7(assets, "terminal.js"), "utf8"), css: readFileSync6(join7(assets, "terminal.css"), "utf8"), xtermCss: readFileSync6(join7(assets, "xterm.css"), "utf8") }
|
|
5731
|
+
}, { starReminder: createStarReminder({ entry, userBase: homedir(), env: process.env }), terminalWorker: join7(dirname9(entry), "terminal-worker.mjs"), onClose: () => {
|
|
5596
5732
|
try {
|
|
5597
|
-
if (JSON.parse(
|
|
5733
|
+
if (JSON.parse(readFileSync6(webRuntimeFile(file2), "utf8")).token === service.token) rmSync4(webRuntimeFile(file2));
|
|
5598
5734
|
} catch {
|
|
5599
5735
|
}
|
|
5600
5736
|
} });
|
|
@@ -5625,8 +5761,8 @@ async function runWeb(args) {
|
|
|
5625
5761
|
else throw new Error(usage);
|
|
5626
5762
|
}
|
|
5627
5763
|
const project = resolve2(args[0]);
|
|
5628
|
-
if (!
|
|
5629
|
-
const file =
|
|
5764
|
+
if (!existsSync5(project) || !statSync2(project).isDirectory()) throw new Error(`Project directory does not exist: ${project}`);
|
|
5765
|
+
const file = join7(project, STATE_FILE_RELATIVE_PATH);
|
|
5630
5766
|
if (stop) {
|
|
5631
5767
|
const url = await runningWebUrl(file);
|
|
5632
5768
|
if (url) await fetch(`${url}api/stop`, { method: "POST", signal: AbortSignal.timeout(2e3) });
|
|
@@ -5635,7 +5771,7 @@ async function runWeb(args) {
|
|
|
5635
5771
|
}
|
|
5636
5772
|
console.log(JSON.stringify({ surface: terminal ? "web-terminal" : "web", url: await openWebPreview(file, entry, page, terminal), visibility: "unconfirmed" }));
|
|
5637
5773
|
}
|
|
5638
|
-
if (process.argv[1] &&
|
|
5774
|
+
if (process.argv[1] && existsSync5(process.argv[1]) && import.meta.url === pathToFileURL(realpathSync3(process.argv[1])).href) {
|
|
5639
5775
|
runWeb(process.argv.slice(2)).catch((error) => {
|
|
5640
5776
|
console.error(String(error));
|
|
5641
5777
|
process.exitCode = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mellos-mapping",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.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",
|