mellos-mapping 0.20.3 → 0.23.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.
Files changed (75) hide show
  1. package/README.md +130 -69
  2. package/README.zh-CN.md +126 -65
  3. package/dist/hook-session-start.mjs +28 -20
  4. package/dist/mmap.mjs +303 -171
  5. package/dist/preview.mjs +1457 -0
  6. package/dist/server.mjs +1984 -741
  7. package/dist/store-paths.mjs +69 -22
  8. package/dist/terminal-worker.mjs +3331 -0
  9. package/dist/watch.mjs +957 -571
  10. package/dist/web/TERMINAL-LICENSES.txt +70 -0
  11. package/dist/web/app.css +967 -0
  12. package/dist/web/app.js +1356 -0
  13. package/dist/web/index.html +9 -0
  14. package/dist/web/terminal.css +9 -0
  15. package/dist/web/terminal.html +7 -0
  16. package/dist/web/terminal.js +9293 -0
  17. package/dist/web/xterm.css +285 -0
  18. package/dist/web.mjs +5646 -0
  19. package/docs/codex.md +189 -0
  20. package/docs/map-api.md +148 -0
  21. package/lib/domain/context.d.ts +11 -0
  22. package/lib/domain/context.js +27 -0
  23. package/lib/domain/text.d.ts +9 -0
  24. package/lib/domain/text.js +54 -0
  25. package/lib/domain/types.d.ts +3 -0
  26. package/lib/preview/index.d.ts +3 -0
  27. package/lib/preview/index.js +3 -0
  28. package/lib/preview/markdown.d.ts +8 -0
  29. package/lib/preview/markdown.js +54 -0
  30. package/lib/preview/presentation.d.ts +6 -0
  31. package/lib/preview/presentation.js +14 -0
  32. package/lib/preview/publisher.d.ts +23 -0
  33. package/lib/preview/publisher.js +143 -0
  34. package/lib/preview/svg.d.ts +3 -0
  35. package/lib/preview/svg.js +74 -0
  36. package/lib/preview/text.d.ts +4 -0
  37. package/lib/preview/text.js +13 -0
  38. package/lib/render/canvas.d.ts +1 -1
  39. package/lib/render/canvas.js +4 -2
  40. package/lib/render/draw.js +9 -6
  41. package/lib/render/render.d.ts +6 -0
  42. package/lib/render/render.js +50 -18
  43. package/lib/render/width.js +3 -1
  44. package/lib/store/atomic.d.ts +18 -0
  45. package/lib/store/atomic.js +86 -0
  46. package/lib/store/channels.d.ts +40 -0
  47. package/lib/store/channels.js +135 -0
  48. package/lib/store/format.js +21 -4
  49. package/lib/store/json-text.d.ts +9 -0
  50. package/lib/store/json-text.js +16 -0
  51. package/lib/store/maps.d.ts +12 -0
  52. package/lib/store/maps.js +42 -0
  53. package/lib/store/migration.d.ts +12 -0
  54. package/lib/store/migration.js +46 -0
  55. package/lib/store/pages.d.ts +46 -0
  56. package/lib/store/pages.js +89 -0
  57. package/lib/store/policy.d.ts +74 -0
  58. package/lib/store/policy.js +144 -0
  59. package/lib/store/project.d.ts +2 -0
  60. package/lib/store/project.js +29 -0
  61. package/lib/store/store.d.ts +15 -257
  62. package/lib/store/store.js +16 -695
  63. package/lib/store/transaction.d.ts +12 -0
  64. package/lib/store/transaction.js +91 -0
  65. package/lib/store/viewers.d.ts +81 -0
  66. package/lib/store/viewers.js +186 -0
  67. package/package.json +27 -5
  68. package/scripts/codex-cli.mjs +42 -0
  69. package/scripts/codex-register.mjs +27 -94
  70. package/scripts/mmap.mjs +27 -17
  71. package/scripts/open-pane.mjs +37 -18
  72. package/scripts/pane-core.mjs +57 -220
  73. package/scripts/terminal-session.mjs +137 -0
  74. package/scripts/tmux-session.mjs +90 -0
  75. package/scripts/watcher-command.mjs +16 -0
package/dist/watch.mjs CHANGED
@@ -2,9 +2,8 @@
2
2
  import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
3
3
 
4
4
  // src/watch/watch.ts
5
- import { realpathSync, statSync as statSync2 } from "node:fs";
6
- import { dirname as dirname2, join as join2 } from "node:path";
7
- import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
6
+ import { dirname as dirname9, join as join8 } from "node:path";
8
7
 
9
8
  // src/domain/types.ts
10
9
  var ok = (value) => ({ ok: true, value });
@@ -235,6 +234,392 @@ function updateNode(map, input) {
235
234
  return ok({ ...map, nodes: map.nodes.map((n) => n.id === input.id ? updated : n) });
236
235
  }
237
236
 
237
+ // src/store/project.ts
238
+ import { existsSync, realpathSync } from "node:fs";
239
+ import { dirname, join, resolve } from "node:path";
240
+ import { homedir, tmpdir } from "node:os";
241
+ function resolveProjectDirectory(cwd, stopAt = [homedir(), tmpdir()]) {
242
+ let start = resolve(cwd);
243
+ try {
244
+ start = realpathSync(start);
245
+ } catch {
246
+ }
247
+ let dir = start;
248
+ const boundaries = new Set(stopAt.map((path) => {
249
+ try {
250
+ return realpathSync(path);
251
+ } catch {
252
+ return resolve(path);
253
+ }
254
+ }));
255
+ while (true) {
256
+ if (dir !== start && boundaries.has(dir)) return start;
257
+ if (existsSync(join(dir, ".git")) || existsSync(join(dir, ".mellos", "map.json")) || existsSync(join(dir, ".mellos", "pages"))) return dir;
258
+ const parent = dirname(dir);
259
+ if (parent === dir) return start;
260
+ dir = parent;
261
+ }
262
+ }
263
+
264
+ // src/store/transaction.ts
265
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
266
+ import { dirname as dirname2, join as join2 } from "node:path";
267
+ import { randomUUID, createHash } from "node:crypto";
268
+
269
+ // src/domain/context.ts
270
+ function sourceError(raw) {
271
+ if (!Array.isArray(raw) || raw.length > 100) return "sources must be an array of at most 100 file references";
272
+ for (const item of raw) {
273
+ if (!item || typeof item !== "object" || Array.isArray(item)) return "source must be an object";
274
+ const s = item;
275
+ if (Object.keys(s).some((k) => k !== "path" && k !== "sha256")) return "unknown source field";
276
+ if (typeof s.path !== "string" || s.path.length > 1024 || !s.path || /[\u0000-\u001f\u007f-\u009f\\:]/.test(s.path) || s.path.startsWith("/") || s.path.split("/").some((p) => !p || p === "." || p === "..")) return "source path must be relative to the project, with forward slashes and no traversal";
277
+ if (s.sha256 !== void 0 && (typeof s.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(s.sha256))) return "source sha256 must be a lowercase SHA256 hash";
278
+ }
279
+ return void 0;
280
+ }
281
+ function contextError(raw) {
282
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return "context must be an object";
283
+ for (const [key, value] of Object.entries(raw)) {
284
+ if (key !== "summary" && key !== "next") return "unknown context field";
285
+ if (typeof value !== "string" || value.length > 2e3 || /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/.test(value)) return "context fields must be text of at most 2000 characters";
286
+ }
287
+ return void 0;
288
+ }
289
+
290
+ // src/domain/text.ts
291
+ var NO_CONTROLS = /^[^\u0000-\u001f\u007f-\u009f]*$/;
292
+ var NO_CONTROLS_TEXT = "one line of text; control characters (ESC, newline, tab) are not allowed";
293
+ var NO_CONTROLS_BUT_BREAKS = /^[^\u0000-\u0008\u000b-\u001f\u007f-\u009f]*$/;
294
+ var NO_CONTROLS_BUT_BREAKS_TEXT = "text with optional newlines (\\n) and tabs; other control characters (ESC, BEL, CR) are not allowed";
295
+ function mapTextError(map) {
296
+ if (map.context !== void 0) {
297
+ const error2 = contextError(map.context);
298
+ if (error2) return error2;
299
+ }
300
+ const check = (field, value, multiline = false) => value === void 0 || (multiline ? NO_CONTROLS_BUT_BREAKS : NO_CONTROLS).test(value) ? void 0 : `${field}: ${multiline ? NO_CONTROLS_BUT_BREAKS_TEXT : NO_CONTROLS_TEXT}`;
301
+ let error = check("title", map.title);
302
+ if (error) return error;
303
+ for (const [i, layer] of map.layers.entries()) {
304
+ error = check(`layers[${i}].name`, layer.name);
305
+ if (error) return error;
306
+ }
307
+ for (const name of ["lanes", "groups"]) {
308
+ for (const [i, item] of map[name].entries()) {
309
+ error = check(`${name}[${i}].label`, item.label);
310
+ if (error) return error;
311
+ }
312
+ }
313
+ for (const [i, node] of map.nodes.entries()) {
314
+ if (node.sources !== void 0) {
315
+ const error2 = sourceError(node.sources);
316
+ if (error2) return `nodes[${i}]: ${error2}`;
317
+ }
318
+ for (const name of ["label", "evidence", "detail"]) {
319
+ error = check(`nodes[${i}].${name}`, node[name], name !== "label");
320
+ if (error) return error;
321
+ }
322
+ }
323
+ for (const [i, edge] of map.edges.entries()) {
324
+ error = check(`edges[${i}].label`, edge.label);
325
+ if (error) return error;
326
+ }
327
+ return void 0;
328
+ }
329
+ function terminalText(text, multiline = false) {
330
+ return text.replace(multiline ? /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g : /[\u0000-\u001f\u007f-\u009f]/g, "?");
331
+ }
332
+
333
+ // src/store/format.ts
334
+ var STATE_FILE_VERSION = 1;
335
+ function makePageId(raw) {
336
+ return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
337
+ }
338
+ function describeStoreError(e) {
339
+ switch (e.kind) {
340
+ case "not-found":
341
+ return `no map file at ${e.path}`;
342
+ case "malformed-json":
343
+ return `map file ${e.path} is not valid JSON: ${e.detail}`;
344
+ case "bad-shape":
345
+ return `map file ${e.path} has an unexpected shape: ${e.detail}`;
346
+ case "invariant-violation":
347
+ return `map file ${e.path} violates a structural invariant: ${describeMapError(e.violation)}`;
348
+ case "save-failed":
349
+ return `could not write ${e.path}: ${e.detail}`;
350
+ case "delete-failed":
351
+ return `could not delete ${e.path}: ${e.detail}`;
352
+ }
353
+ }
354
+ function isRecord(v) {
355
+ return typeof v === "object" && v !== null && !Array.isArray(v);
356
+ }
357
+ function describeValue(v) {
358
+ if (v === void 0) return "missing";
359
+ if (v === null) return "null";
360
+ if (Array.isArray(v)) return "an array";
361
+ return `a ${typeof v}`;
362
+ }
363
+ function badShape(path, where, expected, got) {
364
+ return err({ kind: "bad-shape", path, detail: `${where} is ${describeValue(got)}, expected ${expected}` });
365
+ }
366
+ function arrayField(raw, key, path, presence) {
367
+ const v = raw[key];
368
+ if (Array.isArray(v)) return ok(v);
369
+ if (v === void 0 && presence === "optional") return ok([]);
370
+ return badShape(path, `"${key}"`, "an array", v);
371
+ }
372
+ function requiredString(rec, key, where, path) {
373
+ const v = rec[key];
374
+ return typeof v === "string" ? ok(v) : badShape(path, `${where}.${key}`, "a string", v);
375
+ }
376
+ function optionalString(rec, key, where, path) {
377
+ const v = rec[key];
378
+ if (v === void 0) return ok(void 0);
379
+ return typeof v === "string" ? ok(v) : badShape(path, `${where}.${key}`, "a string", v);
380
+ }
381
+ function parseMap(raw, path) {
382
+ if (!isRecord(raw)) return err({ kind: "bad-shape", path, detail: "root is not an object" });
383
+ if (raw["version"] !== STATE_FILE_VERSION && raw["version"] !== 2) {
384
+ return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${STATE_FILE_VERSION} or 2` });
385
+ }
386
+ const layers = arrayField(raw, "layers", path, "required");
387
+ if (!layers.ok) return layers;
388
+ const nodes = arrayField(raw, "nodes", path, "required");
389
+ if (!nodes.ok) return nodes;
390
+ const edges = arrayField(raw, "edges", path, "required");
391
+ if (!edges.ok) return edges;
392
+ const lanes = arrayField(raw, "lanes", path, "optional");
393
+ if (!lanes.ok) return lanes;
394
+ const groups = arrayField(raw, "groups", path, "optional");
395
+ if (!groups.ok) return groups;
396
+ let map = EMPTY_MAP;
397
+ if (raw["context"] !== void 0) {
398
+ const error = contextError(raw["context"]);
399
+ if (error) return err({ kind: "bad-shape", path, detail: error });
400
+ map = { ...map, context: raw["context"] };
401
+ }
402
+ const title = optionalString(raw, "title", "map", path);
403
+ if (!title.ok) return title;
404
+ if (title.value !== void 0) map = setTitle(map, title.value);
405
+ const rawKind = optionalString(raw, "kind", "map", path);
406
+ if (!rawKind.ok) return rawKind;
407
+ if (rawKind.value !== void 0) {
408
+ const kind = makeMapKind(rawKind.value);
409
+ if (!kind.ok) return err({ kind: "invariant-violation", path, violation: kind.error });
410
+ map = setKind(map, kind.value);
411
+ }
412
+ for (const [i, rawLayer] of layers.value.entries()) {
413
+ const where = `layers[${i}]`;
414
+ if (!isRecord(rawLayer)) return badShape(path, where, "an object", rawLayer);
415
+ const rawId = requiredString(rawLayer, "id", where, path);
416
+ if (!rawId.ok) return rawId;
417
+ const id = makeLayerId(rawId.value);
418
+ if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
419
+ const name = requiredString(rawLayer, "name", where, path);
420
+ if (!name.ok) return name;
421
+ const rawRank = rawLayer["rank"];
422
+ if (typeof rawRank !== "number") return badShape(path, `${where}.rank`, "a number", rawRank);
423
+ const rank = makeRank(rawRank);
424
+ if (!rank.ok) return err({ kind: "invariant-violation", path, violation: rank.error });
425
+ const next = declareLayer(map, { id: id.value, name: name.value, rank: rank.value });
426
+ if (!next.ok) return err({ kind: "invariant-violation", path, violation: next.error });
427
+ map = next.value;
428
+ }
429
+ for (const [i, rawLane] of lanes.value.entries()) {
430
+ const where = `lanes[${i}]`;
431
+ if (!isRecord(rawLane)) return badShape(path, where, "an object", rawLane);
432
+ const rawId = requiredString(rawLane, "id", where, path);
433
+ if (!rawId.ok) return rawId;
434
+ const id = makeLaneId(rawId.value);
435
+ if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
436
+ const label = requiredString(rawLane, "label", where, path);
437
+ if (!label.ok) return label;
438
+ const declared = declareLane(map, { id: id.value, label: label.value });
439
+ if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
440
+ map = declared.value;
441
+ }
442
+ for (const [i, rawGroup] of groups.value.entries()) {
443
+ const where = `groups[${i}]`;
444
+ if (!isRecord(rawGroup)) return badShape(path, where, "an object", rawGroup);
445
+ const rawId = requiredString(rawGroup, "id", where, path);
446
+ if (!rawId.ok) return rawId;
447
+ const id = makeGroupId(rawId.value);
448
+ if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
449
+ const rawLayer = requiredString(rawGroup, "layer", where, path);
450
+ if (!rawLayer.ok) return rawLayer;
451
+ const layer = makeLayerId(rawLayer.value);
452
+ if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
453
+ const label = requiredString(rawGroup, "label", where, path);
454
+ if (!label.ok) return label;
455
+ const declared = declareGroup(map, { id: id.value, label: label.value, layer: layer.value });
456
+ if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
457
+ map = declared.value;
458
+ }
459
+ for (const [i, rawNode] of nodes.value.entries()) {
460
+ const where = `nodes[${i}]`;
461
+ if (!isRecord(rawNode)) return badShape(path, where, "an object", rawNode);
462
+ const rawId = requiredString(rawNode, "id", where, path);
463
+ if (!rawId.ok) return rawId;
464
+ const id = makeNodeId(rawId.value);
465
+ if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
466
+ const rawLayer = requiredString(rawNode, "layer", where, path);
467
+ if (!rawLayer.ok) return rawLayer;
468
+ const layer = makeLayerId(rawLayer.value);
469
+ if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
470
+ const rawStatus = requiredString(rawNode, "status", where, path);
471
+ if (!rawStatus.ok) return rawStatus;
472
+ const status = makeNodeStatus(rawStatus.value);
473
+ if (!status.ok) return err({ kind: "invariant-violation", path, violation: status.error });
474
+ const label = requiredString(rawNode, "label", where, path);
475
+ if (!label.ok) return label;
476
+ const detail = optionalString(rawNode, "detail", where, path);
477
+ if (!detail.ok) return detail;
478
+ const rawGroup = optionalString(rawNode, "group", where, path);
479
+ if (!rawGroup.ok) return rawGroup;
480
+ let group;
481
+ if (rawGroup.value !== void 0) {
482
+ const made = makeGroupId(rawGroup.value);
483
+ if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
484
+ group = made.value;
485
+ }
486
+ const rawNodeKind = optionalString(rawNode, "kind", where, path);
487
+ if (!rawNodeKind.ok) return rawNodeKind;
488
+ let nodeKind;
489
+ if (rawNodeKind.value !== void 0) {
490
+ const made = makeNodeKind(rawNodeKind.value);
491
+ if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
492
+ nodeKind = made.value;
493
+ }
494
+ const rawLane = optionalString(rawNode, "lane", where, path);
495
+ if (!rawLane.ok) return rawLane;
496
+ let lane;
497
+ if (rawLane.value !== void 0) {
498
+ const made = makeLaneId(rawLane.value);
499
+ if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
500
+ lane = made.value;
501
+ }
502
+ const rawSubmap = optionalString(rawNode, "submap", where, path);
503
+ if (!rawSubmap.ok) return rawSubmap;
504
+ let submap;
505
+ if (rawSubmap.value !== void 0) {
506
+ const made = makeSubmapRef(rawSubmap.value);
507
+ if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
508
+ submap = made.value;
509
+ }
510
+ const declared = declareNode(map, {
511
+ id: id.value,
512
+ label: label.value,
513
+ layer: layer.value,
514
+ status: status.value,
515
+ ...detail.value !== void 0 ? { detail: detail.value } : {},
516
+ ...group !== void 0 ? { group } : {},
517
+ ...nodeKind !== void 0 ? { kind: nodeKind } : {},
518
+ ...lane !== void 0 ? { lane } : {},
519
+ ...submap !== void 0 ? { submap } : {}
520
+ });
521
+ if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
522
+ map = declared.value;
523
+ const evidence = optionalString(rawNode, "evidence", where, path);
524
+ if (!evidence.ok) return evidence;
525
+ if (evidence.value !== void 0) {
526
+ const updated = updateNode(map, { id: id.value, evidence: evidence.value });
527
+ if (!updated.ok) return err({ kind: "invariant-violation", path, violation: updated.error });
528
+ map = updated.value;
529
+ }
530
+ if (rawNode["sources"] !== void 0) {
531
+ const error = sourceError(rawNode["sources"]);
532
+ if (error) return err({ kind: "bad-shape", path, detail: `${where}: ${error}` });
533
+ map = { ...map, nodes: map.nodes.map((n) => n.id === id.value ? { ...n, sources: rawNode["sources"] } : n) };
534
+ }
535
+ }
536
+ for (const [i, rawEdge] of edges.value.entries()) {
537
+ const where = `edges[${i}]`;
538
+ if (!isRecord(rawEdge)) return badShape(path, where, "an object", rawEdge);
539
+ const rawFrom = requiredString(rawEdge, "from", where, path);
540
+ if (!rawFrom.ok) return rawFrom;
541
+ const from = makeNodeId(rawFrom.value);
542
+ if (!from.ok) return err({ kind: "invariant-violation", path, violation: from.error });
543
+ const rawTo = requiredString(rawEdge, "to", where, path);
544
+ if (!rawTo.ok) return rawTo;
545
+ const to = makeNodeId(rawTo.value);
546
+ if (!to.ok) return err({ kind: "invariant-violation", path, violation: to.error });
547
+ const label = optionalString(rawEdge, "label", where, path);
548
+ if (!label.ok) return label;
549
+ const linked = linkNodes(map, from.value, to.value, label.value);
550
+ if (!linked.ok) return err({ kind: "invariant-violation", path, violation: linked.error });
551
+ map = linked.value;
552
+ }
553
+ const textError = mapTextError(map);
554
+ return textError ? err({ kind: "bad-shape", path, detail: textError }) : ok(map);
555
+ }
556
+
557
+ // src/store/transaction.ts
558
+ var LedgerError = class extends Error {
559
+ constructor(code, message, details = {}) {
560
+ super(message);
561
+ this.code = code;
562
+ this.details = details;
563
+ }
564
+ };
565
+ function storeDirectory(file) {
566
+ const dir = dirname2(file);
567
+ return dir.endsWith("/pages") || dir.endsWith("\\pages") ? dirname2(dir) : dir;
568
+ }
569
+ function deadOwner(lock) {
570
+ try {
571
+ const owner = JSON.parse(readFileSync(join2(lock, "owner.json"), "utf8"));
572
+ if (!Number.isSafeInteger(owner.pid) || owner.pid <= 0) return false;
573
+ try {
574
+ process.kill(owner.pid, 0);
575
+ return false;
576
+ } catch (e) {
577
+ return e.code === "ESRCH";
578
+ }
579
+ } catch {
580
+ return false;
581
+ }
582
+ }
583
+ function withStoreLock(file, action) {
584
+ const dir = storeDirectory(file);
585
+ mkdirSync(dir, { recursive: true });
586
+ const lock = join2(dir, ".write-lock");
587
+ const owner = join2(lock, "owner.json");
588
+ const token = randomUUID();
589
+ try {
590
+ mkdirSync(lock);
591
+ } catch (error) {
592
+ if (error.code !== "EEXIST") throw error;
593
+ if (!deadOwner(lock)) throw new LedgerError("BUSY", `Another writer owns ${lock}; retry after it completes. An orphan without owner metadata needs manual inspection.`);
594
+ try {
595
+ mkdirSync(join2(lock, ".reap"));
596
+ } catch {
597
+ throw new LedgerError("BUSY", "Another process is recovering the writer lock.");
598
+ }
599
+ if (!deadOwner(lock)) {
600
+ rmSync(join2(lock, ".reap"), { recursive: true, force: true });
601
+ throw new LedgerError("BUSY", "Writer ownership changed.");
602
+ }
603
+ rmSync(lock, { recursive: true });
604
+ try {
605
+ mkdirSync(lock);
606
+ } catch {
607
+ throw new LedgerError("BUSY", "Another writer acquired the recovered lock.");
608
+ }
609
+ }
610
+ let initialized = false;
611
+ try {
612
+ writeFileSync(owner, JSON.stringify({ pid: process.pid, token }), { flag: "wx" });
613
+ initialized = true;
614
+ return action();
615
+ } finally {
616
+ try {
617
+ if (!initialized || JSON.parse(readFileSync(owner, "utf8")).token === token) rmSync(lock, { recursive: true });
618
+ } catch {
619
+ }
620
+ }
621
+ }
622
+
238
623
  // src/semantics/vocabulary.ts
239
624
  var STATUS_GLYPHS = {
240
625
  planned: ["\xB7", "."],
@@ -573,6 +958,7 @@ function displayWidth(text) {
573
958
  return w;
574
959
  }
575
960
  function fitWidth(s, width) {
961
+ s = terminalText(s);
576
962
  if (displayWidth(s) <= width) return s;
577
963
  let out = "";
578
964
  let w = 0;
@@ -588,7 +974,7 @@ function wrapWidth(s, width) {
588
974
  const lines = [];
589
975
  let line = "";
590
976
  let w = 0;
591
- for (const ch of s.replace(/\r/g, "")) {
977
+ for (const ch of terminalText(s.replace(/\r/g, "").replace(/\t/g, " "), true)) {
592
978
  if (ch === "\n") {
593
979
  lines.push(line);
594
980
  line = "";
@@ -617,7 +1003,9 @@ var SGR = {
617
1003
  greenDim: "32;2",
618
1004
  // done, but nothing behind the claim: green, not fully lit
619
1005
  red: "31",
620
- faint: "90"
1006
+ faint: "90",
1007
+ focus: "97"
1008
+ // bright foreground without changing font weight
621
1009
  };
622
1010
  var ANSI_RESET = "\x1B[0m";
623
1011
  var UP = 1;
@@ -678,7 +1066,7 @@ var Canvas = class {
678
1066
  /** Write literal text starting at (x, y). Returns the column just past it. */
679
1067
  text(x, y, s, style, bold = false) {
680
1068
  let cx = x;
681
- for (const ch of s) {
1069
+ for (const ch of terminalText(s)) {
682
1070
  const w = charWidth(ch.codePointAt(0));
683
1071
  if (w === 0) {
684
1072
  const base = this.cell(Math.max(0, cx - 1), y);
@@ -737,7 +1125,7 @@ var Canvas = class {
737
1125
  } else if (charWidth(ch.codePointAt(0)) === 2 && x + 1 >= vp.x + vp.width) {
738
1126
  ch = " ";
739
1127
  }
740
- const params = ch === " " ? "" : isWire ? c.bright ? "1" : SGR.faint : [SGR[c.style], c.bold ? "1" : ""].filter(Boolean).join(";");
1128
+ const params = ch === " " ? "" : isWire ? c.bright ? SGR.focus : SGR.faint : [SGR[c.style], c.bold ? "1" : ""].filter(Boolean).join(";");
741
1129
  if (opts.color && params !== open) {
742
1130
  line += (open !== "" ? ANSI_RESET : "") + (params !== "" ? `\x1B[${params}m` : "");
743
1131
  open = params;
@@ -1086,6 +1474,7 @@ function drawBands(canvas, columns, rows, wiredWidth, totalWidth) {
1086
1474
  function drawBox(canvas, box, opts, neutral, face, focused = false) {
1087
1475
  const { node, x, y, w } = box;
1088
1476
  const skin = neutral ? neutralSkin(opts.unicode) : skinFor(face, opts.unicode);
1477
+ const borderStyle = focused ? "focus" : skin.style;
1089
1478
  const slotGlyph = neutral ? neutralGlyph(node, opts.unicode) : glyphFor(face, opts);
1090
1479
  if (box.borderless) {
1091
1480
  canvas.text(x + 1, y, slotGlyph, skin.style, true);
@@ -1093,18 +1482,18 @@ function drawBox(canvas, box, opts, neutral, face, focused = false) {
1093
1482
  }
1094
1483
  const inner = w - 2;
1095
1484
  const pad = box.pad === 1 ? " " : "";
1096
- canvas.text(x, y, skin.corners[0] + skin.h.repeat(inner) + skin.corners[1], skin.style, focused);
1097
- canvas.text(x, y + 1, skin.v, skin.style, focused);
1485
+ canvas.text(x, y, skin.corners[0] + skin.h.repeat(inner) + skin.corners[1], borderStyle);
1486
+ canvas.text(x, y + 1, skin.v, borderStyle);
1098
1487
  canvas.text(x + 1, y + 1, `${pad}${slotGlyph} ${box.label}${pad}`, skin.style, true);
1099
- canvas.text(x + w - 1, y + 1, skin.v, skin.style, focused);
1488
+ canvas.text(x + w - 1, y + 1, skin.v, borderStyle);
1100
1489
  for (let i = 0; i < box.extra.length; i++) {
1101
1490
  const row = box.extra[i];
1102
1491
  const yy = y + 2 + i;
1103
- canvas.text(x, yy, skin.v, skin.style, focused);
1492
+ canvas.text(x, yy, skin.v, borderStyle);
1104
1493
  canvas.text(x + 1, yy, row.text, row.style);
1105
- canvas.text(x + w - 1, yy, skin.v, skin.style, focused);
1494
+ canvas.text(x + w - 1, yy, skin.v, borderStyle);
1106
1495
  }
1107
- canvas.text(x, y + box.h - 1, skin.corners[2] + skin.h.repeat(inner) + skin.corners[3], skin.style, focused);
1496
+ canvas.text(x, y + box.h - 1, skin.corners[2] + skin.h.repeat(inner) + skin.corners[3], borderStyle);
1108
1497
  }
1109
1498
  function drawEdges(canvas, edges, rows, opts) {
1110
1499
  for (const edge of edges) {
@@ -1234,322 +1623,133 @@ function layoutColumns(map, geo, unicode, neutral) {
1234
1623
  }
1235
1624
  }
1236
1625
  }
1237
- const placed = /* @__PURE__ */ new Map();
1238
- for (const [, spec] of sized) placed.set(spec, { ...spec, x: columnOf.get(spec) ?? LEFT_MARGIN });
1239
- const bandBoxes = bandSized.map((row) => row.map((spec) => placed.get(spec)));
1240
- const boxOf = /* @__PURE__ */ new Map();
1241
- for (const node of map.nodes) boxOf.set(node.id, placed.get(sized.get(node.id)));
1242
- const bandLabel = bands.map((l, i) => {
1243
- const row = bandBoxes[i];
1244
- const done = row.filter((b) => b.node.status === "done").length;
1245
- return geo.bandCounts && row.length > 0 && !neutral ? ` ${l.name} ${done}/${row.length}` : ` ${l.name}`;
1246
- });
1247
- let contentWidth = LEFT_MARGIN + BAR_MIN_RUN;
1248
- for (const row of bandBoxes) for (const box of row) contentWidth = Math.max(contentWidth, box.x + box.w);
1249
- for (const lane of lanes) contentWidth = Math.max(contentWidth, lane.x + lane.w);
1250
- return { bands, bandIndexOf, bandBoxes, boxOf, lanes, bandLabel, contentWidth };
1251
- }
1252
- function layoutRows(columns, geo, gapRowCount, hasTitle, hasLanes) {
1253
- let y = 0;
1254
- if (hasTitle) y += 1 + geo.titleGap;
1255
- let laneHeaderY;
1256
- if (hasLanes) {
1257
- laneHeaderY = y;
1258
- y += 1 + geo.barGap;
1259
- }
1260
- const barY = [];
1261
- const gapTrackStartY = [];
1262
- const bandBoxes = [];
1263
- const placed = /* @__PURE__ */ new Map();
1264
- const gapCount = columns.bands.length - 1;
1265
- for (let b = 0; b < columns.bands.length; b++) {
1266
- barY.push(y);
1267
- y += 1 + geo.barGap;
1268
- const row = columns.bandBoxes[b];
1269
- for (const box of row) placed.set(box, { ...box, y });
1270
- bandBoxes.push(row.map((box) => placed.get(box)));
1271
- y += row.reduce((max, box) => Math.max(max, box.h), geo.mode === "constellation" ? 1 : BOX_H);
1272
- if (b < gapCount) {
1273
- y += geo.breathe;
1274
- gapTrackStartY.push(y);
1275
- y += gapRowCount[b];
1276
- y += geo.breathe;
1277
- }
1278
- }
1279
- const boxOf = /* @__PURE__ */ new Map();
1280
- for (const [id, box] of columns.boxOf) boxOf.set(id, placed.get(box));
1281
- return { boxOf, bandBoxes, barY, gapTrackStartY, laneHeaderY, legendY: y + 1 };
1282
- }
1283
-
1284
- // src/render/render.ts
1285
- function renderMapWindow(map, opts, viewport) {
1286
- const built = buildCanvas(map, opts);
1287
- return {
1288
- lines: built.canvas.emit(opts, viewport),
1289
- contentWidth: built.canvas.width,
1290
- contentHeight: built.canvas.height,
1291
- hits: built.hits
1292
- };
1293
- }
1294
- function buildCanvas(map, opts) {
1295
- const oriented = flipForSequence(map);
1296
- const plainGeo = zoomGeometry(opts.zoom ?? ZOOM_DEFAULT);
1297
- const aggregated = plainGeo.mode === "constellation" ? aggregateMap(oriented) : void 0;
1298
- const drawn = aggregated ?? oriented;
1299
- return paint(drawn, opts, aggregated !== void 0 ? AGGREGATE_GEO : plainGeo, unverifiedDoneIds(oriented, drawn));
1300
- }
1301
- function paint(map, opts, geo, unverified) {
1302
- const canvas = new Canvas();
1303
- if (map.layers.length === 0) {
1304
- canvas.text(0, 0, map.title ?? "mellos mapping", "none", true);
1305
- canvas.text(0, 2, "(empty map \u2014 declare layers and nodes to begin)", "dim");
1306
- return { canvas, hits: [] };
1307
- }
1308
- const neutral = isNeutralKind(map);
1309
- const columns = layoutColumns(map, geo, opts.unicode, neutral);
1310
- const routing = routeEdges(map, columns);
1311
- const rows = layoutRows(columns, geo, routing.gapRowCount, map.title !== void 0, map.lanes.length > 0);
1312
- const wiredWidth = routing.fallbackCount > 0 ? columns.contentWidth + 2 + routing.fallbackCount * 2 : columns.contentWidth;
1313
- const totalWidth = wiredWidth + Math.max(...columns.bandLabel.map(displayWidth));
1314
- if (map.title !== void 0) drawTitle(canvas, map.title);
1315
- drawLaneHeaders(canvas, map, columns, rows);
1316
- drawBands(canvas, columns, rows, wiredWidth, totalWidth);
1317
- const faceOf = (id, status) => unverified.has(id) ? "done-unverified" : status;
1318
- for (const box of rows.boxOf.values()) {
1319
- const id = box.node.id;
1320
- drawBox(canvas, box, opts, neutral, faceOf(id, box.node.status), opts.focus !== void 0 && id === opts.focus);
1321
- }
1322
- drawEdges(canvas, routing.edges, rows, opts);
1323
- drawLegend(canvas, map, opts, rows.legendY, neutral, unverified.size > 0);
1324
- const hits = [...rows.boxOf.values()].map((b) => ({
1325
- id: b.node.id,
1326
- x: b.x,
1327
- y: b.y,
1328
- w: b.w,
1329
- h: b.h
1330
- }));
1331
- return { canvas, hits };
1332
- }
1333
-
1334
- // src/store/store.ts
1335
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
1336
- import { basename, dirname, join } from "node:path";
1337
-
1338
- // src/store/format.ts
1339
- var STATE_FILE_VERSION = 1;
1340
- function makePageId(raw) {
1341
- return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
1342
- }
1343
- function describeStoreError(e) {
1344
- switch (e.kind) {
1345
- case "not-found":
1346
- return `no map file at ${e.path}`;
1347
- case "malformed-json":
1348
- return `map file ${e.path} is not valid JSON: ${e.detail}`;
1349
- case "bad-shape":
1350
- return `map file ${e.path} has an unexpected shape: ${e.detail}`;
1351
- case "invariant-violation":
1352
- return `map file ${e.path} violates a structural invariant: ${describeMapError(e.violation)}`;
1353
- case "save-failed":
1354
- return `could not write ${e.path}: ${e.detail}`;
1355
- case "delete-failed":
1356
- return `could not delete ${e.path}: ${e.detail}`;
1357
- }
1358
- }
1359
- function isRecord(v) {
1360
- return typeof v === "object" && v !== null && !Array.isArray(v);
1361
- }
1362
- function describeValue(v) {
1363
- if (v === void 0) return "missing";
1364
- if (v === null) return "null";
1365
- if (Array.isArray(v)) return "an array";
1366
- return `a ${typeof v}`;
1367
- }
1368
- function badShape(path, where, expected, got) {
1369
- return err({ kind: "bad-shape", path, detail: `${where} is ${describeValue(got)}, expected ${expected}` });
1370
- }
1371
- function arrayField(raw, key, path, presence) {
1372
- const v = raw[key];
1373
- if (Array.isArray(v)) return ok(v);
1374
- if (v === void 0 && presence === "optional") return ok([]);
1375
- return badShape(path, `"${key}"`, "an array", v);
1376
- }
1377
- function requiredString(rec, key, where, path) {
1378
- const v = rec[key];
1379
- return typeof v === "string" ? ok(v) : badShape(path, `${where}.${key}`, "a string", v);
1380
- }
1381
- function optionalString(rec, key, where, path) {
1382
- const v = rec[key];
1383
- if (v === void 0) return ok(void 0);
1384
- return typeof v === "string" ? ok(v) : badShape(path, `${where}.${key}`, "a string", v);
1385
- }
1386
- function parseMap(raw, path) {
1387
- if (!isRecord(raw)) return err({ kind: "bad-shape", path, detail: "root is not an object" });
1388
- if (raw["version"] !== STATE_FILE_VERSION) {
1389
- return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${STATE_FILE_VERSION}` });
1390
- }
1391
- const layers = arrayField(raw, "layers", path, "required");
1392
- if (!layers.ok) return layers;
1393
- const nodes = arrayField(raw, "nodes", path, "required");
1394
- if (!nodes.ok) return nodes;
1395
- const edges = arrayField(raw, "edges", path, "required");
1396
- if (!edges.ok) return edges;
1397
- const lanes = arrayField(raw, "lanes", path, "optional");
1398
- if (!lanes.ok) return lanes;
1399
- const groups = arrayField(raw, "groups", path, "optional");
1400
- if (!groups.ok) return groups;
1401
- let map = EMPTY_MAP;
1402
- const title = optionalString(raw, "title", "map", path);
1403
- if (!title.ok) return title;
1404
- if (title.value !== void 0) map = setTitle(map, title.value);
1405
- const rawKind = optionalString(raw, "kind", "map", path);
1406
- if (!rawKind.ok) return rawKind;
1407
- if (rawKind.value !== void 0) {
1408
- const kind = makeMapKind(rawKind.value);
1409
- if (!kind.ok) return err({ kind: "invariant-violation", path, violation: kind.error });
1410
- map = setKind(map, kind.value);
1411
- }
1412
- for (const [i, rawLayer] of layers.value.entries()) {
1413
- const where = `layers[${i}]`;
1414
- if (!isRecord(rawLayer)) return badShape(path, where, "an object", rawLayer);
1415
- const rawId = requiredString(rawLayer, "id", where, path);
1416
- if (!rawId.ok) return rawId;
1417
- const id = makeLayerId(rawId.value);
1418
- if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
1419
- const name = requiredString(rawLayer, "name", where, path);
1420
- if (!name.ok) return name;
1421
- const rawRank = rawLayer["rank"];
1422
- if (typeof rawRank !== "number") return badShape(path, `${where}.rank`, "a number", rawRank);
1423
- const rank = makeRank(rawRank);
1424
- if (!rank.ok) return err({ kind: "invariant-violation", path, violation: rank.error });
1425
- const next = declareLayer(map, { id: id.value, name: name.value, rank: rank.value });
1426
- if (!next.ok) return err({ kind: "invariant-violation", path, violation: next.error });
1427
- map = next.value;
1428
- }
1429
- for (const [i, rawLane] of lanes.value.entries()) {
1430
- const where = `lanes[${i}]`;
1431
- if (!isRecord(rawLane)) return badShape(path, where, "an object", rawLane);
1432
- const rawId = requiredString(rawLane, "id", where, path);
1433
- if (!rawId.ok) return rawId;
1434
- const id = makeLaneId(rawId.value);
1435
- if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
1436
- const label = requiredString(rawLane, "label", where, path);
1437
- if (!label.ok) return label;
1438
- const declared = declareLane(map, { id: id.value, label: label.value });
1439
- if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
1440
- map = declared.value;
1441
- }
1442
- for (const [i, rawGroup] of groups.value.entries()) {
1443
- const where = `groups[${i}]`;
1444
- if (!isRecord(rawGroup)) return badShape(path, where, "an object", rawGroup);
1445
- const rawId = requiredString(rawGroup, "id", where, path);
1446
- if (!rawId.ok) return rawId;
1447
- const id = makeGroupId(rawId.value);
1448
- if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
1449
- const rawLayer = requiredString(rawGroup, "layer", where, path);
1450
- if (!rawLayer.ok) return rawLayer;
1451
- const layer = makeLayerId(rawLayer.value);
1452
- if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
1453
- const label = requiredString(rawGroup, "label", where, path);
1454
- if (!label.ok) return label;
1455
- const declared = declareGroup(map, { id: id.value, label: label.value, layer: layer.value });
1456
- if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
1457
- map = declared.value;
1458
- }
1459
- for (const [i, rawNode] of nodes.value.entries()) {
1460
- const where = `nodes[${i}]`;
1461
- if (!isRecord(rawNode)) return badShape(path, where, "an object", rawNode);
1462
- const rawId = requiredString(rawNode, "id", where, path);
1463
- if (!rawId.ok) return rawId;
1464
- const id = makeNodeId(rawId.value);
1465
- if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
1466
- const rawLayer = requiredString(rawNode, "layer", where, path);
1467
- if (!rawLayer.ok) return rawLayer;
1468
- const layer = makeLayerId(rawLayer.value);
1469
- if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
1470
- const rawStatus = requiredString(rawNode, "status", where, path);
1471
- if (!rawStatus.ok) return rawStatus;
1472
- const status = makeNodeStatus(rawStatus.value);
1473
- if (!status.ok) return err({ kind: "invariant-violation", path, violation: status.error });
1474
- const label = requiredString(rawNode, "label", where, path);
1475
- if (!label.ok) return label;
1476
- const detail = optionalString(rawNode, "detail", where, path);
1477
- if (!detail.ok) return detail;
1478
- const rawGroup = optionalString(rawNode, "group", where, path);
1479
- if (!rawGroup.ok) return rawGroup;
1480
- let group;
1481
- if (rawGroup.value !== void 0) {
1482
- const made = makeGroupId(rawGroup.value);
1483
- if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
1484
- group = made.value;
1485
- }
1486
- const rawNodeKind = optionalString(rawNode, "kind", where, path);
1487
- if (!rawNodeKind.ok) return rawNodeKind;
1488
- let nodeKind;
1489
- if (rawNodeKind.value !== void 0) {
1490
- const made = makeNodeKind(rawNodeKind.value);
1491
- if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
1492
- nodeKind = made.value;
1493
- }
1494
- const rawLane = optionalString(rawNode, "lane", where, path);
1495
- if (!rawLane.ok) return rawLane;
1496
- let lane;
1497
- if (rawLane.value !== void 0) {
1498
- const made = makeLaneId(rawLane.value);
1499
- if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
1500
- lane = made.value;
1626
+ const placed = /* @__PURE__ */ new Map();
1627
+ for (const [, spec] of sized) placed.set(spec, { ...spec, x: columnOf.get(spec) ?? LEFT_MARGIN });
1628
+ const bandBoxes = bandSized.map((row) => row.map((spec) => placed.get(spec)));
1629
+ const boxOf = /* @__PURE__ */ new Map();
1630
+ for (const node of map.nodes) boxOf.set(node.id, placed.get(sized.get(node.id)));
1631
+ const bandLabel = bands.map((l, i) => {
1632
+ const row = bandBoxes[i];
1633
+ const done = row.filter((b) => b.node.status === "done").length;
1634
+ return geo.bandCounts && row.length > 0 && !neutral ? ` ${l.name} ${done}/${row.length}` : ` ${l.name}`;
1635
+ });
1636
+ let contentWidth = LEFT_MARGIN + BAR_MIN_RUN;
1637
+ for (const row of bandBoxes) for (const box of row) contentWidth = Math.max(contentWidth, box.x + box.w);
1638
+ for (const lane of lanes) contentWidth = Math.max(contentWidth, lane.x + lane.w);
1639
+ return { bands, bandIndexOf, bandBoxes, boxOf, lanes, bandLabel, contentWidth };
1640
+ }
1641
+ function layoutRows(columns, geo, gapRowCount, hasTitle, hasLanes) {
1642
+ let y = 0;
1643
+ if (hasTitle) y += 1 + geo.titleGap;
1644
+ let laneHeaderY;
1645
+ if (hasLanes) {
1646
+ laneHeaderY = y;
1647
+ y += 1 + geo.barGap;
1648
+ }
1649
+ const barY = [];
1650
+ const gapTrackStartY = [];
1651
+ const bandBoxes = [];
1652
+ const placed = /* @__PURE__ */ new Map();
1653
+ const gapCount = columns.bands.length - 1;
1654
+ for (let b = 0; b < columns.bands.length; b++) {
1655
+ barY.push(y);
1656
+ y += 1 + geo.barGap;
1657
+ const row = columns.bandBoxes[b];
1658
+ for (const box of row) placed.set(box, { ...box, y });
1659
+ bandBoxes.push(row.map((box) => placed.get(box)));
1660
+ y += row.reduce((max, box) => Math.max(max, box.h), geo.mode === "constellation" ? 1 : BOX_H);
1661
+ if (b < gapCount) {
1662
+ y += geo.breathe;
1663
+ gapTrackStartY.push(y);
1664
+ y += gapRowCount[b];
1665
+ y += geo.breathe;
1501
1666
  }
1502
- const rawSubmap = optionalString(rawNode, "submap", where, path);
1503
- if (!rawSubmap.ok) return rawSubmap;
1504
- let submap;
1505
- if (rawSubmap.value !== void 0) {
1506
- const made = makeSubmapRef(rawSubmap.value);
1507
- if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
1508
- submap = made.value;
1667
+ }
1668
+ const boxOf = /* @__PURE__ */ new Map();
1669
+ for (const [id, box] of columns.boxOf) boxOf.set(id, placed.get(box));
1670
+ return { boxOf, bandBoxes, barY, gapTrackStartY, laneHeaderY, legendY: y + 1 };
1671
+ }
1672
+
1673
+ // src/render/render.ts
1674
+ function renderMapWindow(map, opts, viewport) {
1675
+ return renderSceneWindow(prepareScene(map, opts), opts, viewport);
1676
+ }
1677
+ function createWindowRenderer() {
1678
+ let previous;
1679
+ let frame;
1680
+ return (map, opts, viewport) => {
1681
+ const zoom = opts.zoom ?? ZOOM_DEFAULT;
1682
+ if (previous?.map !== map || previous.unicode !== opts.unicode || previous.zoom !== zoom) {
1683
+ previous = { map, unicode: opts.unicode, zoom, scene: prepareScene(map, opts) };
1509
1684
  }
1510
- const declared = declareNode(map, {
1511
- id: id.value,
1512
- label: label.value,
1513
- layer: layer.value,
1514
- status: status.value,
1515
- ...detail.value !== void 0 ? { detail: detail.value } : {},
1516
- ...group !== void 0 ? { group } : {},
1517
- ...nodeKind !== void 0 ? { kind: nodeKind } : {},
1518
- ...lane !== void 0 ? { lane } : {},
1519
- ...submap !== void 0 ? { submap } : {}
1520
- });
1521
- if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
1522
- map = declared.value;
1523
- const evidence = optionalString(rawNode, "evidence", where, path);
1524
- if (!evidence.ok) return evidence;
1525
- if (evidence.value !== void 0) {
1526
- const updated = updateNode(map, { id: id.value, evidence: evidence.value });
1527
- if (!updated.ok) return err({ kind: "invariant-violation", path, violation: updated.error });
1528
- map = updated.value;
1685
+ const scene = previous.scene;
1686
+ if (frame?.scene !== scene || frame.spinner !== opts.spinnerFrame || frame.focus !== opts.focus || frame.color !== opts.color) {
1687
+ frame = { scene, spinner: opts.spinnerFrame, focus: opts.focus, color: opts.color, built: paint(scene, opts) };
1529
1688
  }
1689
+ return emitWindow(frame.built, opts, viewport);
1690
+ };
1691
+ }
1692
+ function renderSceneWindow(scene, opts, viewport) {
1693
+ return emitWindow(paint(scene, opts), opts, viewport);
1694
+ }
1695
+ function emitWindow(built, opts, viewport) {
1696
+ return {
1697
+ lines: built.canvas.emit(opts, viewport),
1698
+ contentWidth: built.canvas.width,
1699
+ contentHeight: built.canvas.height,
1700
+ hits: built.hits
1701
+ };
1702
+ }
1703
+ function prepareScene(map, opts) {
1704
+ const oriented = flipForSequence(map);
1705
+ const plainGeo = zoomGeometry(opts.zoom ?? ZOOM_DEFAULT);
1706
+ const aggregated = plainGeo.mode === "constellation" ? aggregateMap(oriented) : void 0;
1707
+ const drawn = aggregated ?? oriented;
1708
+ const unverified = unverifiedDoneIds(oriented, drawn);
1709
+ if (drawn.layers.length === 0) return { map: drawn, unverified };
1710
+ return { map: drawn, unverified, geometry: prepareGeometry(drawn, opts, aggregated !== void 0 ? AGGREGATE_GEO : plainGeo) };
1711
+ }
1712
+ function prepareGeometry(map, opts, geo) {
1713
+ const neutral = isNeutralKind(map);
1714
+ const columns = layoutColumns(map, geo, opts.unicode, neutral);
1715
+ const routing = routeEdges(map, columns);
1716
+ const rows = layoutRows(columns, geo, routing.gapRowCount, map.title !== void 0, map.lanes.length > 0);
1717
+ const wiredWidth = routing.fallbackCount > 0 ? columns.contentWidth + 2 + routing.fallbackCount * 2 : columns.contentWidth;
1718
+ const totalWidth = wiredWidth + Math.max(...columns.bandLabel.map(displayWidth));
1719
+ const hits = [...rows.boxOf.values()].map((b) => ({
1720
+ id: b.node.id,
1721
+ x: b.x,
1722
+ y: b.y,
1723
+ w: b.w,
1724
+ h: b.h
1725
+ }));
1726
+ return { neutral, columns, routing, rows, wiredWidth, totalWidth, hits };
1727
+ }
1728
+ function paint(scene, opts) {
1729
+ const { map, unverified, geometry } = scene;
1730
+ const canvas = new Canvas();
1731
+ if (geometry === void 0) {
1732
+ canvas.text(0, 0, map.title ?? "mellos mapping", "none", true);
1733
+ canvas.text(0, 2, "(empty map \u2014 declare layers and nodes to begin)", "dim");
1734
+ return { canvas, hits: [] };
1530
1735
  }
1531
- for (const [i, rawEdge] of edges.value.entries()) {
1532
- const where = `edges[${i}]`;
1533
- if (!isRecord(rawEdge)) return badShape(path, where, "an object", rawEdge);
1534
- const rawFrom = requiredString(rawEdge, "from", where, path);
1535
- if (!rawFrom.ok) return rawFrom;
1536
- const from = makeNodeId(rawFrom.value);
1537
- if (!from.ok) return err({ kind: "invariant-violation", path, violation: from.error });
1538
- const rawTo = requiredString(rawEdge, "to", where, path);
1539
- if (!rawTo.ok) return rawTo;
1540
- const to = makeNodeId(rawTo.value);
1541
- if (!to.ok) return err({ kind: "invariant-violation", path, violation: to.error });
1542
- const label = optionalString(rawEdge, "label", where, path);
1543
- if (!label.ok) return label;
1544
- const linked = linkNodes(map, from.value, to.value, label.value);
1545
- if (!linked.ok) return err({ kind: "invariant-violation", path, violation: linked.error });
1546
- map = linked.value;
1736
+ const { neutral, columns, routing, rows, wiredWidth, totalWidth, hits } = geometry;
1737
+ if (map.title !== void 0) drawTitle(canvas, map.title);
1738
+ drawLaneHeaders(canvas, map, columns, rows);
1739
+ drawBands(canvas, columns, rows, wiredWidth, totalWidth);
1740
+ const faceOf = (id, status) => unverified.has(id) ? "done-unverified" : status;
1741
+ for (const box of rows.boxOf.values()) {
1742
+ const id = box.node.id;
1743
+ drawBox(canvas, box, opts, neutral, faceOf(id, box.node.status), opts.focus !== void 0 && id === opts.focus);
1547
1744
  }
1548
- return ok(map);
1745
+ drawEdges(canvas, routing.edges, rows, opts);
1746
+ drawLegend(canvas, map, opts, rows.legendY, neutral, unverified.size > 0);
1747
+ return { canvas, hits };
1549
1748
  }
1550
1749
 
1551
- // src/store/store.ts
1552
- var RENAME_MAX_ATTEMPTS = 10;
1750
+ // src/store/atomic.ts
1751
+ import { mkdirSync as mkdirSync2, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
1752
+ import { dirname as dirname3 } from "node:path";
1553
1753
  var RENAME_BACKOFF_STEP_MS = 10;
1554
1754
  var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOENT"]);
1555
1755
  function sleepSync(ms) {
@@ -1557,18 +1757,18 @@ function sleepSync(ms) {
1557
1757
  }
1558
1758
  function discardTemp(tmp) {
1559
1759
  try {
1560
- rmSync(tmp, { force: true });
1760
+ rmSync2(tmp, { force: true });
1561
1761
  } catch {
1562
1762
  }
1563
1763
  }
1564
1764
  function errnoOf(e) {
1565
1765
  return e.code ?? e.message;
1566
1766
  }
1567
- function writeFileAtomic(path, contents) {
1767
+ function writeAtomic(path, contents, maxAttempts) {
1568
1768
  const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2, 10)}.tmp`;
1569
1769
  try {
1570
- mkdirSync(dirname(path), { recursive: true });
1571
- writeFileSync(tmp, contents, "utf8");
1770
+ mkdirSync2(dirname3(path), { recursive: true });
1771
+ writeFileSync2(tmp, contents, "utf8");
1572
1772
  } catch (e) {
1573
1773
  discardTemp(tmp);
1574
1774
  return err({ kind: "save-failed", path, detail: `writing the temp file failed: ${errnoOf(e)}` });
@@ -1580,7 +1780,7 @@ function writeFileAtomic(path, contents) {
1580
1780
  return ok(void 0);
1581
1781
  } catch (e) {
1582
1782
  const code = errnoOf(e);
1583
- if (!TRANSIENT_RENAME_CODES.has(code) || attempt >= RENAME_MAX_ATTEMPTS) {
1783
+ if (!TRANSIENT_RENAME_CODES.has(code) || attempt >= maxAttempts) {
1584
1784
  discardTemp(tmp);
1585
1785
  return err({ kind: "save-failed", path, detail: `${code} after ${attempt} attempt(s)` });
1586
1786
  }
@@ -1589,17 +1789,15 @@ function writeFileAtomic(path, contents) {
1589
1789
  }
1590
1790
  }
1591
1791
  }
1592
- function isRecord2(v) {
1593
- return typeof v === "object" && v !== null && !Array.isArray(v);
1594
- }
1595
- function stripBom(text) {
1596
- return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
1597
- }
1792
+
1793
+ // src/store/pages.ts
1794
+ import { existsSync as existsSync2, readdirSync, rmSync as rmSync3 } from "node:fs";
1795
+ import { basename, dirname as dirname4, join as join3 } from "node:path";
1598
1796
  var STORE_DIR_NAME = ".mellos";
1599
- var STATE_FILE_RELATIVE_PATH = join(STORE_DIR_NAME, "map.json");
1797
+ var STATE_FILE_RELATIVE_PATH = join3(STORE_DIR_NAME, "map.json");
1600
1798
  var PAGES_DIR_NAME = "pages";
1601
1799
  function pageFilePath(defaultFile, page) {
1602
- return page === void 0 ? defaultFile : join(dirname(defaultFile), PAGES_DIR_NAME, `${page}.json`);
1800
+ return page === void 0 ? defaultFile : join3(dirname4(defaultFile), PAGES_DIR_NAME, `${page}.json`);
1603
1801
  }
1604
1802
  function pageIdOfFile(defaultFile, path) {
1605
1803
  if (path === defaultFile) return void 0;
@@ -1608,126 +1806,161 @@ function pageIdOfFile(defaultFile, path) {
1608
1806
  }
1609
1807
  function listPageFiles(defaultFile) {
1610
1808
  const out = [];
1611
- if (existsSync(defaultFile)) out.push(defaultFile);
1809
+ if (existsSync2(defaultFile)) out.push(defaultFile);
1612
1810
  let entries = [];
1613
1811
  try {
1614
- entries = readdirSync(join(dirname(defaultFile), PAGES_DIR_NAME));
1812
+ entries = readdirSync(join3(dirname4(defaultFile), PAGES_DIR_NAME));
1615
1813
  } catch {
1616
1814
  }
1617
1815
  for (const e of entries.sort()) {
1618
- if (e.endsWith(".json")) out.push(join(dirname(defaultFile), PAGES_DIR_NAME, e));
1816
+ if (e.endsWith(".json")) out.push(join3(dirname4(defaultFile), PAGES_DIR_NAME, e));
1619
1817
  }
1620
1818
  return out;
1621
1819
  }
1622
1820
  function deletePageFile(path) {
1623
1821
  try {
1624
- rmSync(path, { force: true });
1822
+ rmSync3(path, { force: true });
1625
1823
  return ok(void 0);
1626
1824
  } catch (e) {
1627
1825
  return err({ kind: "delete-failed", path, detail: errnoOf(e) });
1628
1826
  }
1629
1827
  }
1828
+
1829
+ // src/store/channels.ts
1830
+ import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync as rmSync5 } from "node:fs";
1831
+ import { dirname as dirname6, join as join5 } from "node:path";
1832
+
1833
+ // src/store/json-text.ts
1834
+ function isRecord2(v) {
1835
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1836
+ }
1837
+ function stripBom(text) {
1838
+ return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
1839
+ }
1840
+
1841
+ // src/store/viewers.ts
1842
+ import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync, rmSync as rmSync4 } from "node:fs";
1843
+ import { dirname as dirname5, join as join4 } from "node:path";
1844
+ var VIEWERS_DIR_NAME = "viewers";
1845
+ var VIEWER_FILE_VERSION = 1;
1846
+ var VIEWER_HEARTBEAT_MS = 1e3;
1847
+ function viewersDirPath(defaultFile) {
1848
+ return join4(dirname5(defaultFile), VIEWERS_DIR_NAME);
1849
+ }
1850
+ function viewerFilePath(defaultFile, pid) {
1851
+ return join4(viewersDirPath(defaultFile), `${pid}.json`);
1852
+ }
1853
+ function publishViewer(defaultFile, pid, report) {
1854
+ const body = { version: VIEWER_FILE_VERSION, page: report.page ?? null, follow: report.follow, owner: report.owner };
1855
+ return writeAtomic(viewerFilePath(defaultFile, pid), `${JSON.stringify(body, null, 2)}
1856
+ `, 1);
1857
+ }
1858
+ function retireViewer(defaultFile, pid) {
1859
+ try {
1860
+ rmSync4(viewerFilePath(defaultFile, pid), { force: true });
1861
+ } catch {
1862
+ }
1863
+ }
1864
+
1865
+ // src/store/channels.ts
1630
1866
  var FOCUS_FILE_NAME = "focus";
1631
- function focusFilePath(defaultFile) {
1632
- return join(dirname(defaultFile), FOCUS_FILE_NAME);
1867
+ function focusFilePath(defaultFile, pid) {
1868
+ return paneChannelPath(defaultFile, FOCUS_FILE_NAME, pid);
1633
1869
  }
1634
- function takeFocusRequest(defaultFile) {
1635
- const path = focusFilePath(defaultFile);
1870
+ function paneChannelPath(defaultFile, channel, pid) {
1871
+ if (pid === void 0) return join5(dirname6(defaultFile), channel);
1872
+ if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error("Invalid pane process id");
1873
+ return join5(viewersDirPath(defaultFile), `${pid}.${channel}`);
1874
+ }
1875
+ function takeFocusRequest(defaultFile, pid) {
1876
+ const targeted = pid === void 0 ? void 0 : focusFilePath(defaultFile, pid);
1877
+ const path = targeted !== void 0 && existsSync3(targeted) ? targeted : focusFilePath(defaultFile);
1636
1878
  let raw;
1637
1879
  try {
1638
- raw = readFileSync(path, "utf8");
1880
+ raw = readFileSync3(path, "utf8");
1639
1881
  } catch {
1640
1882
  return void 0;
1641
1883
  }
1642
1884
  try {
1643
- rmSync(path, { force: true });
1885
+ rmSync5(path, { force: true });
1644
1886
  } catch {
1645
1887
  }
1646
- let parsed;
1888
+ let parsed2;
1647
1889
  try {
1648
- parsed = JSON.parse(raw);
1890
+ parsed2 = JSON.parse(raw);
1649
1891
  } catch {
1650
1892
  return void 0;
1651
1893
  }
1652
- if (typeof parsed !== "object" || parsed === null) return void 0;
1653
- const page = parsed.page;
1894
+ if (typeof parsed2 !== "object" || parsed2 === null) return void 0;
1895
+ const page = parsed2.page;
1654
1896
  if (page === void 0 || page === null) return { page: void 0 };
1655
1897
  if (typeof page !== "string") return void 0;
1656
1898
  const id = makePageId(page);
1657
1899
  return id.ok ? { page: id.value } : void 0;
1658
1900
  }
1659
1901
  var QUIT_FILE_NAME = "quit";
1660
- function quitFilePath(defaultFile) {
1661
- return join(dirname(defaultFile), QUIT_FILE_NAME);
1902
+ function quitFilePath(defaultFile, pid) {
1903
+ return paneChannelPath(defaultFile, QUIT_FILE_NAME, pid);
1662
1904
  }
1663
- function takeQuitRequest(defaultFile) {
1664
- const path = quitFilePath(defaultFile);
1905
+ function takeQuitRequest(defaultFile, pid) {
1906
+ const targeted = pid === void 0 ? void 0 : quitFilePath(defaultFile, pid);
1907
+ const path = targeted !== void 0 && existsSync3(targeted) ? targeted : quitFilePath(defaultFile);
1665
1908
  let raw;
1666
1909
  try {
1667
- raw = readFileSync(path, "utf8");
1910
+ raw = readFileSync3(path, "utf8");
1668
1911
  } catch {
1669
1912
  return false;
1670
1913
  }
1671
- sweepQuitRequest(defaultFile);
1672
- let parsed;
1914
+ sweepQuitRequest(defaultFile, path === targeted ? pid : void 0);
1915
+ let parsed2;
1673
1916
  try {
1674
- parsed = JSON.parse(stripBom(raw));
1917
+ parsed2 = JSON.parse(stripBom(raw));
1675
1918
  } catch {
1676
1919
  return false;
1677
1920
  }
1678
- return isRecord2(parsed);
1679
- }
1680
- function sweepQuitRequest(defaultFile) {
1681
- try {
1682
- rmSync(quitFilePath(defaultFile), { force: true });
1683
- } catch {
1684
- }
1685
- }
1686
- var VIEWERS_DIR_NAME = "viewers";
1687
- var VIEWER_FILE_VERSION = 1;
1688
- var VIEWER_HEARTBEAT_MS = 1e3;
1689
- function viewersDirPath(defaultFile) {
1690
- return join(dirname(defaultFile), VIEWERS_DIR_NAME);
1691
- }
1692
- function viewerFilePath(defaultFile, pid) {
1693
- return join(viewersDirPath(defaultFile), `${pid}.json`);
1921
+ return isRecord2(parsed2);
1694
1922
  }
1695
- function publishViewer(defaultFile, pid, report) {
1696
- const body = { version: VIEWER_FILE_VERSION, page: report.page ?? null, follow: report.follow };
1697
- return writeFileAtomic(viewerFilePath(defaultFile, pid), `${JSON.stringify(body, null, 2)}
1698
- `);
1699
- }
1700
- function retireViewer(defaultFile, pid) {
1923
+ function sweepQuitRequest(defaultFile, pid) {
1701
1924
  try {
1702
- rmSync(viewerFilePath(defaultFile, pid), { force: true });
1925
+ rmSync5(quitFilePath(defaultFile, pid), { force: true });
1703
1926
  } catch {
1704
1927
  }
1705
1928
  }
1929
+
1930
+ // src/store/policy.ts
1931
+ import { dirname as dirname7, join as join6 } from "node:path";
1706
1932
  var CONFIG_FILE_NAME = "config.json";
1707
1933
  function configFilePath(defaultFile) {
1708
- return join(dirname(defaultFile), CONFIG_FILE_NAME);
1934
+ return join6(dirname7(defaultFile), CONFIG_FILE_NAME);
1709
1935
  }
1710
- var LEGACY_STATE_FILE_RELATIVE_PATH = join(".claude", "mellos-mapping.json");
1936
+
1937
+ // src/store/migration.ts
1938
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, renameSync as renameSync2 } from "node:fs";
1939
+ import { dirname as dirname8, join as join7 } from "node:path";
1940
+ var LEGACY_STATE_FILE_RELATIVE_PATH = join7(".claude", "mellos-mapping.json");
1711
1941
  var LEGACY_PAGES_DIR_NAME = "mellos-mapping.pages";
1712
1942
  var LEGACY_CONFIG_FILE_NAME = "mellos-mapping.config.json";
1713
1943
  function migrateLegacyStore(defaultFile) {
1714
- const projectRoot = dirname(dirname(defaultFile));
1715
- const legacyDefault = join(projectRoot, LEGACY_STATE_FILE_RELATIVE_PATH);
1716
- const legacyPages = join(dirname(legacyDefault), LEGACY_PAGES_DIR_NAME);
1717
- const legacyConfig = join(dirname(legacyDefault), LEGACY_CONFIG_FILE_NAME);
1718
- const hasLegacy = existsSync(legacyDefault) || existsSync(legacyPages) || existsSync(legacyConfig);
1719
- const hasCurrent = existsSync(defaultFile) || existsSync(join(dirname(defaultFile), PAGES_DIR_NAME)) || existsSync(configFilePath(defaultFile));
1944
+ const projectRoot = dirname8(dirname8(defaultFile));
1945
+ const legacyDefault = join7(projectRoot, LEGACY_STATE_FILE_RELATIVE_PATH);
1946
+ const legacyPages = join7(dirname8(legacyDefault), LEGACY_PAGES_DIR_NAME);
1947
+ const legacyConfig = join7(dirname8(legacyDefault), LEGACY_CONFIG_FILE_NAME);
1948
+ const hasLegacy = existsSync4(legacyDefault) || existsSync4(legacyPages) || existsSync4(legacyConfig);
1949
+ const hasCurrent = existsSync4(defaultFile) || existsSync4(join7(dirname8(defaultFile), PAGES_DIR_NAME)) || existsSync4(configFilePath(defaultFile));
1720
1950
  if (!hasLegacy || hasCurrent) return false;
1721
- mkdirSync(dirname(defaultFile), { recursive: true });
1722
- if (existsSync(legacyDefault)) renameSync(legacyDefault, defaultFile);
1723
- if (existsSync(legacyPages)) renameSync(legacyPages, join(dirname(defaultFile), PAGES_DIR_NAME));
1724
- if (existsSync(legacyConfig)) renameSync(legacyConfig, configFilePath(defaultFile));
1951
+ mkdirSync3(dirname8(defaultFile), { recursive: true });
1952
+ if (existsSync4(legacyDefault)) renameSync2(legacyDefault, defaultFile);
1953
+ if (existsSync4(legacyPages)) renameSync2(legacyPages, join7(dirname8(defaultFile), PAGES_DIR_NAME));
1954
+ if (existsSync4(legacyConfig)) renameSync2(legacyConfig, configFilePath(defaultFile));
1725
1955
  return true;
1726
1956
  }
1957
+
1958
+ // src/store/maps.ts
1959
+ import { readFileSync as readFileSync4 } from "node:fs";
1727
1960
  function loadMapFile(path) {
1728
1961
  let text;
1729
1962
  try {
1730
- text = readFileSync(path, "utf8");
1963
+ text = readFileSync4(path, "utf8");
1731
1964
  } catch (e) {
1732
1965
  const code = e.code;
1733
1966
  if (code === "ENOENT") return err({ kind: "not-found", path });
@@ -1843,6 +2076,175 @@ function parseInput(chunk) {
1843
2076
  return { events, rest: "" };
1844
2077
  }
1845
2078
 
2079
+ // src/watch/view-state.ts
2080
+ var defaultPage = () => ({ offsetX: 0, offsetY: 0, zoom: ZOOM_DEFAULT, selectedId: void 0 });
2081
+ function initialViewState() {
2082
+ return {
2083
+ ...defaultPage(),
2084
+ pageViews: /* @__PURE__ */ new Map(),
2085
+ hoverId: void 0,
2086
+ dragAnchor: void 0,
2087
+ press: void 0,
2088
+ dividerDrag: false,
2089
+ lastClick: void 0
2090
+ };
2091
+ }
2092
+ function reduceView(state, event) {
2093
+ switch (event.kind) {
2094
+ case "reset":
2095
+ return { ...state, offsetX: 0, offsetY: 0, zoom: ZOOM_DEFAULT };
2096
+ case "pan":
2097
+ return { ...state, offsetX: state.offsetX + event.dx, offsetY: state.offsetY + event.dy };
2098
+ case "position":
2099
+ return { ...state, offsetX: event.x, offsetY: event.y };
2100
+ case "zoom":
2101
+ return { ...state, zoom: event.value };
2102
+ case "hover":
2103
+ return event.id === state.hoverId ? state : { ...state, hoverId: event.id };
2104
+ case "select":
2105
+ return { ...state, selectedId: event.id };
2106
+ case "down":
2107
+ return {
2108
+ ...state,
2109
+ dividerDrag: event.divider,
2110
+ dragAnchor: event.divider ? void 0 : { x: event.x, y: event.y, ox: state.offsetX, oy: state.offsetY },
2111
+ press: event.divider ? void 0 : { moved: false }
2112
+ };
2113
+ case "drag": {
2114
+ if (state.dividerDrag || !state.dragAnchor) return state;
2115
+ const { x, y, ox, oy } = state.dragAnchor;
2116
+ const offsetX = ox - (event.x - x);
2117
+ const offsetY = oy - (event.y - y);
2118
+ return offsetX === state.offsetX && offsetY === state.offsetY ? state : { ...state, offsetX, offsetY, press: { moved: true } };
2119
+ }
2120
+ case "up":
2121
+ return { ...state, dragAnchor: void 0, press: void 0, dividerDrag: false };
2122
+ case "pages": {
2123
+ const pageViews = new Map([...state.pageViews].filter(([file]) => event.files.includes(file)));
2124
+ if (event.previous === event.active || event.active === void 0) return { ...state, pageViews };
2125
+ if (event.previous !== void 0 && event.files.includes(event.previous)) pageViews.set(event.previous, {
2126
+ offsetX: state.offsetX,
2127
+ offsetY: state.offsetY,
2128
+ zoom: state.zoom,
2129
+ selectedId: state.selectedId
2130
+ });
2131
+ return {
2132
+ ...state,
2133
+ ...pageViews.get(event.active) ?? defaultPage(),
2134
+ pageViews,
2135
+ hoverId: void 0,
2136
+ dragAnchor: void 0,
2137
+ press: void 0,
2138
+ dividerDrag: false,
2139
+ lastClick: void 0
2140
+ };
2141
+ }
2142
+ }
2143
+ }
2144
+ function isPointerClick(state) {
2145
+ return !state.dividerDrag && state.press !== void 0 && !state.press.moved;
2146
+ }
2147
+ function clickNode(state, id, now, doubleClickMs) {
2148
+ const double = id !== void 0 && state.lastClick?.id === id && now - state.lastClick.at <= doubleClickMs;
2149
+ return {
2150
+ state: { ...state, selectedId: id, lastClick: double || id === void 0 ? void 0 : { id, at: now } },
2151
+ diveId: double ? id : void 0
2152
+ };
2153
+ }
2154
+
2155
+ // src/watch/io.ts
2156
+ var nativeWatcherIO = () => ({
2157
+ interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
2158
+ input: process.stdin,
2159
+ output: process.stdout
2160
+ });
2161
+
2162
+ // src/watch/terminal-session.ts
2163
+ var HIDE_CURSOR = "\x1B[?25l";
2164
+ var SHOW_CURSOR = "\x1B[?25h";
2165
+ var ENTER_SCREEN = "\x1B[?1049h";
2166
+ var LEAVE_SCREEN = "\x1B[?1049l";
2167
+ var CLEAR_SCREEN = "\x1B[H\x1B[2J";
2168
+ var MOUSE_ON = "\x1B[?1003h\x1B[?1006h";
2169
+ var MOUSE_OFF = "\x1B[?1003l\x1B[?1006l";
2170
+ var RESET = "\x1B[0m";
2171
+ function terminalRestoreSequence(mouseActive, alternateScreen = false) {
2172
+ return (mouseActive ? MOUSE_OFF : "") + RESET + (alternateScreen ? LEAVE_SCREEN : "") + SHOW_CURSOR + (alternateScreen ? "" : "\n");
2173
+ }
2174
+ function openTerminalSession(options) {
2175
+ const { interactive, write } = options;
2176
+ const mouseActive = interactive && options.mouse;
2177
+ write((interactive ? ENTER_SCREEN : "") + HIDE_CURSOR + CLEAR_SCREEN + (mouseActive ? MOUSE_ON : ""));
2178
+ const timer = mouseActive ? setInterval(() => write(HIDE_CURSOR + MOUSE_ON), 1e3) : void 0;
2179
+ timer?.unref();
2180
+ let closed = false;
2181
+ return {
2182
+ close() {
2183
+ if (closed) return;
2184
+ closed = true;
2185
+ clearInterval(timer);
2186
+ write(terminalRestoreSequence(mouseActive, interactive));
2187
+ }
2188
+ };
2189
+ }
2190
+
2191
+ // src/watch/frame-output.ts
2192
+ function frameDifference(previous, next) {
2193
+ const compatible = previous?.columns === next.columns;
2194
+ let output = "";
2195
+ for (let row = 0; row < Math.max(next.rows.length, previous?.rows.length ?? 0); row++) {
2196
+ const text = next.rows[row] ?? "";
2197
+ if (compatible && text === previous?.rows[row]) continue;
2198
+ output += `\x1B[${row + 1};1H${text}\x1B[0m\x1B[K`;
2199
+ }
2200
+ return output;
2201
+ }
2202
+ function createFrameOutput(port) {
2203
+ let previous;
2204
+ let latest;
2205
+ let pending;
2206
+ let unsubscribe;
2207
+ let closed = false;
2208
+ const flush = () => {
2209
+ if (closed || unsubscribe !== void 0 || pending === void 0) return;
2210
+ const frame = pending;
2211
+ pending = void 0;
2212
+ const output = frameDifference(previous, frame);
2213
+ if (output === "") return;
2214
+ previous = frame;
2215
+ if (!port.write(output)) {
2216
+ unsubscribe = port.onDrain(() => {
2217
+ unsubscribe?.();
2218
+ unsubscribe = void 0;
2219
+ flush();
2220
+ });
2221
+ }
2222
+ };
2223
+ const refresh = setInterval(() => {
2224
+ previous = void 0;
2225
+ pending = latest;
2226
+ flush();
2227
+ }, 1e3);
2228
+ refresh.unref();
2229
+ return {
2230
+ present(frame) {
2231
+ if (closed) return;
2232
+ latest = pending = frame;
2233
+ flush();
2234
+ },
2235
+ invalidate() {
2236
+ previous = void 0;
2237
+ },
2238
+ close() {
2239
+ closed = true;
2240
+ clearInterval(refresh);
2241
+ unsubscribe?.();
2242
+ unsubscribe = void 0;
2243
+ pending = latest = previous = void 0;
2244
+ }
2245
+ };
2246
+ }
2247
+
1846
2248
  // src/watch/pane-state.ts
1847
2249
  function describePageFault(fault) {
1848
2250
  return fault.kind === "unreadable" ? `cannot read ${fault.path}: ${fault.detail}` : describeStoreError(fault);
@@ -2011,13 +2413,14 @@ function describeArgsError(e) {
2011
2413
  }
2012
2414
  var USAGE = "usage: mellos-mapping-watch [--file <map.json>] [--page <slug>] [--interval <ms>] [--ascii] [--no-color] [--no-mouse] [--no-follow]";
2013
2415
  function parseArgs(argv, cwd) {
2014
- let file = join2(cwd, STATE_FILE_RELATIVE_PATH);
2416
+ let file = join8(resolveProjectDirectory(cwd), STATE_FILE_RELATIVE_PATH);
2015
2417
  let intervalMs = POLL_INTERVAL_DEFAULT_MS;
2016
2418
  let unicode = true;
2017
2419
  let color = true;
2018
2420
  let mouse = true;
2019
2421
  let page;
2020
2422
  let follow = true;
2423
+ let owner;
2021
2424
  const valueOf = (flag, raw) => raw === void 0 || raw.startsWith("--") ? err({ kind: "missing-value", flag }) : ok(raw);
2022
2425
  for (let i = 0; i < argv.length; i++) {
2023
2426
  const flag = argv[i];
@@ -2031,9 +2434,17 @@ function parseArgs(argv, cwd) {
2031
2434
  case "--page": {
2032
2435
  const value = valueOf(flag, argv[++i]);
2033
2436
  if (!value.ok) return value;
2034
- const parsed = makePageId(value.value);
2035
- if (!parsed.ok) return err({ kind: "invalid-value", flag, raw: value.value, rule: parsed.error.rule });
2036
- page = parsed.value;
2437
+ const parsed2 = makePageId(value.value);
2438
+ if (!parsed2.ok) return err({ kind: "invalid-value", flag, raw: value.value, rule: parsed2.error.rule });
2439
+ page = parsed2.value;
2440
+ break;
2441
+ }
2442
+ case "--owner": {
2443
+ const value = valueOf(flag, argv[++i]);
2444
+ if (!value.ok) return value;
2445
+ const parsed2 = makePageId(value.value);
2446
+ if (!parsed2.ok) return err({ kind: "invalid-value", flag, raw: value.value, rule: parsed2.error.rule });
2447
+ owner = parsed2.value;
2037
2448
  break;
2038
2449
  }
2039
2450
  case "--interval": {
@@ -2062,7 +2473,7 @@ function parseArgs(argv, cwd) {
2062
2473
  return err({ kind: "unknown-flag", flag });
2063
2474
  }
2064
2475
  }
2065
- return ok({ file, intervalMs, unicode, color, mouse, page, follow });
2476
+ return ok({ file, intervalMs, unicode, color, mouse, page, follow, ...owner === void 0 ? {} : { owner } });
2066
2477
  }
2067
2478
  function readPage(file) {
2068
2479
  try {
@@ -2071,9 +2482,9 @@ function readPage(file) {
2071
2482
  return err({ kind: "unreadable", path: file, detail: e.code ?? e.message });
2072
2483
  }
2073
2484
  }
2074
- function renderWindow(map, opts, viewport) {
2485
+ function renderWindow(map, opts, viewport, render = renderMapWindow) {
2075
2486
  try {
2076
- return ok(renderMapWindow(map, opts, viewport));
2487
+ return ok(render(map, opts, viewport));
2077
2488
  } catch (e) {
2078
2489
  return err(`this map could not be drawn: ${e.message}`);
2079
2490
  }
@@ -2090,17 +2501,10 @@ function dividerRow(width, unicode, follow) {
2090
2501
  }
2091
2502
  return bar;
2092
2503
  }
2093
- var HIDE_CURSOR = "\x1B[?25l";
2094
- var SHOW_CURSOR = "\x1B[?25h";
2095
2504
  var CLEAR_ALL = "\x1B[H\x1B[2J";
2096
2505
  var HOME = "\x1B[H";
2097
2506
  var ERASE_LINE_END = "\x1B[K";
2098
- var MOUSE_ON = "\x1B[?1003h\x1B[?1006h";
2099
- var MOUSE_OFF = "\x1B[?1003l\x1B[?1006l";
2100
- var RESET = "\x1B[0m";
2101
- function terminalRestoreSequence(mouseActive) {
2102
- return (mouseActive ? MOUSE_OFF : "") + SHOW_CURSOR + RESET + "\n";
2103
- }
2507
+ var RESET2 = "\x1B[0m";
2104
2508
  var PANEL_CONTENT_ROWS = 6;
2105
2509
  var PANEL_ROWS_MIN = 2;
2106
2510
  var MAP_ROWS_MIN = 4;
@@ -2291,7 +2695,7 @@ function waitingInfo(s, width) {
2291
2695
  const clock = s.elapsedMs !== void 0 ? ` \xB7 waiting ${elapsedLabel(s.elapsedMs)}` : "";
2292
2696
  const lines = [
2293
2697
  `watching ${s.defaultFile}`,
2294
- ` and ${join2(s.pagesDir, "*.json")}`,
2698
+ ` and ${join8(s.pagesDir, "*.json")}`,
2295
2699
  `polling every ${s.intervalMs} ms${clock}`
2296
2700
  ];
2297
2701
  for (const b of s.broken) lines.push(`! ${b}`);
@@ -2300,7 +2704,7 @@ function waitingInfo(s, width) {
2300
2704
  }
2301
2705
  function splashFrame(notice, info, frame, width, height, unicode, color) {
2302
2706
  if (width < 24 || height < info.length + 3) return void 0;
2303
- const dim = (s) => color ? `\x1B[90m${s}${RESET}` : s;
2707
+ const dim = (s) => color ? `\x1B[90m${s}${RESET2}` : s;
2304
2708
  const spinner = SPINNER_FRAMES[unicode ? "unicode" : "ascii"];
2305
2709
  const status = fitWidth(`${spinner[frame % spinner.length]} ${notice}`, Math.max(1, width - 2));
2306
2710
  const statusIndent = " ".repeat(Math.max(0, Math.floor((width - displayWidth(status)) / 2)));
@@ -2331,17 +2735,12 @@ function viewerReportOf(pane, defaultFile) {
2331
2735
  const shown = pane.activeFile ?? pane.pendingFocusFile ?? defaultFile;
2332
2736
  return { page: pageIdOfFile(defaultFile, shown), follow: pane.follow };
2333
2737
  }
2334
- function main() {
2335
- const parsed = parseArgs(process.argv.slice(2), process.cwd());
2336
- if (!parsed.ok) {
2337
- console.error(`mellos-mapping-watch: ${describeArgsError(parsed.error)}
2338
- ${USAGE}`);
2339
- process.exit(1);
2340
- }
2341
- const cfg = parsed.value;
2738
+ function runWatcher(cfg, io = nativeWatcherIO()) {
2739
+ const renderScene = createWindowRenderer();
2342
2740
  if (migrateLegacyStore(cfg.file)) console.error("mellos-mapping: moved the legacy .claude map store to .mellos/ \u2014 commit the move.");
2343
2741
  sweepQuitRequest(cfg.file);
2344
- const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
2742
+ sweepQuitRequest(cfg.file, process.pid);
2743
+ const interactive = io.interactive;
2345
2744
  const mouseActive = interactive && cfg.mouse;
2346
2745
  let lastFrame = "";
2347
2746
  let spinnerFrame = 0;
@@ -2350,28 +2749,19 @@ ${USAGE}`);
2350
2749
  const standbyNotice = "waiting for the first mmap_declare ...";
2351
2750
  let map;
2352
2751
  let notice = standbyNotice;
2353
- let lastCols = process.stdout.columns ?? 0;
2354
- let lastRows = process.stdout.rows ?? 0;
2752
+ let lastCols = io.output.columns ?? 0;
2753
+ let lastRows = io.output.rows ?? 0;
2355
2754
  let pane = initialPaneState(
2356
2755
  cfg.follow,
2357
2756
  cfg.page === void 0 ? void 0 : pageFilePath(cfg.file, cfg.page)
2358
2757
  );
2359
- const pageViews = /* @__PURE__ */ new Map();
2758
+ let view = initialViewState();
2360
2759
  let lastTabSegments = [];
2361
2760
  let tabScroll = 0;
2362
- let offsetX = 0;
2363
- let offsetY = 0;
2364
- let zoom = ZOOM_DEFAULT;
2365
- let dragAnchor;
2366
- let press;
2367
- let hoverId;
2368
- let selectedId;
2369
2761
  let lastHits = [];
2370
2762
  let lastContent = { w: 0, h: 0 };
2371
2763
  let pendingInput = "";
2372
2764
  let panelContentRows = PANEL_CONTENT_ROWS;
2373
- let dividerDrag = false;
2374
- let lastClick;
2375
2765
  let flash;
2376
2766
  let lastTabFiles = [];
2377
2767
  const topFiles = () => topLevelFiles(cfg.file, filesOf(pane), mapsOf(pane));
@@ -2387,8 +2777,8 @@ ${USAGE}`);
2387
2777
  }
2388
2778
  return false;
2389
2779
  };
2390
- const viewWidth = () => usableColumns(process.stdout.columns ?? FALLBACK_COLUMNS);
2391
- const viewHeight = () => Math.max(1, (process.stdout.rows ?? FALLBACK_ROWS) - (1 + panelContentRows) - 1 - tabRows());
2780
+ const viewWidth = () => usableColumns(io.output.columns ?? FALLBACK_COLUMNS);
2781
+ const viewHeight = () => Math.max(1, (io.output.rows ?? FALLBACK_ROWS) - (1 + panelContentRows) - 1 - tabRows());
2392
2782
  const dividerY = () => tabRows() + viewHeight() + 1;
2393
2783
  const pageTabsOf = (files) => files.map((f) => {
2394
2784
  const entry = entryOf(pane, f);
@@ -2415,14 +2805,8 @@ ${USAGE}`);
2415
2805
  };
2416
2806
  const adoptView = (previous) => {
2417
2807
  const file = pane.activeFile;
2808
+ view = reduceView(view, { kind: "pages", previous, active: file, files: filesOf(pane) });
2418
2809
  if (file === void 0 || file === previous) return;
2419
- if (previous !== void 0) pageViews.set(previous, { offsetX, offsetY, zoom, selectedId });
2420
- const view = pageViews.get(file);
2421
- offsetX = view?.offsetX ?? 0;
2422
- offsetY = view?.offsetY ?? 0;
2423
- zoom = view?.zoom ?? ZOOM_DEFAULT;
2424
- selectedId = view?.selectedId;
2425
- hoverId = void 0;
2426
2810
  const top = topFiles();
2427
2811
  const tabIndex = top.indexOf(file);
2428
2812
  if (tabIndex >= 0) {
@@ -2444,16 +2828,33 @@ ${USAGE}`);
2444
2828
  const sy = termY - 1 - tabRows();
2445
2829
  if (sx < 0 || sx >= viewWidth()) return void 0;
2446
2830
  if (sy < 0 || sy >= viewHeight()) return void 0;
2447
- const cx = sx + offsetX;
2448
- const cy = sy + offsetY;
2831
+ const cx = sx + view.offsetX;
2832
+ const cy = sy + view.offsetY;
2449
2833
  return lastHits.find((h) => cx >= h.x && cx < h.x + h.w && cy >= h.y && cy < h.y + h.h)?.id;
2450
2834
  };
2451
2835
  const publishPresence = () => {
2452
- publishViewer(cfg.file, process.pid, viewerReportOf(pane, cfg.file));
2836
+ io.report?.(viewerReportOf(pane, cfg.file));
2837
+ publishViewer(cfg.file, process.pid, {
2838
+ ...viewerReportOf(pane, cfg.file),
2839
+ ...cfg.owner === void 0 ? {} : { owner: cfg.owner }
2840
+ });
2453
2841
  };
2454
- process.stdout.write(HIDE_CURSOR + CLEAR_ALL + (mouseActive ? MOUSE_ON : ""));
2842
+ if (interactive) io.input.setRawMode(true);
2843
+ const terminal = openTerminalSession({ interactive, mouse: cfg.mouse, write: (text) => io.output.write(text) });
2844
+ const frameOutput = interactive ? createFrameOutput({
2845
+ write: (text) => io.output.write(text),
2846
+ onDrain: (ready) => {
2847
+ io.output.once("drain", ready);
2848
+ return () => {
2849
+ io.output.off("drain", ready);
2850
+ };
2851
+ }
2852
+ }) : void 0;
2853
+ let paintTimer;
2455
2854
  process.on("exit", () => {
2456
- process.stdout.write(terminalRestoreSequence(mouseActive));
2855
+ clearTimeout(paintTimer);
2856
+ frameOutput?.close();
2857
+ terminal.close();
2457
2858
  retireViewer(cfg.file, process.pid);
2458
2859
  });
2459
2860
  const quit = () => process.exit(0);
@@ -2465,20 +2866,31 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2465
2866
  `);
2466
2867
  process.exit(1);
2467
2868
  });
2869
+ const sceneOptions = () => ({
2870
+ color: cfg.color,
2871
+ unicode: cfg.unicode,
2872
+ zoom: view.zoom,
2873
+ focus: view.hoverId ?? view.selectedId,
2874
+ // A spinner on another page must not invalidate this completed picture.
2875
+ spinnerFrame: map?.nodes.some((node) => node.status === "in-progress") ? spinnerFrame : 0
2876
+ });
2468
2877
  const paint2 = () => {
2469
- const cols = process.stdout.columns ?? FALLBACK_COLUMNS;
2878
+ clearTimeout(paintTimer);
2879
+ paintTimer = void 0;
2880
+ const cols = io.output.columns ?? FALLBACK_COLUMNS;
2470
2881
  const viewW = viewWidth();
2471
- panelContentRows = clampPanelRows(panelContentRows, process.stdout.rows ?? FALLBACK_ROWS, tabRows());
2882
+ panelContentRows = clampPanelRows(panelContentRows, io.output.rows ?? FALLBACK_ROWS, tabRows());
2472
2883
  const viewH = viewHeight();
2473
- const focus = hoverId ?? selectedId;
2884
+ const focus = view.hoverId ?? view.selectedId;
2474
2885
  let body;
2475
2886
  let panned = "";
2476
2887
  let pannable = false;
2477
2888
  if (map !== void 0) {
2478
2889
  const rendered = renderWindow(
2479
2890
  map,
2480
- { color: cfg.color, unicode: cfg.unicode, spinnerFrame, focus, zoom },
2481
- { x: offsetX, y: offsetY, width: viewW, height: viewH }
2891
+ sceneOptions(),
2892
+ { x: view.offsetX, y: view.offsetY, width: viewW, height: viewH },
2893
+ renderScene
2482
2894
  );
2483
2895
  if (!rendered.ok) {
2484
2896
  body = ["", fitWidth(` ! ${rendered.error}`, viewW), ""];
@@ -2487,9 +2899,12 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2487
2899
  const windowed = rendered.value;
2488
2900
  const maxX = Math.max(0, windowed.contentWidth - viewW);
2489
2901
  const maxY = Math.max(0, windowed.contentHeight - viewH);
2490
- if (offsetX > maxX || offsetY > maxY || offsetX < 0 || offsetY < 0) {
2491
- offsetX = Math.min(Math.max(0, offsetX), maxX);
2492
- offsetY = Math.min(Math.max(0, offsetY), maxY);
2902
+ if (view.offsetX > maxX || view.offsetY > maxY || view.offsetX < 0 || view.offsetY < 0) {
2903
+ view = reduceView(view, {
2904
+ kind: "position",
2905
+ x: Math.min(Math.max(0, view.offsetX), maxX),
2906
+ y: Math.min(Math.max(0, view.offsetY), maxY)
2907
+ });
2493
2908
  paint2();
2494
2909
  return;
2495
2910
  }
@@ -2497,13 +2912,13 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2497
2912
  body = windowed.lines;
2498
2913
  lastHits = windowed.hits;
2499
2914
  lastContent = { w: windowed.contentWidth, h: windowed.contentHeight };
2500
- if (offsetX !== 0 || offsetY !== 0) panned = ` (+${offsetX},+${offsetY})`;
2915
+ if (view.offsetX !== 0 || view.offsetY !== 0) panned = ` (+${view.offsetX},+${view.offsetY})`;
2501
2916
  }
2502
2917
  } else {
2503
2918
  const info = waitingInfo(
2504
2919
  {
2505
2920
  defaultFile: cfg.file,
2506
- pagesDir: join2(dirname2(cfg.file), PAGES_DIR_NAME),
2921
+ pagesDir: join8(dirname9(cfg.file), PAGES_DIR_NAME),
2507
2922
  intervalMs: cfg.intervalMs,
2508
2923
  elapsedMs: interactive ? Date.now() - startedAt : void 0,
2509
2924
  broken: pane.pages.flatMap(
@@ -2522,15 +2937,15 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2522
2937
  if (map === void 0) {
2523
2938
  panel = Array.from({ length: panelContentRows }, () => ({ text: "", sgr: "" }));
2524
2939
  } else if (focus !== void 0) {
2525
- panel = nodePanel(map, focus, cfg.unicode, panelWidth, selectedId === focus, panelContentRows) ?? mapPanel(map, cfg.unicode, panelWidth, panelContentRows);
2940
+ panel = nodePanel(map, focus, cfg.unicode, panelWidth, view.selectedId === focus, panelContentRows) ?? mapPanel(map, cfg.unicode, panelWidth, panelContentRows);
2526
2941
  } else {
2527
2942
  panel = mapPanel(map, cfg.unicode, panelWidth, panelContentRows);
2528
2943
  }
2529
2944
  const separator = dividerRow(viewW, cfg.unicode, pane.follow);
2530
2945
  const panelRows = [
2531
- cfg.color ? `\x1B[90m${separator}${RESET}` : separator,
2946
+ cfg.color ? `\x1B[90m${separator}${RESET2}` : separator,
2532
2947
  ...panel.map(
2533
- (l) => cfg.color && l.sgr !== "" && l.text !== "" ? ` \x1B[${l.sgr}m${l.text}${RESET}` : ` ${l.text}`
2948
+ (l) => cfg.color && l.sgr !== "" && l.text !== "" ? ` \x1B[${l.sgr}m${l.text}${RESET2}` : ` ${l.text}`
2534
2949
  )
2535
2950
  ];
2536
2951
  let tabLine;
@@ -2552,43 +2967,53 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2552
2967
  action: { kind: "back" }
2553
2968
  };
2554
2969
  lastTabSegments = [head, tail];
2555
- tabLine = lastTabSegments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET}` : s.text).join("");
2970
+ tabLine = lastTabSegments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET2}` : s.text).join("");
2556
2971
  } else if (tabRows() > 0) {
2557
2972
  const segments = pageTabRow(pageTabsOf(lastTabFiles), viewW, cfg.unicode, tabScroll, mouseActive);
2558
2973
  lastTabSegments = segments;
2559
- tabLine = segments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET}` : s.text).join("");
2974
+ tabLine = segments.map((s) => cfg.color && s.sgr !== "" ? `\x1B[${s.sgr}m${s.text}${RESET2}` : s.text).join("");
2560
2975
  } else {
2561
2976
  lastTabSegments = [];
2562
2977
  }
2563
- const zoomTag = `${cfg.unicode ? "\u2295" : "zoom"} ${zoomLabel(zoom)}`;
2978
+ const zoomTag = `${cfg.unicode ? "\u2295" : "zoom"} ${zoomLabel(view.zoom)}`;
2564
2979
  const hint = !interactive ? cfg.file : (flash !== void 0 ? `${flash.text} \xB7 ` : "") + `${zoomTag} \xB7 wheel zoom \xB7 ` + (pannable ? "drag pan \xB7 " : "") + "hover/click \xB7 0 reset \xB7 x delete page \xB7 q quit";
2565
2980
  const footerText = fitWidth(` ${hint}${panned}`, viewW);
2566
- const footer = cfg.color ? `\x1B[90m${footerText}${RESET}` : footerText;
2567
- let frame = HOME;
2568
- if (tabLine !== void 0) frame += tabLine + ERASE_LINE_END + "\n";
2569
- for (let i = 0; i < viewH; i++) frame += (body[i] ?? "") + ERASE_LINE_END + "\n";
2570
- for (const row of panelRows) frame += row + ERASE_LINE_END + "\n";
2571
- frame += footer + ERASE_LINE_END;
2572
- if (frame !== lastFrame) {
2573
- process.stdout.write(frame);
2574
- lastFrame = frame;
2981
+ const footer = cfg.color ? `\x1B[90m${footerText}${RESET2}` : footerText;
2982
+ const rows = [
2983
+ ...tabLine === void 0 ? [] : [tabLine],
2984
+ ...Array.from({ length: viewH }, (_, i) => body[i] ?? ""),
2985
+ ...panelRows,
2986
+ footer
2987
+ ];
2988
+ if (frameOutput !== void 0) {
2989
+ frameOutput.present({ columns: cols, rows });
2990
+ } else {
2991
+ const frame = HOME + rows.map((row) => row + ERASE_LINE_END).join("\n");
2992
+ if (frame !== lastFrame) {
2993
+ io.output.write(frame);
2994
+ lastFrame = frame;
2995
+ }
2575
2996
  }
2576
2997
  };
2998
+ const requestPaint = () => {
2999
+ paintTimer ??= setTimeout(paint2, 16);
3000
+ };
2577
3001
  const handleResize = () => {
2578
- lastCols = process.stdout.columns ?? lastCols;
2579
- lastRows = process.stdout.rows ?? lastRows;
3002
+ lastCols = io.output.columns ?? lastCols;
3003
+ lastRows = io.output.rows ?? lastRows;
2580
3004
  lastFrame = "";
2581
- process.stdout.write(CLEAR_ALL);
3005
+ frameOutput?.invalidate();
3006
+ io.output.write(CLEAR_ALL);
2582
3007
  paint2();
2583
3008
  };
2584
3009
  const tick = () => {
2585
- if (takeQuitRequest(cfg.file)) quit();
2586
- if ((process.stdout.columns ?? lastCols) !== lastCols || (process.stdout.rows ?? lastRows) !== lastRows) {
3010
+ if (takeQuitRequest(cfg.file, process.pid)) quit();
3011
+ if ((io.output.columns ?? lastCols) !== lastCols || (io.output.rows ?? lastRows) !== lastRows) {
2587
3012
  handleResize();
2588
3013
  }
2589
3014
  const discovered = listPageFiles(cfg.file);
2590
3015
  const files = discovered.length > 0 ? discovered : [cfg.file];
2591
- const request = takeFocusRequest(cfg.file);
3016
+ const request = takeFocusRequest(cfg.file, process.pid);
2592
3017
  const previous = pane.activeFile;
2593
3018
  const scanned = scan(pane, {
2594
3019
  files,
@@ -2603,12 +3028,9 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2603
3028
  focusRequest: request === void 0 ? void 0 : pageFilePath(cfg.file, request.page),
2604
3029
  // a drag in progress holds auto-follow off: the user is engaged with
2605
3030
  // THIS page, and a missed switch is re-triggered by the next save
2606
- engaged: dragAnchor !== void 0
3031
+ engaged: view.dragAnchor !== void 0
2607
3032
  });
2608
3033
  pane = scanned.state;
2609
- for (const known of [...pageViews.keys()]) {
2610
- if (!files.includes(known)) pageViews.delete(known);
2611
- }
2612
3034
  adoptView(previous);
2613
3035
  adoptPage();
2614
3036
  if (flash?.confirm === true && pane.pendingDelete === void 0) flash = void 0;
@@ -2644,15 +3066,20 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2644
3066
  return;
2645
3067
  }
2646
3068
  const file = asked.request.file;
2647
- const removed = deletePageFile(file);
3069
+ let removed;
3070
+ try {
3071
+ removed = withStoreLock(file, () => deletePageFile(file));
3072
+ } catch (error) {
3073
+ flash = { text: String(error), until: now + FLASH_NOTICE_MS };
3074
+ return;
3075
+ }
2648
3076
  flash = removed.ok ? { text: `deleted ${pageName(file)}`, until: now + FLASH_ACK_MS } : { text: describeStoreError(removed.error), until: now + FLASH_NOTICE_MS };
2649
3077
  tick();
2650
3078
  };
2651
3079
  if (interactive) {
2652
- process.stdin.setRawMode(true);
2653
- process.stdin.resume();
2654
- process.stdin.setEncoding("utf8");
2655
- process.stdin.on("data", (chunk) => {
3080
+ io.input.resume();
3081
+ io.input.setEncoding("utf8");
3082
+ io.input.on("data", (chunk) => {
2656
3083
  const parsed2 = parseInput(pendingInput + chunk);
2657
3084
  pendingInput = parsed2.rest;
2658
3085
  let dirty = false;
@@ -2662,22 +3089,19 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2662
3089
  quit();
2663
3090
  return;
2664
3091
  case "reset":
2665
- offsetX = 0;
2666
- offsetY = 0;
2667
- zoom = ZOOM_DEFAULT;
3092
+ view = reduceView(view, { kind: "reset" });
2668
3093
  dirty = true;
2669
3094
  break;
2670
3095
  case "clear":
2671
3096
  if (pane.pendingDelete !== void 0) {
2672
3097
  pane = disarmDelete(pane);
2673
3098
  flash = void 0;
2674
- } else if (selectedId !== void 0) selectedId = void 0;
3099
+ } else if (view.selectedId !== void 0) view = reduceView(view, { kind: "select", id: void 0 });
2675
3100
  else climbBack();
2676
3101
  dirty = true;
2677
3102
  break;
2678
3103
  case "pan":
2679
- offsetX += event.dx;
2680
- offsetY += event.dy;
3104
+ view = reduceView(view, event);
2681
3105
  dirty = true;
2682
3106
  break;
2683
3107
  case "zoom": {
@@ -2686,15 +3110,16 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2686
3110
  dirty = true;
2687
3111
  break;
2688
3112
  }
2689
- const next = clampZoom(zoom + event.delta);
2690
- if (next === zoom || map === void 0) break;
2691
- const anchorId = hoverId ?? selectedId ?? nearestHit(lastHits, offsetX + viewWidth() / 2, offsetY + viewHeight() / 2)?.id;
3113
+ const next = clampZoom(view.zoom + event.delta);
3114
+ if (next === view.zoom || map === void 0) break;
3115
+ const anchorId = view.hoverId ?? view.selectedId ?? nearestHit(lastHits, view.offsetX + viewWidth() / 2, view.offsetY + viewHeight() / 2)?.id;
2692
3116
  const before = lastHits.find((h) => h.id === anchorId);
2693
- zoom = next;
3117
+ view = reduceView(view, { kind: "zoom", value: next });
2694
3118
  const measured = renderWindow(
2695
3119
  map,
2696
- { color: false, unicode: cfg.unicode, spinnerFrame: 0, zoom },
2697
- { x: 0, y: 0, width: 0, height: 0 }
3120
+ sceneOptions(),
3121
+ { x: 0, y: 0, width: 0, height: 0 },
3122
+ renderScene
2698
3123
  );
2699
3124
  if (!measured.ok) {
2700
3125
  dirty = true;
@@ -2704,57 +3129,48 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2704
3129
  const after = before === void 0 ? void 0 : sized.hits.find((h) => h.id === before.id);
2705
3130
  const moved = anchorOffsets(
2706
3131
  before !== void 0 && after !== void 0 ? { before, after } : void 0,
2707
- { x: offsetX, y: offsetY },
3132
+ { x: view.offsetX, y: view.offsetY },
2708
3133
  lastContent,
2709
3134
  { w: sized.contentWidth, h: sized.contentHeight }
2710
3135
  );
2711
- offsetX = moved.x;
2712
- offsetY = moved.y;
3136
+ view = reduceView(view, { kind: "position", ...moved });
3137
+ lastHits = sized.hits;
3138
+ lastContent = { w: sized.contentWidth, h: sized.contentHeight };
2713
3139
  dirty = true;
2714
3140
  break;
2715
3141
  }
2716
3142
  case "mouse-move": {
2717
3143
  const over = hitTest(event.x, event.y);
2718
- if (over !== hoverId) {
2719
- hoverId = over;
3144
+ if (over !== view.hoverId) {
3145
+ view = reduceView(view, { kind: "hover", id: over });
2720
3146
  dirty = true;
2721
3147
  }
2722
3148
  break;
2723
3149
  }
2724
3150
  case "mouse-down":
2725
- if (event.y === dividerY()) {
2726
- dividerDrag = true;
2727
- break;
2728
- }
2729
- dragAnchor = { x: event.x, y: event.y, ox: offsetX, oy: offsetY };
2730
- press = { moved: false };
3151
+ view = reduceView(view, { kind: "down", x: event.x, y: event.y, divider: event.y === dividerY() });
2731
3152
  break;
2732
3153
  case "mouse-drag":
2733
- if (dividerDrag) {
2734
- const next = panelRowsFromDividerY(event.y, process.stdout.rows ?? FALLBACK_ROWS, tabRows());
3154
+ if (view.dividerDrag) {
3155
+ const next = panelRowsFromDividerY(event.y, io.output.rows ?? FALLBACK_ROWS, tabRows());
2735
3156
  if (next !== panelContentRows) {
2736
3157
  panelContentRows = next;
2737
3158
  dirty = true;
2738
3159
  }
2739
3160
  break;
2740
3161
  }
2741
- if (dragAnchor) {
2742
- const nx = dragAnchor.ox - (event.x - dragAnchor.x);
2743
- const ny = dragAnchor.oy - (event.y - dragAnchor.y);
2744
- if (nx !== offsetX || ny !== offsetY) {
2745
- offsetX = nx;
2746
- offsetY = ny;
2747
- if (press) press.moved = true;
2748
- dirty = true;
2749
- }
3162
+ {
3163
+ const nextView = reduceView(view, { kind: "drag", x: event.x, y: event.y });
3164
+ dirty ||= nextView !== view;
3165
+ view = nextView;
2750
3166
  }
2751
3167
  break;
2752
3168
  case "mouse-up":
2753
- if (dividerDrag) {
2754
- dividerDrag = false;
3169
+ if (view.dividerDrag) {
3170
+ view = reduceView(view, { kind: "up" });
2755
3171
  break;
2756
3172
  }
2757
- if (press && !press.moved) {
3173
+ if (isPointerClick(view)) {
2758
3174
  const tabHit = tabRows() > 0 && event.y === 1 ? lastTabSegments.find((s) => event.x >= s.lo && event.x <= s.hi) : void 0;
2759
3175
  if (tabHit !== void 0) {
2760
3176
  if (tabHit.action.kind === "back") {
@@ -2770,7 +3186,9 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2770
3186
  } else {
2771
3187
  const id = hitTest(event.x, event.y);
2772
3188
  const now = Date.now();
2773
- if (id !== void 0 && lastClick?.id === id && now - lastClick.at <= DOUBLE_CLICK_MS) {
3189
+ const clicked = clickNode(view, id, now, DOUBLE_CLICK_MS);
3190
+ view = clicked.state;
3191
+ if (clicked.diveId !== void 0) {
2774
3192
  const submap = map?.nodes.find((n) => n.id === id)?.submap;
2775
3193
  if (submap !== void 0 && pane.activeFile !== void 0) {
2776
3194
  const target = pageFilePath(cfg.file, submap);
@@ -2782,16 +3200,11 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2782
3200
  flash = { text: `submap "${submap}" has no page yet`, until: now + FLASH_ACK_MS };
2783
3201
  }
2784
3202
  }
2785
- lastClick = void 0;
2786
- } else {
2787
- lastClick = id !== void 0 ? { id, at: now } : void 0;
2788
3203
  }
2789
- selectedId = id;
2790
3204
  }
2791
3205
  dirty = true;
2792
3206
  }
2793
- dragAnchor = void 0;
2794
- press = void 0;
3207
+ view = reduceView(view, { kind: "up" });
2795
3208
  break;
2796
3209
  case "next-page":
2797
3210
  case "prev-page": {
@@ -2829,9 +3242,13 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2829
3242
  break;
2830
3243
  }
2831
3244
  }
2832
- if (dirty) paint2();
3245
+ if (dirty) {
3246
+ const motionOnly = parsed2.events.every((event) => event.kind === "mouse-move" || event.kind === "mouse-drag");
3247
+ if (motionOnly) requestPaint();
3248
+ else paint2();
3249
+ }
2833
3250
  });
2834
- process.stdout.on("resize", handleResize);
3251
+ io.output.on("resize", handleResize);
2835
3252
  }
2836
3253
  tick();
2837
3254
  setInterval(tick, cfg.intervalMs);
@@ -2845,43 +3262,12 @@ the map pane stopped: ${e instanceof Error ? e.stack ?? e.message : String(e)}
2845
3262
  }, SPLASH_FRAME_MS);
2846
3263
  }
2847
3264
  }
2848
- function launchedAsEntry(argv1, moduleUrl) {
2849
- if (argv1 === void 0) return false;
2850
- try {
2851
- return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
2852
- } catch {
2853
- return pathToFileURL(argv1).href === moduleUrl;
2854
- }
2855
- }
2856
- if (launchedAsEntry(process.argv[1], import.meta.url)) {
2857
- main();
2858
- }
2859
- export {
2860
- PANEL_ROWS_MIN,
2861
- USAGE,
2862
- anchorOffsets,
2863
- clampPanelRows,
2864
- describeArgsError,
2865
- describePageFault,
2866
- diveOrigin,
2867
- dividerRow,
2868
- elapsedLabel,
2869
- fitWidth,
2870
- launchedAsEntry,
2871
- mapPanel,
2872
- nearestHit,
2873
- nodePanel,
2874
- pageTabRow,
2875
- panelRowsFromDividerY,
2876
- parseArgs,
2877
- readPage,
2878
- renderWindow,
2879
- splashFrame,
2880
- tabScrollFor,
2881
- terminalRestoreSequence,
2882
- topLevelFiles,
2883
- usableColumns,
2884
- viewerReportOf,
2885
- waitingInfo,
2886
- wrapWidth
2887
- };
3265
+
3266
+ // src/watch/cli.ts
3267
+ var parsed = parseArgs(process.argv.slice(2), process.cwd());
3268
+ if (!parsed.ok) {
3269
+ console.error(`mellos-mapping-watch: ${describeArgsError(parsed.error)}
3270
+ ${USAGE}`);
3271
+ process.exit(1);
3272
+ }
3273
+ runWatcher(parsed.value);