mellos-mapping 0.20.2 → 0.22.1

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 (68) hide show
  1. package/README.md +125 -88
  2. package/README.zh-CN.md +122 -84
  3. package/dist/hook-session-start.mjs +25 -19
  4. package/dist/mmap.mjs +302 -170
  5. package/dist/preview.mjs +1418 -0
  6. package/dist/server.mjs +1198 -400
  7. package/dist/store-paths.mjs +40 -21
  8. package/dist/terminal-worker.mjs +3188 -0
  9. package/dist/watch.mjs +523 -280
  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 +5535 -0
  19. package/docs/codex.md +183 -0
  20. package/lib/domain/text.d.ts +9 -0
  21. package/lib/domain/text.js +43 -0
  22. package/lib/domain/types.js +10 -1
  23. package/lib/preview/index.d.ts +3 -0
  24. package/lib/preview/index.js +3 -0
  25. package/lib/preview/markdown.d.ts +8 -0
  26. package/lib/preview/markdown.js +54 -0
  27. package/lib/preview/presentation.d.ts +6 -0
  28. package/lib/preview/presentation.js +14 -0
  29. package/lib/preview/publisher.d.ts +23 -0
  30. package/lib/preview/publisher.js +143 -0
  31. package/lib/preview/svg.d.ts +3 -0
  32. package/lib/preview/svg.js +74 -0
  33. package/lib/preview/text.d.ts +4 -0
  34. package/lib/preview/text.js +13 -0
  35. package/lib/render/canvas.d.ts +1 -1
  36. package/lib/render/canvas.js +4 -2
  37. package/lib/render/draw.js +9 -6
  38. package/lib/render/render.d.ts +6 -0
  39. package/lib/render/render.js +50 -18
  40. package/lib/render/width.js +3 -1
  41. package/lib/store/atomic.d.ts +18 -0
  42. package/lib/store/atomic.js +86 -0
  43. package/lib/store/channels.d.ts +40 -0
  44. package/lib/store/channels.js +135 -0
  45. package/lib/store/format.js +3 -1
  46. package/lib/store/json-text.d.ts +9 -0
  47. package/lib/store/json-text.js +16 -0
  48. package/lib/store/maps.d.ts +12 -0
  49. package/lib/store/maps.js +42 -0
  50. package/lib/store/migration.d.ts +12 -0
  51. package/lib/store/migration.js +46 -0
  52. package/lib/store/pages.d.ts +46 -0
  53. package/lib/store/pages.js +89 -0
  54. package/lib/store/policy.d.ts +74 -0
  55. package/lib/store/policy.js +144 -0
  56. package/lib/store/store.d.ts +9 -256
  57. package/lib/store/store.js +10 -694
  58. package/lib/store/viewers.d.ts +81 -0
  59. package/lib/store/viewers.js +186 -0
  60. package/package.json +25 -6
  61. package/scripts/codex-cli.mjs +42 -0
  62. package/scripts/codex-register.mjs +27 -94
  63. package/scripts/mmap.mjs +26 -16
  64. package/scripts/open-pane.mjs +37 -18
  65. package/scripts/pane-core.mjs +57 -220
  66. package/scripts/terminal-session.mjs +137 -0
  67. package/scripts/tmux-session.mjs +90 -0
  68. package/scripts/watcher-command.mjs +16 -0
@@ -0,0 +1,46 @@
1
+ import { type Result } from '../domain/types.js';
2
+ import { type StoreError } from './format.js';
3
+ import { type PageId } from './format.js';
4
+ /**
5
+ * The directory a store lives in, under a project root or under a user's home.
6
+ * It is tool-owned: the map belongs to mellos-mapping, not to whichever host
7
+ * (Claude Code, Codex, a harness) happens to drive the server, so no host
8
+ * brand appears in the path. Pre-0.20 stores under `.claude/` are moved once
9
+ * by {@link migrateLegacyStore}.
10
+ */
11
+ export declare const STORE_DIR_NAME = ".mellos";
12
+ /** Project-relative location of the DEFAULT page's state file. */
13
+ export declare const STATE_FILE_RELATIVE_PATH: string;
14
+ /** Directory (next to the default file) holding the named pages. */
15
+ export declare const PAGES_DIR_NAME = "pages";
16
+ /** Where a page's map file lives, given the default page's file path. */
17
+ export declare function pageFilePath(defaultFile: string, page?: PageId): string;
18
+ /** The page id a file path denotes; undefined = the default page. */
19
+ export declare function pageIdOfFile(defaultFile: string, path: string): PageId | undefined;
20
+ /** Existing page files: the default page first (when present), then named pages sorted by slug. */
21
+ export declare function listPageFiles(defaultFile: string): string[];
22
+ /**
23
+ * Delete one page's file — a named page, or the DEFAULT page (whose file is
24
+ * optional by design, so removing it is a legal state, not a mutilation).
25
+ *
26
+ * Preconditions: none. Postcondition on ok: no file at `path` — an already
27
+ * absent one is ok too, because the goal state is what is promised, not the
28
+ * act. Postcondition on error: the file is still there and the caller may
29
+ * retry or report; the errno is carried in the detail.
30
+ *
31
+ * Concurrency, stated plainly: deletion races a concurrent writer and THE
32
+ * WRITER WINS. A server saving that page while this runs simply recreates the
33
+ * file (its rename is atomic and needs no existing target), so the page comes
34
+ * back. That is accepted rather than defended against — the store has no
35
+ * lost-update protection anywhere (see the module header), and locking one
36
+ * operation would only make the race rarer, never absent, while claiming
37
+ * otherwise. Pages are the isolation unit: nobody deletes a page another
38
+ * session is writing.
39
+ *
40
+ * What it deliberately does NOT do: sweep `<path>.<pid>.<random>.tmp`
41
+ * siblings. Those temps are private to a save IN FLIGHT, and a live writer
42
+ * whose temp vanished would fail its rename — turning a harmless leftover
43
+ * into a broken save. A stray temp only exists when a write failed AND its
44
+ * own cleanup failed; it is inert, and the README documents it.
45
+ */
46
+ export declare function deletePageFile(path: string): Result<void, StoreError>;
@@ -0,0 +1,89 @@
1
+ import { existsSync, readdirSync, rmSync } from 'node:fs';
2
+ import { basename, dirname, join } from 'node:path';
3
+ import { err, ok } from '../domain/types.js';
4
+ import { errnoOf } from './atomic.js';
5
+ /**
6
+ * The directory a store lives in, under a project root or under a user's home.
7
+ * It is tool-owned: the map belongs to mellos-mapping, not to whichever host
8
+ * (Claude Code, Codex, a harness) happens to drive the server, so no host
9
+ * brand appears in the path. Pre-0.20 stores under `.claude/` are moved once
10
+ * by {@link migrateLegacyStore}.
11
+ */
12
+ export const STORE_DIR_NAME = '.mellos';
13
+ /** Project-relative location of the DEFAULT page's state file. */
14
+ export const STATE_FILE_RELATIVE_PATH = join(STORE_DIR_NAME, 'map.json');
15
+ // ---------------------------------------------------------------------------
16
+ // pages — a project may keep several maps side by side (one effort = one page)
17
+ // ---------------------------------------------------------------------------
18
+ //
19
+ // The default page IS the classic map.json. Named pages live in a sibling
20
+ // directory, one file each: file-per-page keeps concurrent sessions isolated —
21
+ // two writers on two pages can never clobber each other, because every save
22
+ // renames a whole file.
23
+ /** Directory (next to the default file) holding the named pages. */
24
+ export const PAGES_DIR_NAME = 'pages';
25
+ /** Where a page's map file lives, given the default page's file path. */
26
+ export function pageFilePath(defaultFile, page) {
27
+ return page === undefined ? defaultFile : join(dirname(defaultFile), PAGES_DIR_NAME, `${page}.json`);
28
+ }
29
+ /** The page id a file path denotes; undefined = the default page. */
30
+ export function pageIdOfFile(defaultFile, path) {
31
+ if (path === defaultFile)
32
+ return undefined;
33
+ const name = basename(path);
34
+ return name.endsWith('.json') ? name.slice(0, -'.json'.length) : name;
35
+ }
36
+ /** Existing page files: the default page first (when present), then named pages sorted by slug. */
37
+ export function listPageFiles(defaultFile) {
38
+ const out = [];
39
+ if (existsSync(defaultFile))
40
+ out.push(defaultFile);
41
+ let entries = [];
42
+ try {
43
+ entries = readdirSync(join(dirname(defaultFile), PAGES_DIR_NAME));
44
+ }
45
+ catch {
46
+ // no pages directory — a single-page project, the common case
47
+ }
48
+ for (const e of entries.sort()) {
49
+ if (e.endsWith('.json'))
50
+ out.push(join(dirname(defaultFile), PAGES_DIR_NAME, e));
51
+ }
52
+ return out;
53
+ }
54
+ /**
55
+ * Delete one page's file — a named page, or the DEFAULT page (whose file is
56
+ * optional by design, so removing it is a legal state, not a mutilation).
57
+ *
58
+ * Preconditions: none. Postcondition on ok: no file at `path` — an already
59
+ * absent one is ok too, because the goal state is what is promised, not the
60
+ * act. Postcondition on error: the file is still there and the caller may
61
+ * retry or report; the errno is carried in the detail.
62
+ *
63
+ * Concurrency, stated plainly: deletion races a concurrent writer and THE
64
+ * WRITER WINS. A server saving that page while this runs simply recreates the
65
+ * file (its rename is atomic and needs no existing target), so the page comes
66
+ * back. That is accepted rather than defended against — the store has no
67
+ * lost-update protection anywhere (see the module header), and locking one
68
+ * operation would only make the race rarer, never absent, while claiming
69
+ * otherwise. Pages are the isolation unit: nobody deletes a page another
70
+ * session is writing.
71
+ *
72
+ * What it deliberately does NOT do: sweep `<path>.<pid>.<random>.tmp`
73
+ * siblings. Those temps are private to a save IN FLIGHT, and a live writer
74
+ * whose temp vanished would fail its rename — turning a harmless leftover
75
+ * into a broken save. A stray temp only exists when a write failed AND its
76
+ * own cleanup failed; it is inert, and the README documents it.
77
+ */
78
+ export function deletePageFile(path) {
79
+ try {
80
+ // force: an absent file is the goal state already, not a failure.
81
+ // No `recursive`: a DIRECTORY where a page file belongs is a fault to
82
+ // report, never a tree to erase.
83
+ rmSync(path, { force: true });
84
+ return ok(undefined);
85
+ }
86
+ catch (e) {
87
+ return err({ kind: 'delete-failed', path, detail: errnoOf(e) });
88
+ }
89
+ }
@@ -0,0 +1,74 @@
1
+ import { type Result } from '../domain/types.js';
2
+ import { type StoreError } from './format.js';
3
+ /** Name of the file holding a mapping-policy configuration, in either scope. */
4
+ export declare const CONFIG_FILE_NAME = "config.json";
5
+ /** On-disk config format version. Bump only with a documented migration. */
6
+ export declare const CONFIG_FILE_VERSION = 1;
7
+ /** The PROJECT-scope configuration file: sibling of the default page. */
8
+ export declare function configFilePath(defaultFile: string): string;
9
+ /**
10
+ * The USER-scope configuration file: the same store directory name, under the
11
+ * user's own base directory.
12
+ *
13
+ * @param userBase - the user's home directory. Always passed in, never read
14
+ * from the environment down here: a function that reached for os.homedir()
15
+ * itself would make every spec a gamble on the developer's real
16
+ * configuration, and one of them would eventually write it. Entry points
17
+ * resolve the home once and hand it down.
18
+ */
19
+ export declare function userConfigFilePath(userBase: string): string;
20
+ export declare const MAPPING_POLICIES: readonly ["always", "complex", "on-request"];
21
+ /** How eagerly maps are opened; 'complex' is the behavior of an unconfigured project. */
22
+ export type MappingPolicy = (typeof MAPPING_POLICIES)[number];
23
+ export interface InvalidPolicy {
24
+ readonly kind: 'invalid-policy';
25
+ readonly raw: string;
26
+ readonly allowed: readonly string[];
27
+ }
28
+ export declare function makeMappingPolicy(raw: string): Result<MappingPolicy, InvalidPolicy>;
29
+ /** One line of meaning per policy — the wording every surface repeats. */
30
+ export declare function describeMappingPolicy(policy: MappingPolicy): string;
31
+ /**
32
+ * The policy recorded in ONE configuration file, or ok(undefined) when nobody
33
+ * has chosen there (missing file or missing key — both mean the same thing).
34
+ * A file that exists but does not parse is an error, never silently ignored.
35
+ *
36
+ * @param path - the configuration file itself: {@link configFilePath} for a
37
+ * project, {@link userConfigFilePath} for the user. One loader, two scopes.
38
+ */
39
+ export declare function loadMappingPolicy(path: string): Result<MappingPolicy | undefined, StoreError>;
40
+ /**
41
+ * Persist the policy atomically (P2), same write as the map files.
42
+ * @param path - the configuration file to write; see {@link loadMappingPolicy}.
43
+ * @returns ok when the file holds the policy; save-failed leaves the previous
44
+ * configuration in place.
45
+ */
46
+ export declare function saveMappingPolicy(path: string, policy: MappingPolicy): Result<void, StoreError>;
47
+ /** The two places a mapping policy can be recorded, in override order. */
48
+ export declare const POLICY_SCOPES: readonly ["user", "project"];
49
+ export type PolicyScope = (typeof POLICY_SCOPES)[number];
50
+ /** What both scopes say, and which of them actually governs. */
51
+ export interface MappingPolicyScopes {
52
+ readonly project: MappingPolicy | undefined;
53
+ readonly user: MappingPolicy | undefined;
54
+ /** What to act on. undefined = nobody has chosen yet, anywhere. */
55
+ readonly effective: MappingPolicy | undefined;
56
+ /** Where `effective` came from; undefined exactly when `effective` is. */
57
+ readonly source: PolicyScope | undefined;
58
+ }
59
+ /**
60
+ * Resolve the policy that governs this project: the PROJECT file if it names
61
+ * one, otherwise the USER file, otherwise nothing.
62
+ *
63
+ * Both scopes are reported, not just the winner — a surface that says "always"
64
+ * without saying where it came from cannot tell a user why changing their
65
+ * user-level choice did nothing here.
66
+ *
67
+ * @param projectConfigFile - see {@link configFilePath}.
68
+ * @param userConfigFile - see {@link userConfigFilePath}.
69
+ * @returns err as soon as EITHER file exists and is broken, project first: a
70
+ * configuration nobody can read is not the same as a configuration nobody
71
+ * wrote, and silently falling through to the other scope would act on a
72
+ * choice the user did not make.
73
+ */
74
+ export declare function effectiveMappingPolicy(projectConfigFile: string, userConfigFile: string): Result<MappingPolicyScopes, StoreError>;
@@ -0,0 +1,144 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { err, ok } from '../domain/types.js';
4
+ import { STORE_DIR_NAME } from './pages.js';
5
+ import { writeFileAtomic } from './atomic.js';
6
+ import { isRecord, stripBom } from './json-text.js';
7
+ // ---------------------------------------------------------------------------
8
+ // mapping policy — WHEN the assistant should open a map, chosen by the user
9
+ // ---------------------------------------------------------------------------
10
+ //
11
+ // Plugin configuration, not map data: it never enters a MellosMap and the
12
+ // ledger never enforces it (the ledger is not a judge). It lives in its own
13
+ // file so hand-editing or corrupting it can never touch a map.
14
+ //
15
+ // TWO SCOPES, one file format:
16
+ //
17
+ // user <home>/.mellos/config.json — the normal one. The question "when
18
+ // should maps open?" is about how somebody works, not about a
19
+ // particular repository, so it is asked ONCE, right after install,
20
+ // and answered for every project they will ever open.
21
+ // project <root>/.mellos/config.json — the override. A project that needs a
22
+ // different answer from its owner's habit says so, and wins.
23
+ //
24
+ // PROJECT beats USER wherever both are set (effectiveMappingPolicy); neither
25
+ // set means nobody has chosen yet, which is the one state that still prompts.
26
+ // Both are read and written by the same pair of functions, which take the
27
+ // CONFIG FILE PATH — not a map path — precisely so neither scope can grow a
28
+ // loader of its own.
29
+ /** Name of the file holding a mapping-policy configuration, in either scope. */
30
+ export const CONFIG_FILE_NAME = 'config.json';
31
+ /** On-disk config format version. Bump only with a documented migration. */
32
+ export const CONFIG_FILE_VERSION = 1;
33
+ /** The PROJECT-scope configuration file: sibling of the default page. */
34
+ export function configFilePath(defaultFile) {
35
+ return join(dirname(defaultFile), CONFIG_FILE_NAME);
36
+ }
37
+ /**
38
+ * The USER-scope configuration file: the same store directory name, under the
39
+ * user's own base directory.
40
+ *
41
+ * @param userBase - the user's home directory. Always passed in, never read
42
+ * from the environment down here: a function that reached for os.homedir()
43
+ * itself would make every spec a gamble on the developer's real
44
+ * configuration, and one of them would eventually write it. Entry points
45
+ * resolve the home once and hand it down.
46
+ */
47
+ export function userConfigFilePath(userBase) {
48
+ return join(userBase, STORE_DIR_NAME, CONFIG_FILE_NAME);
49
+ }
50
+ export const MAPPING_POLICIES = ['always', 'complex', 'on-request'];
51
+ export function makeMappingPolicy(raw) {
52
+ return MAPPING_POLICIES.includes(raw)
53
+ ? ok(raw)
54
+ : err({ kind: 'invalid-policy', raw, allowed: MAPPING_POLICIES });
55
+ }
56
+ /** One line of meaning per policy — the wording every surface repeats. */
57
+ export function describeMappingPolicy(policy) {
58
+ switch (policy) {
59
+ case 'always':
60
+ return 'map every structured task — workflows, designs, architecture, technical dependencies';
61
+ case 'complex':
62
+ return 'map only medium or complex tasks — several modules, a new subsystem, roughly an hour or more';
63
+ case 'on-request':
64
+ return 'map only when the user explicitly asks';
65
+ }
66
+ }
67
+ /**
68
+ * The policy recorded in ONE configuration file, or ok(undefined) when nobody
69
+ * has chosen there (missing file or missing key — both mean the same thing).
70
+ * A file that exists but does not parse is an error, never silently ignored.
71
+ *
72
+ * @param path - the configuration file itself: {@link configFilePath} for a
73
+ * project, {@link userConfigFilePath} for the user. One loader, two scopes.
74
+ */
75
+ export function loadMappingPolicy(path) {
76
+ let text;
77
+ try {
78
+ text = readFileSync(path, 'utf8');
79
+ }
80
+ catch (e) {
81
+ if (e.code === 'ENOENT')
82
+ return ok(undefined);
83
+ throw e; // unexpected I/O fault: fail fast, nothing meaningful to recover here
84
+ }
85
+ let raw;
86
+ try {
87
+ raw = JSON.parse(stripBom(text));
88
+ }
89
+ catch (e) {
90
+ return err({ kind: 'malformed-json', path, detail: e.message });
91
+ }
92
+ if (!isRecord(raw))
93
+ return err({ kind: 'bad-shape', path, detail: 'root is not an object' });
94
+ if (raw['version'] !== CONFIG_FILE_VERSION) {
95
+ return err({ kind: 'bad-shape', path, detail: `version is ${String(raw['version'])}, expected ${CONFIG_FILE_VERSION}` });
96
+ }
97
+ const rawPolicy = raw['policy'];
98
+ if (rawPolicy === undefined)
99
+ return ok(undefined);
100
+ if (typeof rawPolicy !== 'string')
101
+ return err({ kind: 'bad-shape', path, detail: 'policy is not a string' });
102
+ const policy = makeMappingPolicy(rawPolicy);
103
+ return policy.ok
104
+ ? ok(policy.value)
105
+ : err({ kind: 'bad-shape', path, detail: `policy is "${rawPolicy}", expected one of: ${MAPPING_POLICIES.join(' | ')}` });
106
+ }
107
+ /**
108
+ * Persist the policy atomically (P2), same write as the map files.
109
+ * @param path - the configuration file to write; see {@link loadMappingPolicy}.
110
+ * @returns ok when the file holds the policy; save-failed leaves the previous
111
+ * configuration in place.
112
+ */
113
+ export function saveMappingPolicy(path, policy) {
114
+ const body = JSON.stringify({ version: CONFIG_FILE_VERSION, policy }, null, 2) + '\n';
115
+ return writeFileAtomic(path, body);
116
+ }
117
+ /** The two places a mapping policy can be recorded, in override order. */
118
+ export const POLICY_SCOPES = ['user', 'project'];
119
+ /**
120
+ * Resolve the policy that governs this project: the PROJECT file if it names
121
+ * one, otherwise the USER file, otherwise nothing.
122
+ *
123
+ * Both scopes are reported, not just the winner — a surface that says "always"
124
+ * without saying where it came from cannot tell a user why changing their
125
+ * user-level choice did nothing here.
126
+ *
127
+ * @param projectConfigFile - see {@link configFilePath}.
128
+ * @param userConfigFile - see {@link userConfigFilePath}.
129
+ * @returns err as soon as EITHER file exists and is broken, project first: a
130
+ * configuration nobody can read is not the same as a configuration nobody
131
+ * wrote, and silently falling through to the other scope would act on a
132
+ * choice the user did not make.
133
+ */
134
+ export function effectiveMappingPolicy(projectConfigFile, userConfigFile) {
135
+ const project = loadMappingPolicy(projectConfigFile);
136
+ if (!project.ok)
137
+ return project;
138
+ const user = loadMappingPolicy(userConfigFile);
139
+ if (!user.ok)
140
+ return user;
141
+ const effective = project.value ?? user.value;
142
+ const source = project.value !== undefined ? 'project' : user.value !== undefined ? 'user' : undefined;
143
+ return ok({ project: project.value, user: user.value, effective, source });
144
+ }
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * The state file IS the event bus of the whole plugin: the MCP server writes
5
5
  * it, the terminal watcher polls it — and the watcher reports back what it is
6
- * showing (see viewers below), which is how a writer can tell whether anybody
7
- * is actually SEEING the map it is updating. The file FORMAT (version, page-id
6
+ * showing (see viewers below). These are process reports; terminal visibility
7
+ * is checked separately by the host adapter. The file FORMAT (version, page-id
8
8
  * grammar, parse/serialize with boundary validation) lives in ./format.ts,
9
9
  * pure of I/O so browsers can consume it; this module owns everything that
10
10
  * touches the filesystem, and one promise:
@@ -39,258 +39,11 @@
39
39
  * Node consumers import everything from here; the format surface is
40
40
  * re-exported so persistence has one import site per runtime.
41
41
  */
42
- import { type MellosMap, type Result } from '../domain/types.js';
43
- import { type PageId, type StoreError } from './format.js';
44
42
  export { STATE_FILE_VERSION, type PageId, makePageId, type StoreError, describeStoreError, parseMap, serializeMap, } from './format.js';
45
- /**
46
- * The directory a store lives in, under a project root or under a user's home.
47
- * It is tool-owned: the map belongs to mellos-mapping, not to whichever host
48
- * (Claude Code, Codex, a harness) happens to drive the server, so no host
49
- * brand appears in the path. Pre-0.20 stores under `.claude/` are moved once
50
- * by {@link migrateLegacyStore}.
51
- */
52
- export declare const STORE_DIR_NAME = ".mellos";
53
- /** Project-relative location of the DEFAULT page's state file. */
54
- export declare const STATE_FILE_RELATIVE_PATH: string;
55
- /** Directory (next to the default file) holding the named pages. */
56
- export declare const PAGES_DIR_NAME = "pages";
57
- /** Where a page's map file lives, given the default page's file path. */
58
- export declare function pageFilePath(defaultFile: string, page?: PageId): string;
59
- /** The page id a file path denotes; undefined = the default page. */
60
- export declare function pageIdOfFile(defaultFile: string, path: string): PageId | undefined;
61
- /** Existing page files: the default page first (when present), then named pages sorted by slug. */
62
- export declare function listPageFiles(defaultFile: string): string[];
63
- /**
64
- * Delete one page's file — a named page, or the DEFAULT page (whose file is
65
- * optional by design, so removing it is a legal state, not a mutilation).
66
- *
67
- * Preconditions: none. Postcondition on ok: no file at `path` — an already
68
- * absent one is ok too, because the goal state is what is promised, not the
69
- * act. Postcondition on error: the file is still there and the caller may
70
- * retry or report; the errno is carried in the detail.
71
- *
72
- * Concurrency, stated plainly: deletion races a concurrent writer and THE
73
- * WRITER WINS. A server saving that page while this runs simply recreates the
74
- * file (its rename is atomic and needs no existing target), so the page comes
75
- * back. That is accepted rather than defended against — the store has no
76
- * lost-update protection anywhere (see the module header), and locking one
77
- * operation would only make the race rarer, never absent, while claiming
78
- * otherwise. Pages are the isolation unit: nobody deletes a page another
79
- * session is writing.
80
- *
81
- * What it deliberately does NOT do: sweep `<path>.<pid>.<random>.tmp`
82
- * siblings. Those temps are private to a save IN FLIGHT, and a live writer
83
- * whose temp vanished would fail its rename — turning a harmless leftover
84
- * into a broken save. A stray temp only exists when a write failed AND its
85
- * own cleanup failed; it is inert, and the README documents it.
86
- */
87
- export declare function deletePageFile(path: string): Result<void, StoreError>;
88
- /** Sibling of the default file carrying a one-shot "show this page" request. */
89
- export declare const FOCUS_FILE_NAME = "focus";
90
- export declare function focusFilePath(defaultFile: string): string;
91
- /** A consumed focus request: the page to show (undefined = the default page). */
92
- export interface FocusRequest {
93
- readonly page: PageId | undefined;
94
- }
95
- /**
96
- * Consume a pending focus request: read it, delete the file, return it.
97
- * Absent file — the overwhelmingly common case — or junk content means no
98
- * request; the channel is best-effort and junk is swept by the same delete.
99
- */
100
- export declare function takeFocusRequest(defaultFile: string): FocusRequest | undefined;
101
- /** Sibling of the default file carrying a one-shot "close the pane" request. */
102
- export declare const QUIT_FILE_NAME = "quit";
103
- export declare function quitFilePath(defaultFile: string): string;
104
- /**
105
- * Consume a pending quit request: read it, delete the file, say whether there
106
- * was one. Absent file — the overwhelmingly common case — or content that is
107
- * not a JSON object means NO request; the channel is best-effort and junk is
108
- * swept by the same delete.
109
- *
110
- * The empty JSON object is the whole grammar. It exists so that a stray file
111
- * of this name — an editor backup, a half-written write from a foreign tool —
112
- * cannot take a live pane down by accident; a pane closing is the one thing
113
- * in this channel a user cannot undo by waiting.
114
- */
115
- export declare function takeQuitRequest(defaultFile: string): boolean;
116
- /**
117
- * Delete a quit request WITHOUT acting on it — the same file, read as a
118
- * leftover rather than as a message.
119
- *
120
- * A toggle that wrote the request and then lost its watcher (a crash, a
121
- * closed window, a `taskkill`) leaves the file behind, and the next pane to
122
- * open would consume it on its first tick and close instantly. The watcher
123
- * sweeps at STARTUP for exactly that: a request that predates the pane cannot
124
- * have been addressed to it. Best-effort, like every delete in this channel.
125
- */
126
- export declare function sweepQuitRequest(defaultFile: string): void;
127
- /** Directory (next to the default file) holding one report per live pane. */
128
- export declare const VIEWERS_DIR_NAME = "viewers";
129
- /** On-disk report format version. Bump only with a documented migration. */
130
- export declare const VIEWER_FILE_VERSION = 1;
131
- /**
132
- * How often a pane refreshes its report — the pane's timer, and the unit the
133
- * two thresholds below are counted in.
134
- */
135
- export declare const VIEWER_HEARTBEAT_MS = 1000;
136
- /**
137
- * A report older than this is not a pane, it is a pane's remains. Five missed
138
- * heartbeats: long enough to survive a garbage collection, a slow repaint or
139
- * a busy disk, short enough that a closed pane is known closed before anyone
140
- * asks twice.
141
- */
142
- export declare const VIEWER_STALE_MS = 5000;
143
- /**
144
- * A report this old is deleted on sight by whoever reads it. A pane that was
145
- * killed leaves its file behind forever, and a file that outlives every pane
146
- * would keep answering for one. The gap to VIEWER_STALE_MS is deliberate
147
- * slack: a machine that just woke from sleep has stale reports whose panes
148
- * are alive and about to beat again, and there is no reason to make them pay
149
- * for the sleep with a deleted file.
150
- */
151
- export declare const VIEWER_SWEEP_MS = 60000;
152
- /** Where a store's viewer reports live, given the default page's file path. */
153
- export declare function viewersDirPath(defaultFile: string): string;
154
- /** Where ONE pane's report lives. The pid in the name is the pane's identity. */
155
- export declare function viewerFilePath(defaultFile: string, pid: number): string;
156
- /** What a pane says about itself while it is up. */
157
- export interface ViewerReport {
158
- /** The page on screen right now; undefined = the default page. */
159
- readonly page: PageId | undefined;
160
- /** Auto-follow: whether the pane will switch to whatever page is written next. */
161
- readonly follow: boolean;
162
- }
163
- /** A report fresh enough to be a pane, with whose it is and how old. */
164
- export interface LiveViewer extends ViewerReport {
165
- readonly pid: number;
166
- /** Age of the report in ms — how long ago that pane last said anything. */
167
- readonly ageMs: number;
168
- }
169
- /**
170
- * Publish this pane's report, atomically (P2) — a reader polling the file
171
- * sees the previous whole report or the new one, never half of one.
172
- *
173
- * Preconditions: none; the viewers directory is created if missing.
174
- * Postcondition on ok: the pane's file holds `report` and its mtime is now,
175
- * which is what says the pane is alive. On error nothing about the pane is
176
- * claimed, and the next heartbeat is the whole recovery — so a caller reports
177
- * a failed beat at most once and keeps beating.
178
- */
179
- export declare function publishViewer(defaultFile: string, pid: number, report: ViewerReport): Result<void, StoreError>;
180
- /**
181
- * Take this pane's report back — the pane is going away and says so, rather
182
- * than leaving readers to wait out VIEWER_STALE_MS for the same conclusion.
183
- *
184
- * Best-effort by contract: an exit path is the worst place to raise, and a
185
- * report nobody could delete goes stale on its own within seconds.
186
- */
187
- export declare function retireViewer(defaultFile: string, pid: number): void;
188
- /**
189
- * Every pane currently showing this store, youngest report first.
190
- *
191
- * @param nowMs - the caller's clock (Date.now()), passed in so the ageing
192
- * rules can be tested without waiting for real seconds to pass.
193
- * @returns the live reports; an EMPTY array means nobody is seeing this map.
194
- *
195
- * Reading sweeps: a report past VIEWER_SWEEP_MS is deleted here, because the
196
- * pane that would have refreshed it is provably gone and no other code runs
197
- * often enough to notice. A report between stale and sweep is ignored but
198
- * kept — see VIEWER_SWEEP_MS. Nothing here has an opinion about WHOSE pane a
199
- * report is: a viewer started by hand counts exactly like one the launcher
200
- * opened, because the user can see both.
201
- */
202
- export declare function readLiveViewers(defaultFile: string, nowMs: number): readonly LiveViewer[];
203
- /** Name of the file holding a mapping-policy configuration, in either scope. */
204
- export declare const CONFIG_FILE_NAME = "config.json";
205
- /** On-disk config format version. Bump only with a documented migration. */
206
- export declare const CONFIG_FILE_VERSION = 1;
207
- /** The PROJECT-scope configuration file: sibling of the default page. */
208
- export declare function configFilePath(defaultFile: string): string;
209
- /**
210
- * The USER-scope configuration file: the same store directory name, under the
211
- * user's own base directory.
212
- *
213
- * @param userBase - the user's home directory. Always passed in, never read
214
- * from the environment down here: a function that reached for os.homedir()
215
- * itself would make every spec a gamble on the developer's real
216
- * configuration, and one of them would eventually write it. Entry points
217
- * resolve the home once and hand it down.
218
- */
219
- export declare function userConfigFilePath(userBase: string): string;
220
- export declare const MAPPING_POLICIES: readonly ["always", "complex", "on-request"];
221
- /** How eagerly maps are opened; 'complex' is the behavior of an unconfigured project. */
222
- export type MappingPolicy = (typeof MAPPING_POLICIES)[number];
223
- export interface InvalidPolicy {
224
- readonly kind: 'invalid-policy';
225
- readonly raw: string;
226
- readonly allowed: readonly string[];
227
- }
228
- export declare function makeMappingPolicy(raw: string): Result<MappingPolicy, InvalidPolicy>;
229
- /** One line of meaning per policy — the wording every surface repeats. */
230
- export declare function describeMappingPolicy(policy: MappingPolicy): string;
231
- /**
232
- * The policy recorded in ONE configuration file, or ok(undefined) when nobody
233
- * has chosen there (missing file or missing key — both mean the same thing).
234
- * A file that exists but does not parse is an error, never silently ignored.
235
- *
236
- * @param path - the configuration file itself: {@link configFilePath} for a
237
- * project, {@link userConfigFilePath} for the user. One loader, two scopes.
238
- */
239
- export declare function loadMappingPolicy(path: string): Result<MappingPolicy | undefined, StoreError>;
240
- /**
241
- * Persist the policy atomically (P2), same write as the map files.
242
- * @param path - the configuration file to write; see {@link loadMappingPolicy}.
243
- * @returns ok when the file holds the policy; save-failed leaves the previous
244
- * configuration in place.
245
- */
246
- export declare function saveMappingPolicy(path: string, policy: MappingPolicy): Result<void, StoreError>;
247
- /** The two places a mapping policy can be recorded, in override order. */
248
- export declare const POLICY_SCOPES: readonly ["user", "project"];
249
- export type PolicyScope = (typeof POLICY_SCOPES)[number];
250
- /** What both scopes say, and which of them actually governs. */
251
- export interface MappingPolicyScopes {
252
- readonly project: MappingPolicy | undefined;
253
- readonly user: MappingPolicy | undefined;
254
- /** What to act on. undefined = nobody has chosen yet, anywhere. */
255
- readonly effective: MappingPolicy | undefined;
256
- /** Where `effective` came from; undefined exactly when `effective` is. */
257
- readonly source: PolicyScope | undefined;
258
- }
259
- /**
260
- * Resolve the policy that governs this project: the PROJECT file if it names
261
- * one, otherwise the USER file, otherwise nothing.
262
- *
263
- * Both scopes are reported, not just the winner — a surface that says "always"
264
- * without saying where it came from cannot tell a user why changing their
265
- * user-level choice did nothing here.
266
- *
267
- * @param projectConfigFile - see {@link configFilePath}.
268
- * @param userConfigFile - see {@link userConfigFilePath}.
269
- * @returns err as soon as EITHER file exists and is broken, project first: a
270
- * configuration nobody can read is not the same as a configuration nobody
271
- * wrote, and silently falling through to the other scope would act on a
272
- * choice the user did not make.
273
- */
274
- export declare function effectiveMappingPolicy(projectConfigFile: string, userConfigFile: string): Result<MappingPolicyScopes, StoreError>;
275
- /** Project-relative location of the pre-0.20 default page file. */
276
- export declare const LEGACY_STATE_FILE_RELATIVE_PATH: string;
277
- /**
278
- * Move a legacy `.claude` store — map, pages, and mapping-policy config —
279
- * into the tool-owned `.mellos` location. Never merges: a project whose new
280
- * store already holds anything keeps it untouched, whatever the legacy
281
- * directory still contains.
282
- * @param defaultFile - the NEW default page path (`<root>/.mellos/map.json`);
283
- * the legacy store is looked up relative to `<root>`.
284
- * @returns whether a legacy store was moved.
285
- */
286
- export declare function migrateLegacyStore(defaultFile: string): boolean;
287
- /** Load and validate the map file at `path`. */
288
- export declare function loadMapFile(path: string): Result<MellosMap, StoreError>;
289
- /**
290
- * Write the map to `path` atomically (P2): serialize to a private sibling
291
- * temp file, then rename it over the target, retrying a rename the OS
292
- * refuses transiently. Creates the parent directory if missing.
293
- * @returns ok when the file holds the new map; save-failed when it does not,
294
- * in which case the previous content is intact and the call may be retried.
295
- */
296
- export declare function saveMapFile(path: string, map: MellosMap): Result<void, StoreError>;
43
+ export { writeFileAtomic } from './atomic.js';
44
+ export { STORE_DIR_NAME, STATE_FILE_RELATIVE_PATH, PAGES_DIR_NAME, pageFilePath, pageIdOfFile, listPageFiles, deletePageFile } from './pages.js';
45
+ export { FOCUS_FILE_NAME, focusFilePath, type FocusRequest, takeFocusRequest, QUIT_FILE_NAME, quitFilePath, takeQuitRequest, sweepQuitRequest } from './channels.js';
46
+ export { VIEWERS_DIR_NAME, VIEWER_FILE_VERSION, VIEWER_HEARTBEAT_MS, VIEWER_STALE_MS, VIEWER_SWEEP_MS, viewersDirPath, viewerFilePath, type ViewerReport, type LiveViewer, publishViewer, retireViewer, readLiveViewers } from './viewers.js';
47
+ export { CONFIG_FILE_NAME, CONFIG_FILE_VERSION, configFilePath, userConfigFilePath, MAPPING_POLICIES, type MappingPolicy, type InvalidPolicy, makeMappingPolicy, describeMappingPolicy, loadMappingPolicy, saveMappingPolicy, POLICY_SCOPES, type PolicyScope, type MappingPolicyScopes, effectiveMappingPolicy } from './policy.js';
48
+ export { LEGACY_STATE_FILE_RELATIVE_PATH, migrateLegacyStore } from './migration.js';
49
+ export { loadMapFile, saveMapFile } from './maps.js';