mellos-mapping 0.19.0 → 0.20.2

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.
Files changed (43) hide show
  1. package/README.md +366 -56
  2. package/README.zh-CN.md +319 -47
  3. package/dist/hook-session-start.mjs +239 -0
  4. package/dist/mmap.mjs +338 -0
  5. package/dist/server.mjs +1737 -897
  6. package/dist/store-paths.mjs +107 -0
  7. package/dist/watch.mjs +1612 -902
  8. package/lib/domain/ops.d.ts +171 -0
  9. package/lib/domain/ops.js +384 -0
  10. package/lib/domain/types.d.ts +283 -0
  11. package/lib/domain/types.js +153 -0
  12. package/lib/render/canvas.d.ts +50 -0
  13. package/lib/render/canvas.js +210 -0
  14. package/lib/render/draw.d.ts +37 -0
  15. package/lib/render/draw.js +111 -0
  16. package/lib/render/layout.d.ts +89 -0
  17. package/lib/render/layout.js +200 -0
  18. package/lib/render/options.d.ts +39 -0
  19. package/lib/render/options.js +10 -0
  20. package/lib/render/render.d.ts +88 -0
  21. package/lib/render/render.js +128 -0
  22. package/lib/render/routing.d.ts +56 -0
  23. package/lib/render/routing.js +244 -0
  24. package/lib/render/skins.d.ts +54 -0
  25. package/lib/render/skins.js +99 -0
  26. package/lib/render/width.d.ts +24 -0
  27. package/lib/render/width.js +139 -0
  28. package/lib/render/zoom-geometry.d.ts +52 -0
  29. package/lib/render/zoom-geometry.js +56 -0
  30. package/lib/semantics/semantics.d.ts +169 -0
  31. package/lib/semantics/semantics.js +380 -0
  32. package/lib/semantics/vocabulary.d.ts +79 -0
  33. package/lib/semantics/vocabulary.js +112 -0
  34. package/lib/store/format.d.ts +67 -0
  35. package/lib/store/format.js +334 -0
  36. package/lib/store/store.d.ts +296 -0
  37. package/lib/store/store.js +734 -0
  38. package/package.json +41 -5
  39. package/scripts/codex-register.mjs +89 -20
  40. package/scripts/install-mmap-command.mjs +293 -0
  41. package/scripts/mmap.mjs +213 -0
  42. package/scripts/open-pane.mjs +115 -254
  43. package/scripts/pane-core.mjs +418 -0
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
3
+
4
+ // src/hook/session-start.ts
5
+ import { spawnSync } from "node:child_process";
6
+ import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { dirname as dirname2, join as join2 } from "node:path";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
+
11
+ // src/store/store.ts
12
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
13
+ import { basename, dirname, join } from "node:path";
14
+
15
+ // src/domain/types.ts
16
+ var ok = (value) => ({ ok: true, value });
17
+ var err = (error) => ({ ok: false, error });
18
+ var RANK_MIN = 0;
19
+ var RANK_MAX = 99;
20
+ var RANK_RULE_TEXT = `an integer in ${RANK_MIN}..${RANK_MAX}, 0 = bottom / most primitive`;
21
+
22
+ // src/store/store.ts
23
+ function isRecord(v) {
24
+ return typeof v === "object" && v !== null && !Array.isArray(v);
25
+ }
26
+ function stripBom(text) {
27
+ return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
28
+ }
29
+ var STORE_DIR_NAME = ".mellos";
30
+ var STATE_FILE_RELATIVE_PATH = join(STORE_DIR_NAME, "map.json");
31
+ var PAGES_DIR_NAME = "pages";
32
+ var CONFIG_FILE_NAME = "config.json";
33
+ var CONFIG_FILE_VERSION = 1;
34
+ function configFilePath(defaultFile) {
35
+ return join(dirname(defaultFile), CONFIG_FILE_NAME);
36
+ }
37
+ function userConfigFilePath(userBase) {
38
+ return join(userBase, STORE_DIR_NAME, CONFIG_FILE_NAME);
39
+ }
40
+ var MAPPING_POLICIES = ["always", "complex", "on-request"];
41
+ function makeMappingPolicy(raw) {
42
+ return MAPPING_POLICIES.includes(raw) ? ok(raw) : err({ kind: "invalid-policy", raw, allowed: MAPPING_POLICIES });
43
+ }
44
+ function describeMappingPolicy(policy) {
45
+ switch (policy) {
46
+ case "always":
47
+ return "map every structured task \u2014 workflows, designs, architecture, technical dependencies";
48
+ case "complex":
49
+ return "map only medium or complex tasks \u2014 several modules, a new subsystem, roughly an hour or more";
50
+ case "on-request":
51
+ return "map only when the user explicitly asks";
52
+ }
53
+ }
54
+ function loadMappingPolicy(path) {
55
+ let text;
56
+ try {
57
+ text = readFileSync(path, "utf8");
58
+ } catch (e) {
59
+ if (e.code === "ENOENT") return ok(void 0);
60
+ throw e;
61
+ }
62
+ let raw;
63
+ try {
64
+ raw = JSON.parse(stripBom(text));
65
+ } catch (e) {
66
+ return err({ kind: "malformed-json", path, detail: e.message });
67
+ }
68
+ if (!isRecord(raw)) return err({ kind: "bad-shape", path, detail: "root is not an object" });
69
+ if (raw["version"] !== CONFIG_FILE_VERSION) {
70
+ return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${CONFIG_FILE_VERSION}` });
71
+ }
72
+ const rawPolicy = raw["policy"];
73
+ if (rawPolicy === void 0) return ok(void 0);
74
+ if (typeof rawPolicy !== "string") return err({ kind: "bad-shape", path, detail: "policy is not a string" });
75
+ const policy = makeMappingPolicy(rawPolicy);
76
+ return policy.ok ? ok(policy.value) : err({ kind: "bad-shape", path, detail: `policy is "${rawPolicy}", expected one of: ${MAPPING_POLICIES.join(" | ")}` });
77
+ }
78
+ function effectiveMappingPolicy(projectConfigFile, userConfigFile) {
79
+ const project = loadMappingPolicy(projectConfigFile);
80
+ if (!project.ok) return project;
81
+ const user = loadMappingPolicy(userConfigFile);
82
+ if (!user.ok) return user;
83
+ const effective = project.value ?? user.value;
84
+ const source = project.value !== void 0 ? "project" : user.value !== void 0 ? "user" : void 0;
85
+ return ok({ project: project.value, user: user.value, effective, source });
86
+ }
87
+ var LEGACY_STATE_FILE_RELATIVE_PATH = join(".claude", "mellos-mapping.json");
88
+
89
+ // src/hook/session-start.ts
90
+ function sessionStartContext(input) {
91
+ const { policy, hasStore } = input;
92
+ if (policy === void 0) {
93
+ return [
94
+ "mellos-mapping \u2014 first-run setup. This question is asked ONCE EVER, not once per project.",
95
+ "Before starting structured work here, ask the user how eager mapping should be:",
96
+ ...MAPPING_POLICIES.map((p) => ` ${p} \u2014 ${describeMappingPolicy(p)}`),
97
+ 'Record their answer with mmap_setup {policy: "<their choice>", scope: "user"} \u2014 it then applies',
98
+ 'to every project they open, and a single project can still override it with scope: "project".',
99
+ "Do not choose for them, and do not ask again once it is recorded."
100
+ ].join("\n");
101
+ }
102
+ if (policy === "on-request") {
103
+ return hasStore ? "mellos-mapping: this project has a Mellos map. The user's policy is on-request \u2014 use the mmap tools only when they ask." : void 0;
104
+ }
105
+ const scope = policy === "always" ? "For ANY structured task in this project \u2014 workflows, designs, architecture, technical dependencies:" : "For any MEDIUM OR COMPLEX task in this project \u2014 several modules, a new subsystem, roughly an hour of work or more (skip trivial edits):";
106
+ return [
107
+ `mellos-mapping policy: ${policy} \u2014 ${describeMappingPolicy(policy)}.`,
108
+ scope,
109
+ " 1. Load the mellos-mapping skill and follow it.",
110
+ " 2. Declare the whole ghost design with mmap_declare BEFORE writing code.",
111
+ " 3. Open the map pane WITHOUT asking \u2014 the recorded policy is the user's standing consent:",
112
+ ' mmap_open {page: "<the page this effort lives on>"}',
113
+ " It opens the pane beside this conversation, or retargets one that is already open.",
114
+ " 4. Watch the `pane:` line every write answers with: it says whether anybody is actually",
115
+ " looking. `pane: CLOSED` means the user cannot see this map \u2014 call mmap_open then too.",
116
+ " 5. Keep the map current as the work proceeds: in-progress when a node is started,",
117
+ " done WITH EVIDENCE when its verification passes, regressed when something breaks.",
118
+ "An explicit request from the user always outranks this."
119
+ ].join("\n");
120
+ }
121
+ function hasMap(stateFile) {
122
+ return existsSync2(stateFile) || existsSync2(join2(dirname2(stateFile), PAGES_DIR_NAME));
123
+ }
124
+ function mmapShimFilePath(localAppData) {
125
+ return join2(localAppData, "mellos-mapping", "bin", "mmap.cmd");
126
+ }
127
+ function mmapShimCurrent(shimContent, mmapPath) {
128
+ return shimContent !== void 0 && shimContent.includes(`"${mmapPath}"`);
129
+ }
130
+ function installContextLine(outcome) {
131
+ if (typeof outcome !== "object" || outcome === null) return void 0;
132
+ const o = outcome;
133
+ if (o.kind !== "installed" || typeof o.binDir !== "string") return void 0;
134
+ if (o.path === "updated") {
135
+ return [
136
+ "mellos-mapping: the `mmap` terminal command was just installed for the user",
137
+ `(${o.binDir} was added to their user PATH). Typed in any project terminal, \`mmap\``,
138
+ "toggles the map pane. The PATH change reaches only NEW processes \u2014 and a new tab of",
139
+ "a running Windows Terminal inherits the old environment, so if the user says `mmap`",
140
+ "is not recognized, tell them to close Windows Terminal entirely and reopen it."
141
+ ].join("\n");
142
+ }
143
+ if (o.path === "refused" || o.path === "error") {
144
+ const reason = typeof o.reason === "string" ? o.reason : "the PATH edit failed";
145
+ return [
146
+ `mellos-mapping: the \`mmap\` command's launcher was written to ${o.binDir},`,
147
+ `but the user PATH was NOT changed: ${reason}.`,
148
+ "If the user wants the `mmap` pane-toggle command, tell them to add that directory to",
149
+ 'their user PATH (Settings > "Edit environment variables for your account").'
150
+ ].join("\n");
151
+ }
152
+ return void 0;
153
+ }
154
+ function ensureMmapCommand(pluginRoot) {
155
+ if (process.platform !== "win32") return void 0;
156
+ const localAppData = process.env["LOCALAPPDATA"];
157
+ if (localAppData === void 0 || localAppData === "") return void 0;
158
+ const mmapPath = join2(pluginRoot, "dist", "mmap.mjs");
159
+ let shim;
160
+ try {
161
+ shim = readFileSync2(mmapShimFilePath(localAppData), "utf8");
162
+ } catch {
163
+ shim = void 0;
164
+ }
165
+ if (mmapShimCurrent(shim, mmapPath)) return void 0;
166
+ const run = spawnSync(
167
+ process.execPath,
168
+ [join2(pluginRoot, "scripts", "install-mmap-command.mjs"), "--json"],
169
+ { encoding: "utf8", windowsHide: true, timeout: 15e3 }
170
+ );
171
+ if (run.status !== 0 || typeof run.stdout !== "string") return void 0;
172
+ let outcome;
173
+ try {
174
+ outcome = JSON.parse(run.stdout);
175
+ } catch {
176
+ return void 0;
177
+ }
178
+ return installContextLine(outcome);
179
+ }
180
+ function parseHookInput(raw) {
181
+ let parsed;
182
+ try {
183
+ parsed = JSON.parse(raw);
184
+ } catch {
185
+ return { cwd: void 0 };
186
+ }
187
+ if (typeof parsed !== "object" || parsed === null) return { cwd: void 0 };
188
+ const cwd = parsed.cwd;
189
+ return { cwd: typeof cwd === "string" && cwd !== "" ? cwd : void 0 };
190
+ }
191
+ function hookOutput(additionalContext) {
192
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext } });
193
+ }
194
+ async function readAll(stream) {
195
+ const chunks = [];
196
+ for await (const chunk of stream) chunks.push(Buffer.from(chunk));
197
+ return Buffer.concat(chunks).toString("utf8");
198
+ }
199
+ async function main() {
200
+ const raw = process.stdin.isTTY === true ? "" : await readAll(process.stdin);
201
+ const projectDir = parseHookInput(raw).cwd ?? process.cwd();
202
+ const stateFile = join2(projectDir, STATE_FILE_RELATIVE_PATH);
203
+ const scopes = effectiveMappingPolicy(configFilePath(stateFile), userConfigFilePath(homedir()));
204
+ if (!scopes.ok) return;
205
+ const pluginRoot = dirname2(dirname2(fileURLToPath(import.meta.url)));
206
+ const context = sessionStartContext({
207
+ policy: scopes.value.effective,
208
+ hasStore: hasMap(stateFile)
209
+ });
210
+ let installNote;
211
+ try {
212
+ installNote = ensureMmapCommand(pluginRoot);
213
+ } catch {
214
+ installNote = void 0;
215
+ }
216
+ const parts = [context, installNote].filter((p) => p !== void 0);
217
+ if (parts.length > 0) process.stdout.write(hookOutput(parts.join("\n\n")));
218
+ }
219
+ function launchedAsEntry(argv1, moduleUrl) {
220
+ if (argv1 === void 0) return false;
221
+ try {
222
+ return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
223
+ } catch {
224
+ return pathToFileURL(argv1).href === moduleUrl;
225
+ }
226
+ }
227
+ if (launchedAsEntry(process.argv[1], import.meta.url)) {
228
+ main().catch(() => process.exit(0));
229
+ }
230
+ export {
231
+ hasMap,
232
+ hookOutput,
233
+ installContextLine,
234
+ launchedAsEntry,
235
+ mmapShimCurrent,
236
+ mmapShimFilePath,
237
+ parseHookInput,
238
+ sessionStartContext
239
+ };
package/dist/mmap.mjs ADDED
@@ -0,0 +1,338 @@
1
+ #!/usr/bin/env node
2
+
3
+ // scripts/mmap.mjs
4
+ import { existsSync as existsSync2 } from "node:fs";
5
+ import { dirname as dirname2, join as join2, resolve } from "node:path";
6
+
7
+ // scripts/pane-core.mjs
8
+ import { spawnSync } from "node:child_process";
9
+ import { existsSync, mkdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath, pathToFileURL } from "node:url";
12
+ var WATCHER_BOOLEAN_FLAGS = ["--ascii", "--no-color", "--no-mouse", "--no-follow"];
13
+ var WATCHER_VALUE_FLAGS = ["--interval"];
14
+ var PANE_MODE = { split: "split", window: "window" };
15
+ function takeWatcherFlag(argv, i) {
16
+ const flag = argv[i];
17
+ if (WATCHER_BOOLEAN_FLAGS.includes(flag)) return { kind: "taken", next: i, flags: [flag] };
18
+ if (!WATCHER_VALUE_FLAGS.includes(flag)) return { kind: "other" };
19
+ const value = argv[i + 1];
20
+ if (value === void 0 || !Number.isFinite(Number(value))) return { kind: "bad-value", message: `${flag} needs a number` };
21
+ return { kind: "taken", next: i + 1, flags: [flag, value] };
22
+ }
23
+ function pluginRootOf(moduleUrl) {
24
+ return dirname(dirname(fileURLToPath(moduleUrl)));
25
+ }
26
+ async function loadPluginPaths(pluginRoot) {
27
+ const pathsModule = join(pluginRoot, "dist", "store-paths.mjs");
28
+ const watchPath = join(pluginRoot, "dist", "watch.mjs");
29
+ const missing = !existsSync(pathsModule) ? pathsModule : !existsSync(watchPath) ? watchPath : void 0;
30
+ if (missing !== void 0) {
31
+ return { ok: false, error: `the plugin is not built \u2014 run "npm run build" (missing ${missing})` };
32
+ }
33
+ return { ok: true, value: { store: await import(pathToFileURL(pathsModule).href), watchPath } };
34
+ }
35
+ function launchedAsEntry(argv1, moduleUrl) {
36
+ if (argv1 === void 0) return false;
37
+ try {
38
+ return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+ var ORPHANED_FOCUS_FILE_NAME = "mellos-mapping.focus";
44
+ function writeRequest(path, body) {
45
+ mkdirSync(dirname(path), { recursive: true });
46
+ writeFileSync(`${path}.tmp`, JSON.stringify(body));
47
+ renameSync(`${path}.tmp`, path);
48
+ }
49
+ function writeFocusRequest(focusFile, pageSlug) {
50
+ writeRequest(focusFile, { page: pageSlug });
51
+ try {
52
+ rmSync(join(dirname(focusFile), ORPHANED_FOCUS_FILE_NAME), { force: true });
53
+ } catch {
54
+ }
55
+ }
56
+ function writeQuitRequest(quitFile) {
57
+ writeRequest(quitFile, {});
58
+ }
59
+ function paneCommand(cfg, watchPath, mapFile) {
60
+ const cmd = ["--title", "mellos map", "-d", cfg.projectDir, "node", watchPath, "--file", mapFile, ...cfg.watcherFlags];
61
+ if (cfg.pageSlug !== void 0) cmd.push("--page", cfg.pageSlug);
62
+ return cmd;
63
+ }
64
+ function powerShellQuote(value) {
65
+ return `'${value.replace(/'/g, "''")}'`;
66
+ }
67
+ function watcherProbeScript(mapFile) {
68
+ return `$ErrorActionPreference = 'SilentlyContinue'
69
+ $needle = ${powerShellQuote(mapFile)}
70
+ $w = @(Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object {
71
+ $_.CommandLine -and
72
+ $_.CommandLine.IndexOf('watch.mjs', [StringComparison]::OrdinalIgnoreCase) -ge 0 -and
73
+ $_.CommandLine.IndexOf($needle, [StringComparison]::OrdinalIgnoreCase) -ge 0
74
+ })
75
+ Write-Output "WATCHERS=$($w.Count)"`;
76
+ }
77
+ var IDENTIFY_AND_FOCUS = String.raw`
78
+ $ErrorActionPreference = 'SilentlyContinue'
79
+ Add-Type @"
80
+ using System;
81
+ using System.Text;
82
+ using System.Runtime.InteropServices;
83
+ public class MmapWin {
84
+ [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc cb, IntPtr lp);
85
+ public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lp);
86
+ [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
87
+ [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetClassName(IntPtr hWnd, StringBuilder sb, int max);
88
+ [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowText(IntPtr hWnd, StringBuilder sb, int max);
89
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
90
+ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
91
+ [DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr hWnd);
92
+ [DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool f);
93
+ [DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
94
+ [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
95
+ [DllImport("user32.dll")] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, IntPtr dwExtraInfo);
96
+ [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
97
+ [DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd);
98
+ [DllImport("kernel32.dll")] public static extern bool FreeConsole();
99
+ [DllImport("kernel32.dll")] public static extern bool AttachConsole(uint pid);
100
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode)] public static extern bool SetConsoleTitle(string title);
101
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode)] public static extern uint GetConsoleTitle(StringBuilder sb, uint size);
102
+ }
103
+ "@
104
+ function Get-WtWindows {
105
+ $wins = New-Object System.Collections.ArrayList
106
+ $cb = {
107
+ param($h, $lp)
108
+ if ([MmapWin]::IsWindowVisible($h)) {
109
+ $cls = New-Object System.Text.StringBuilder 256
110
+ [void][MmapWin]::GetClassName($h, $cls, 256)
111
+ if ($cls.ToString() -eq 'CASCADIA_HOSTING_WINDOW_CLASS') {
112
+ $t = New-Object System.Text.StringBuilder 512
113
+ [void][MmapWin]::GetWindowText($h, $t, 512)
114
+ [void]$wins.Add(@{ hwnd = $h.ToInt64(); title = $t.ToString() })
115
+ }
116
+ }
117
+ return $true
118
+ }
119
+ [void][MmapWin]::EnumWindows($cb, [IntPtr]::Zero)
120
+ return ,$wins
121
+ }
122
+
123
+ $ancestors = @()
124
+ $p = $PID
125
+ for ($i = 0; $i -lt 12 -and $p; $i++) {
126
+ $proc = Get-CimInstance Win32_Process -Filter "ProcessId=$p"
127
+ if (-not $proc) { break }
128
+ if ($i -gt 0) { $ancestors += [uint32]$proc.ProcessId }
129
+ $p = $proc.ParentProcessId
130
+ }
131
+
132
+ # The agent CLI (claude/codex/...) is some ancestor holding the console that a
133
+ # WT window renders; hidden-console ancestors just never light a window up.
134
+ $nonce = "__NONCE__"
135
+ $hwnd = [IntPtr]::Zero
136
+ foreach ($apid in $ancestors) {
137
+ [void][MmapWin]::FreeConsole()
138
+ if (-not [MmapWin]::AttachConsole($apid)) { continue }
139
+ $sb = New-Object System.Text.StringBuilder 1024
140
+ [void][MmapWin]::GetConsoleTitle($sb, 1024)
141
+ $orig = $sb.ToString()
142
+ for ($i = 0; $i -lt 6 -and $hwnd -eq [IntPtr]::Zero; $i++) {
143
+ [void][MmapWin]::SetConsoleTitle($nonce)
144
+ Start-Sleep -Milliseconds 60
145
+ foreach ($w in (Get-WtWindows)) {
146
+ if ($w.title -like "*$nonce*") { $hwnd = [IntPtr]$w.hwnd; break }
147
+ }
148
+ }
149
+ Start-Sleep -Milliseconds 100
150
+ [void][MmapWin]::SetConsoleTitle($orig)
151
+ Start-Sleep -Milliseconds 200
152
+ [void][MmapWin]::FreeConsole()
153
+ if ($hwnd -ne [IntPtr]::Zero) { break }
154
+ }
155
+
156
+ if ($hwnd -eq [IntPtr]::Zero) { Write-Output 'IDENT=0'; exit 0 }
157
+ Write-Output "IDENT=$($hwnd.ToInt64())"
158
+
159
+ if ([MmapWin]::IsIconic($hwnd)) { [void][MmapWin]::ShowWindow($hwnd, 9) }
160
+ $focused = $false
161
+ [void][MmapWin]::SetForegroundWindow($hwnd)
162
+ Start-Sleep -Milliseconds 150
163
+ if ([MmapWin]::GetForegroundWindow() -eq $hwnd) { $focused = $true }
164
+ if (-not $focused) {
165
+ [MmapWin]::keybd_event(0x12, 0, 0, [IntPtr]::Zero)
166
+ [void][MmapWin]::SetForegroundWindow($hwnd)
167
+ [MmapWin]::keybd_event(0x12, 0, 2, [IntPtr]::Zero)
168
+ Start-Sleep -Milliseconds 150
169
+ if ([MmapWin]::GetForegroundWindow() -eq $hwnd) { $focused = $true }
170
+ }
171
+ if (-not $focused) {
172
+ $fgpid = 0
173
+ $fgThread = [MmapWin]::GetWindowThreadProcessId([MmapWin]::GetForegroundWindow(), [ref]$fgpid)
174
+ $myThread = [MmapWin]::GetCurrentThreadId()
175
+ [void][MmapWin]::AttachThreadInput($myThread, $fgThread, $true)
176
+ [void][MmapWin]::BringWindowToTop($hwnd)
177
+ [void][MmapWin]::SetForegroundWindow($hwnd)
178
+ [void][MmapWin]::AttachThreadInput($myThread, $fgThread, $false)
179
+ Start-Sleep -Milliseconds 150
180
+ if ([MmapWin]::GetForegroundWindow() -eq $hwnd) { $focused = $true }
181
+ }
182
+ Write-Output "FOCUS=$(if ($focused) { 1 } else { 0 })"
183
+ `;
184
+ var PROBE_TIMEOUT_MS = 3e4;
185
+ var WT_TIMEOUT_MS = 15e3;
186
+ var SPLIT_SIZE = "0.42";
187
+ var DEDICATED_WINDOW_NAME = "mellos-mapping";
188
+ function runPowerShell(script) {
189
+ const encoded = Buffer.from(script, "utf16le").toString("base64");
190
+ const r = spawnSync(
191
+ "powershell.exe",
192
+ ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded],
193
+ { encoding: "utf8", timeout: PROBE_TIMEOUT_MS, windowsHide: true }
194
+ );
195
+ return r.stdout ?? "";
196
+ }
197
+ function paneIsOpen(store, mapFile) {
198
+ if (store.readLiveViewers(mapFile, Date.now()).length > 0) return true;
199
+ if (process.platform !== "win32") return false;
200
+ const m = runPowerShell(watcherProbeScript(mapFile)).match(/WATCHERS=(\d+)/);
201
+ return m !== null && Number(m[1]) > 0;
202
+ }
203
+ function openWt(args, what) {
204
+ const r = spawnSync("wt", args, { stdio: "ignore", timeout: WT_TIMEOUT_MS, windowsHide: true });
205
+ return r.status === 0 ? { ok: true, value: void 0 } : { ok: false, error: `wt failed to ${what} (exit ${r.status ?? "timeout"}) \u2014 is Windows Terminal installed?` };
206
+ }
207
+ function placePane(cfg, watchPath, mapFile) {
208
+ const dedicated = (reason) => {
209
+ const opened = openWt(["-w", DEDICATED_WINDOW_NAME, "nt", ...paneCommand(cfg, watchPath, mapFile)], "open the dedicated window");
210
+ return opened.ok ? { ok: true, value: { mode: PANE_MODE.window, reason } } : opened;
211
+ };
212
+ if (cfg.mode === PANE_MODE.window) return dedicated("requested");
213
+ const nonce = `MMAP-NONCE-${process.pid}`;
214
+ const out = runPowerShell(IDENTIFY_AND_FOCUS.replaceAll("__NONCE__", nonce));
215
+ const ident = out.match(/IDENT=(\d+)/)?.[1] ?? "0";
216
+ if (ident === "0") return dedicated("session-window-not-identified");
217
+ if (!/FOCUS=1/.test(out)) return dedicated("session-window-focus-denied");
218
+ const split = openWt(
219
+ ["-w", "0", "sp", "-V", "--size", SPLIT_SIZE, ...paneCommand(cfg, watchPath, mapFile)],
220
+ "split the session window"
221
+ );
222
+ return split.ok ? { ok: true, value: { mode: PANE_MODE.split, hwnd: ident } } : split;
223
+ }
224
+
225
+ // scripts/mmap.mjs
226
+ var USAGE = "usage: mmap [<page-slug>] [--window] [--force] [--ascii] [--no-color] [--no-mouse] [--no-follow] [--interval <ms>]\n bare `mmap` toggles: it opens the map pane for this project, or closes the open one.";
227
+ function parseMmapArgs(argv, idRule) {
228
+ const positional = [];
229
+ const watcherFlags = [];
230
+ let mode = PANE_MODE.split;
231
+ let force = false;
232
+ for (let i = 0; i < argv.length; i++) {
233
+ const a = argv[i];
234
+ const watcher = takeWatcherFlag(argv, i);
235
+ if (watcher.kind === "bad-value") return { ok: false, error: `${watcher.message}
236
+ ${USAGE}` };
237
+ if (watcher.kind === "taken") {
238
+ watcherFlags.push(...watcher.flags);
239
+ i = watcher.next;
240
+ } else if (a === "--window") {
241
+ mode = PANE_MODE.window;
242
+ } else if (a === "--force") {
243
+ force = true;
244
+ } else if (a === "--page") {
245
+ return { ok: false, error: `mmap takes the page slug as its argument, not --page
246
+ ${USAGE}` };
247
+ } else if (a.startsWith("--")) {
248
+ return { ok: false, error: `unknown flag "${a}"
249
+ ${USAGE}` };
250
+ } else {
251
+ positional.push(a);
252
+ }
253
+ }
254
+ if (positional.length > 1) return { ok: false, error: `mmap takes at most one page slug
255
+ ${USAGE}` };
256
+ const pageSlug = positional[0];
257
+ if (pageSlug !== void 0 && !idRule.test(pageSlug)) {
258
+ return { ok: false, error: `a page is a kebab-case slug (got "${pageSlug}")
259
+ ${USAGE}` };
260
+ }
261
+ return { ok: true, value: { pageSlug, mode, force, watcherFlags } };
262
+ }
263
+ function storeSearchPath(startDir) {
264
+ const dirs = [];
265
+ let dir = resolve(startDir);
266
+ for (; ; ) {
267
+ dirs.push(dir);
268
+ const parent = dirname2(dir);
269
+ if (parent === dir) return dirs;
270
+ dir = parent;
271
+ }
272
+ }
273
+ function storeMarkerOf(storeRelativePath) {
274
+ return storeRelativePath.split(/[\\/]/)[0];
275
+ }
276
+ function nearestProject(candidates, holdsStore) {
277
+ const at = holdsStore.indexOf(true);
278
+ return at >= 0 ? { root: candidates[at], found: true } : { root: candidates[0], found: false };
279
+ }
280
+ function toggleAction(running, pageSlug, force = false) {
281
+ if (!running || force) return { kind: "open" };
282
+ return pageSlug === void 0 ? { kind: "quit" } : { kind: "focus", page: pageSlug };
283
+ }
284
+ async function main() {
285
+ const loaded = await loadPluginPaths(pluginRootOf(import.meta.url));
286
+ if (!loaded.ok) {
287
+ console.error(loaded.error);
288
+ process.exit(1);
289
+ }
290
+ const { store, watchPath } = loaded.value;
291
+ const parsed = parseMmapArgs(process.argv.slice(2), store.ID_RULE);
292
+ if (!parsed.ok) {
293
+ console.error(parsed.error);
294
+ process.exit(1);
295
+ }
296
+ if (process.platform !== "win32") {
297
+ console.error("mmap opens the pane through Windows Terminal \u2014 on other platforms run the watcher yourself:");
298
+ console.error(` node "${watchPath}" --file "<project>/${store.STATE_FILE_RELATIVE_PATH}"`);
299
+ process.exit(1);
300
+ }
301
+ const candidates = storeSearchPath(process.cwd());
302
+ const marker = storeMarkerOf(store.STATE_FILE_RELATIVE_PATH);
303
+ const project = nearestProject(candidates, candidates.map((dir) => existsSync2(join2(dir, marker))));
304
+ const cfg = { ...parsed.value, projectDir: project.root };
305
+ const mapFile = join2(project.root, store.STATE_FILE_RELATIVE_PATH);
306
+ const action = toggleAction(paneIsOpen(store, mapFile), cfg.pageSlug, cfg.force);
307
+ if (action.kind === "quit") {
308
+ writeQuitRequest(store.quitFilePath(mapFile));
309
+ console.log(`Closing the map pane for ${project.root}.`);
310
+ return;
311
+ }
312
+ if (action.kind === "focus") {
313
+ writeFocusRequest(store.focusFilePath(mapFile), action.page);
314
+ console.log(`The map pane for ${project.root} is already open \u2014 showing page "${action.page}".`);
315
+ return;
316
+ }
317
+ const placed = placePane(cfg, watchPath, mapFile);
318
+ if (!placed.ok) {
319
+ console.error(placed.error);
320
+ process.exit(1);
321
+ }
322
+ const where = placed.value.mode === PANE_MODE.window ? `in the dedicated "${DEDICATED_WINDOW_NAME}" window (${placed.value.reason})` : "beside this terminal (vertical split)";
323
+ console.log(`Map opened for ${project.root} ${where}.`);
324
+ if (!project.found) {
325
+ console.log(
326
+ "This project has no map yet, so the pane will sit on its standby screen until the assistant declares one."
327
+ );
328
+ }
329
+ }
330
+ if (launchedAsEntry(process.argv[1], import.meta.url)) await main();
331
+ export {
332
+ USAGE,
333
+ nearestProject,
334
+ parseMmapArgs,
335
+ storeMarkerOf,
336
+ storeSearchPath,
337
+ toggleAction
338
+ };