@spikedpunch/mast 0.3.0 → 0.4.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 (81) hide show
  1. package/README.md +183 -9
  2. package/assets/prime.md +18 -0
  3. package/assets/signals.md +51 -0
  4. package/assets/skill.md +6 -47
  5. package/dist/cli/docs-cmd.d.ts +1 -0
  6. package/dist/cli/docs-cmd.d.ts.map +1 -1
  7. package/dist/cli/docs-cmd.js +2 -1
  8. package/dist/cli/docs-cmd.js.map +1 -1
  9. package/dist/cli/hook-cmd.d.ts +3 -0
  10. package/dist/cli/hook-cmd.d.ts.map +1 -0
  11. package/dist/cli/hook-cmd.js +13 -0
  12. package/dist/cli/hook-cmd.js.map +1 -0
  13. package/dist/cli/hook-state-dir.d.ts +24 -0
  14. package/dist/cli/hook-state-dir.d.ts.map +1 -0
  15. package/dist/cli/hook-state-dir.js +57 -0
  16. package/dist/cli/hook-state-dir.js.map +1 -0
  17. package/dist/cli/hook.d.ts +44 -0
  18. package/dist/cli/hook.d.ts.map +1 -0
  19. package/dist/cli/hook.js +200 -0
  20. package/dist/cli/hook.js.map +1 -0
  21. package/dist/cli/index.js +12 -2
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/cli/installed-artifacts.d.ts +23 -0
  24. package/dist/cli/installed-artifacts.d.ts.map +1 -0
  25. package/dist/cli/installed-artifacts.js +39 -0
  26. package/dist/cli/installed-artifacts.js.map +1 -0
  27. package/dist/cli/prime-cmd.d.ts +17 -0
  28. package/dist/cli/prime-cmd.d.ts.map +1 -0
  29. package/dist/cli/prime-cmd.js +70 -0
  30. package/dist/cli/prime-cmd.js.map +1 -0
  31. package/dist/cli/program.d.ts.map +1 -1
  32. package/dist/cli/program.js +6 -0
  33. package/dist/cli/program.js.map +1 -1
  34. package/dist/cli/setup-cmd.d.ts +34 -0
  35. package/dist/cli/setup-cmd.d.ts.map +1 -0
  36. package/dist/cli/setup-cmd.js +214 -0
  37. package/dist/cli/setup-cmd.js.map +1 -0
  38. package/dist/cli/setup-command.d.ts +47 -0
  39. package/dist/cli/setup-command.d.ts.map +1 -0
  40. package/dist/cli/setup-command.js +76 -0
  41. package/dist/cli/setup-command.js.map +1 -0
  42. package/dist/cli/setup-plan.d.ts +52 -0
  43. package/dist/cli/setup-plan.d.ts.map +1 -0
  44. package/dist/cli/setup-plan.js +221 -0
  45. package/dist/cli/setup-plan.js.map +1 -0
  46. package/dist/cli/setup-rules.d.ts +41 -0
  47. package/dist/cli/setup-rules.d.ts.map +1 -0
  48. package/dist/cli/setup-rules.js +76 -0
  49. package/dist/cli/setup-rules.js.map +1 -0
  50. package/dist/cli/setup-static.d.ts +13 -0
  51. package/dist/cli/setup-static.d.ts.map +1 -0
  52. package/dist/cli/setup-static.js +116 -0
  53. package/dist/cli/setup-static.js.map +1 -0
  54. package/dist/cli/skill-install.d.ts +2 -0
  55. package/dist/cli/skill-install.d.ts.map +1 -1
  56. package/dist/cli/skill-install.js +2 -0
  57. package/dist/cli/skill-install.js.map +1 -1
  58. package/dist/cli/upgrade-cmd.d.ts +5 -1
  59. package/dist/cli/upgrade-cmd.d.ts.map +1 -1
  60. package/dist/cli/upgrade-cmd.js +28 -3
  61. package/dist/cli/upgrade-cmd.js.map +1 -1
  62. package/dist/indexer/watcher.d.ts +80 -2
  63. package/dist/indexer/watcher.d.ts.map +1 -1
  64. package/dist/indexer/watcher.js +172 -27
  65. package/dist/indexer/watcher.js.map +1 -1
  66. package/dist/mcp/instructions.d.ts +7 -0
  67. package/dist/mcp/instructions.d.ts.map +1 -0
  68. package/dist/mcp/instructions.js +10 -0
  69. package/dist/mcp/instructions.js.map +1 -0
  70. package/dist/mcp/server.d.ts +8 -0
  71. package/dist/mcp/server.d.ts.map +1 -1
  72. package/dist/mcp/server.js +17 -4
  73. package/dist/mcp/server.js.map +1 -1
  74. package/dist/store/config.d.ts.map +1 -1
  75. package/dist/store/config.js +4 -5
  76. package/dist/store/config.js.map +1 -1
  77. package/dist/store/defaults.d.ts +9 -0
  78. package/dist/store/defaults.d.ts.map +1 -0
  79. package/dist/store/defaults.js +12 -0
  80. package/dist/store/defaults.js.map +1 -0
  81. package/package.json +2 -1
@@ -0,0 +1,214 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { basename, dirname, join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { readDoc } from './docs-cmd.js';
6
+ import { buildHookCommand, hookFilePath, localBinCandidates } from './setup-command.js';
7
+ import { HOOK_DEFS, planFor } from './setup-plan.js';
8
+ import { runCursorRules, runStaticSetup, STATIC_HARNESSES } from './setup-static.js';
9
+ import { detectInstallKind } from './upgrade-cmd.js';
10
+ import { PACKAGE_NAME } from './version.js';
11
+ /**
12
+ * `mast setup <harness>` writes hook configuration and, for harnesses with a rules file
13
+ * mast can own (or Zed's, via a marked block), that file too. It follows the rules
14
+ * `skill-install.ts` states: touch nothing it does not own, and be a byte-level no-op on
15
+ * re-run. Unlike `skill --install` it creates its file when absent, because a hook file
16
+ * is tool configuration and not a hand-curated prompt (ADR 017 section 3).
17
+ */
18
+ const HOOK_HARNESSES = ['claude', 'cursor', 'vscode'];
19
+ const ALL_HARNESSES = [...HOOK_HARNESSES, ...STATIC_HARNESSES];
20
+ /** Re-emits `value` in the file's own indentation, line endings and trailing-newline state. */
21
+ function serialize(value, previous) {
22
+ const indent = previous === null ? ' ' : (/^([ \t]+)\S/m.exec(previous)?.[1] ?? ' ');
23
+ const trailingNewline = previous === null || previous.endsWith('\n');
24
+ let text = JSON.stringify(value, null, indent) + (trailingNewline ? '\n' : '');
25
+ if (previous?.includes('\r\n'))
26
+ text = text.replace(/\n/g, '\r\n');
27
+ return text;
28
+ }
29
+ function parseJson(raw) {
30
+ try {
31
+ return { ok: true, value: JSON.parse(raw) };
32
+ }
33
+ catch (error) {
34
+ return { ok: false, problem: `not valid JSON (${error instanceof Error ? error.message : String(error)})` };
35
+ }
36
+ }
37
+ function buildCommands(harness, scope, env, localBin) {
38
+ const commands = {};
39
+ for (const def of HOOK_DEFS[harness]) {
40
+ const built = buildHookCommand({
41
+ harness, event: def.event, installKind: env.installKind, scope, cliEntry: env.cliEntry,
42
+ ...(localBin !== undefined ? { localBin } : {}),
43
+ });
44
+ if (!built.ok)
45
+ return built;
46
+ commands[def.event] = built.command;
47
+ }
48
+ return { ok: true, commands };
49
+ }
50
+ function notesFor(harness, scope, env) {
51
+ const notes = [];
52
+ if (harness === 'vscode') {
53
+ notes.push('Only the session primer is installed for VS Code. A search reminder is not: VS Code ignores hook matchers, ' +
54
+ 'so it would run on every tool call, and the name of its search tool is not documented.');
55
+ }
56
+ if (env.installKind === 'source') {
57
+ notes.push('The hook command contains a path specific to this machine; do not commit this file.');
58
+ }
59
+ if (env.installKind === 'local' && scope === 'project' && harness === 'vscode') {
60
+ notes.push('The relative command node_modules/.bin/mast is unverified in VS Code: its docs do not say what directory hooks run in.');
61
+ }
62
+ return notes;
63
+ }
64
+ function describePlan(plan, existed, remove) {
65
+ if (remove)
66
+ return plan.removedCount > 0 ? 'removed' : 'not installed';
67
+ if (!plan.changed)
68
+ return 'already up to date';
69
+ return !existed || plan.items.every((i) => i.state === 'missing') ? 'installed' : 'updated';
70
+ }
71
+ function reportProblem(io, path, problem) {
72
+ io.err(`mast setup: cannot update ${path}: ${problem}.`);
73
+ io.err('The file was left unchanged. Fix or delete it, then run the command again.');
74
+ }
75
+ /** Returns the process exit code: 0 done, 1 refused or failed, 2 usage error. */
76
+ export function runSetup(opts, env, io) {
77
+ const hookHarness = HOOK_HARNESSES.find((h) => h === opts.harness);
78
+ const staticHarness = STATIC_HARNESSES.find((h) => h === opts.harness);
79
+ if (hookHarness === undefined && staticHarness === undefined) {
80
+ io.err(`mast setup: unknown harness "${opts.harness}". Supported: ${ALL_HARNESSES.join(', ')}.`);
81
+ return 2;
82
+ }
83
+ if (opts.check && (opts.remove || opts.dryRun)) {
84
+ io.err('mast setup: --check cannot be combined with --remove or --dry-run.');
85
+ return 2;
86
+ }
87
+ if (staticHarness !== undefined)
88
+ return runStaticSetup(staticHarness, opts, env, io);
89
+ if (hookHarness === undefined)
90
+ return 2;
91
+ const hookCode = runHookSetup(hookHarness, opts, env, io);
92
+ if (hookHarness !== 'cursor')
93
+ return hookCode;
94
+ // A failed hooks step stops the rules step, except under --check, which reports both.
95
+ if (hookCode !== 0 && !opts.check)
96
+ return hookCode;
97
+ const rulesCode = runCursorRules(opts, env, io);
98
+ return hookCode !== 0 ? hookCode : rulesCode;
99
+ }
100
+ function runHookSetup(harness, opts, env, io) {
101
+ const scope = opts.global ? 'global' : 'project';
102
+ const path = hookFilePath(harness, scope, opts.projectRoot, env.home);
103
+ let mode = { kind: 'remove' };
104
+ if (!opts.remove) {
105
+ let localBin;
106
+ if (env.installKind === 'local' && scope === 'project') {
107
+ const candidates = localBinCandidates(env.cliEntry, opts.projectRoot);
108
+ localBin = candidates.find((candidate) => io.fileExists(join(opts.projectRoot, candidate)));
109
+ if (localBin === undefined) {
110
+ io.err(`mast setup: no mast binary found for the hook command to run. Looked for: ${candidates.map((c) => join(opts.projectRoot, c)).join(', ')}. ` +
111
+ `Add mast as a direct dependency of this project or of the package you run it from (pnpm add -D ${PACKAGE_NAME}), or install it globally.`);
112
+ return 1;
113
+ }
114
+ }
115
+ const built = buildCommands(harness, scope, env, localBin);
116
+ if (!built.ok) {
117
+ io.err(`mast setup: ${built.problem}`);
118
+ return 1;
119
+ }
120
+ mode = { kind: 'install', commands: built.commands };
121
+ }
122
+ try {
123
+ const raw = io.readFile(path);
124
+ let existing = null;
125
+ if (raw !== null) {
126
+ const parsed = parseJson(raw);
127
+ if (!parsed.ok) {
128
+ reportProblem(io, path, parsed.problem);
129
+ return 1;
130
+ }
131
+ existing = parsed.value;
132
+ }
133
+ const plan = planFor(harness, existing, mode);
134
+ if (!plan.ok) {
135
+ reportProblem(io, path, plan.problem);
136
+ return 1;
137
+ }
138
+ if (opts.check) {
139
+ const stale = plan.items.filter((i) => i.state !== 'current');
140
+ if (stale.length === 0) {
141
+ io.out(`${path}: already up to date`);
142
+ return 0;
143
+ }
144
+ // A settings file that exists but holds none of mast's hooks is "not installed",
145
+ // the same as no file: "out of date" would send someone looking for a stale entry.
146
+ io.out(`${path}: ${stale.length === plan.items.length && stale.every((i) => i.state === 'missing') ? 'not installed' : 'out of date'}`);
147
+ for (const item of stale)
148
+ io.out(` ${item.state === 'missing' ? 'missing' : 'out of date'}: ${item.label}`);
149
+ return 1;
150
+ }
151
+ const status = describePlan(plan, raw !== null, opts.remove);
152
+ const willWrite = plan.changed;
153
+ if (opts.dryRun) {
154
+ io.out(`${path}: ${status} (dry run, nothing written)`);
155
+ if (willWrite)
156
+ io.out(plan.next === null ? '(the file would be deleted)' : serialize(plan.next, raw).trimEnd());
157
+ }
158
+ else {
159
+ if (willWrite) {
160
+ if (plan.next === null)
161
+ io.removeFile(path);
162
+ else
163
+ io.writeFileAtomic(path, serialize(plan.next, raw));
164
+ }
165
+ io.out(`${path}: ${status}`);
166
+ }
167
+ if (!opts.remove)
168
+ for (const note of notesFor(harness, scope, env))
169
+ io.out(`Note: ${note}`);
170
+ return 0;
171
+ }
172
+ catch (error) {
173
+ io.err(`mast setup: ${path}: ${error instanceof Error ? error.message : String(error)}`);
174
+ return 1;
175
+ }
176
+ }
177
+ export function createNodeSetupIo() {
178
+ return {
179
+ readFile: (path) => (existsSync(path) ? readFileSync(path, 'utf8') : null),
180
+ writeFileAtomic: (path, content) => {
181
+ // Resolve a symlinked settings file first, or the rename would replace the link
182
+ // (a dotfiles checkout, say) with a regular file.
183
+ const target = existsSync(path) ? realpathSync(path) : path;
184
+ mkdirSync(dirname(target), { recursive: true });
185
+ // Same directory, so the rename cannot cross a filesystem and stays atomic.
186
+ const temp = join(dirname(target), `.${basename(target)}.${process.pid}.tmp`);
187
+ writeFileSync(temp, content);
188
+ if (existsSync(target))
189
+ chmodSync(temp, statSync(target).mode & 0o777);
190
+ renameSync(temp, target);
191
+ },
192
+ removeFile: (path) => { rmSync(path); },
193
+ fileExists: existsSync,
194
+ directoryExists: (path) => existsSync(path) && statSync(path).isDirectory(),
195
+ out: (line) => { process.stdout.write(`${line}\n`); },
196
+ err: (line) => { process.stderr.write(`${line}\n`); },
197
+ };
198
+ }
199
+ export function registerSetupCommand(program) {
200
+ program
201
+ .command('setup <harness> [path]')
202
+ .description('Install the hooks and rules files that tell an agent to use mast (claude|cursor|vscode|windsurf|zed|desktop)')
203
+ .option('--global', 'Write the user-level file instead of the project one')
204
+ .option('--check', 'Write nothing; exit 0 only if everything is installed and current')
205
+ .option('--remove', 'Remove what mast installed')
206
+ .option('--dry-run', 'Print the file that would be written, and write nothing')
207
+ .action((harness, path, opts) => {
208
+ const projectRoot = resolve(path ?? '.');
209
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
210
+ // The home directory is resolved here, at the edge, and nowhere below.
211
+ process.exitCode = runSetup({ harness, projectRoot, global: opts.global === true, check: opts.check === true, remove: opts.remove === true, dryRun: opts.dryRun === true }, { installKind: detectInstallKind(moduleDir, projectRoot), home: homedir(), cliEntry: join(moduleDir, 'index.js'), skillText: readDoc('skill') }, createNodeSetupIo());
212
+ });
213
+ }
214
+ //# sourceMappingURL=setup-cmd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-cmd.js","sourceRoot":"","sources":["../../src/cli/setup-cmd.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ import type { Harness, HookEvent } from './hook.js';
2
+ import type { InstallKind } from './upgrade-cmd.js';
3
+ export type SetupScope = 'project' | 'global';
4
+ export interface HookCommandInput {
5
+ readonly harness: Harness;
6
+ readonly event: HookEvent;
7
+ readonly installKind: InstallKind;
8
+ readonly scope: SetupScope;
9
+ /** Absolute path of this build's `cli/index.js`; used only for a source checkout. */
10
+ readonly cliEntry: string;
11
+ /**
12
+ * Where a project dependency's binary is, relative to the project root with forward
13
+ * slashes. Defaults to the root's own `node_modules/.bin/mast`.
14
+ */
15
+ readonly localBin?: string;
16
+ }
17
+ export type HookCommandResult = {
18
+ readonly ok: true;
19
+ readonly command: string;
20
+ } | {
21
+ readonly ok: false;
22
+ readonly problem: string;
23
+ };
24
+ /** The hook file `setup` writes for a harness: project-level, or under the user's home. */
25
+ export declare function hookFilePath(harness: Harness, scope: SetupScope, projectRoot: string, home: string): string;
26
+ /**
27
+ * Where a project dependency's `mast` binary can be, most likely first, relative to the
28
+ * project root with forward slashes: beside each `node_modules` this build is running
29
+ * out of (outermost first), then the project root's own.
30
+ *
31
+ * The root is not the only place (D070): a project whose package lives in a
32
+ * subdirectory has `<sub>/node_modules/.bin/mast` and nothing at the root. The root
33
+ * stays as the last candidate for a build that runs from outside the project.
34
+ */
35
+ export declare function localBinCandidates(cliEntry: string, projectRoot: string): string[];
36
+ /**
37
+ * The command line a harness runs for one hook. Every spelling ends in
38
+ * ` hook <harness> <event>`: `setup` finds its own entries by that suffix, so the part
39
+ * before it can change between installs without orphaning the old entry.
40
+ *
41
+ * A project-dependency install cannot be written into user-level settings: the path
42
+ * points into one project's node_modules and fails in every other project.
43
+ */
44
+ export declare function buildHookCommand(input: HookCommandInput): HookCommandResult;
45
+ /** True when `command` is the one `setup` installs for this harness and event, in any spelling. */
46
+ export declare function isMastCommand(command: string | undefined, harness: Harness, event: HookEvent): boolean;
47
+ //# sourceMappingURL=setup-command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-command.d.ts","sourceRoot":"","sources":["../../src/cli/setup-command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE9C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,qFAAqF;IACrF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAErD,2FAA2F;AAC3F,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAK3G;AAOD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CASlF;AAkBD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,CAa3E;AAED,mGAAmG;AACnG,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAEtG"}
@@ -0,0 +1,76 @@
1
+ import { join, relative, isAbsolute, sep } from 'node:path';
2
+ /** The hook file `setup` writes for a harness: project-level, or under the user's home. */
3
+ export function hookFilePath(harness, scope, projectRoot, home) {
4
+ const base = scope === 'global' ? home : projectRoot;
5
+ if (harness === 'claude')
6
+ return join(base, '.claude', 'settings.json');
7
+ if (harness === 'cursor')
8
+ return join(base, '.cursor', 'hooks.json');
9
+ return scope === 'global' ? join(home, '.copilot', 'hooks', 'mast.json') : join(projectRoot, '.github', 'hooks', 'mast.json');
10
+ }
11
+ // Claude Code expands this variable itself when it runs a project hook, so the spelling
12
+ // holds wherever the project is checked out. A template literal would interpolate it.
13
+ const CLAUDE_PROJECT_DIR = '$' + '{CLAUDE_PROJECT_DIR}';
14
+ const ROOT_LOCAL_BIN = 'node_modules/.bin/mast';
15
+ /**
16
+ * Where a project dependency's `mast` binary can be, most likely first, relative to the
17
+ * project root with forward slashes: beside each `node_modules` this build is running
18
+ * out of (outermost first), then the project root's own.
19
+ *
20
+ * The root is not the only place (D070): a project whose package lives in a
21
+ * subdirectory has `<sub>/node_modules/.bin/mast` and nothing at the root. The root
22
+ * stays as the last candidate for a build that runs from outside the project.
23
+ */
24
+ export function localBinCandidates(cliEntry, projectRoot) {
25
+ const rel = relative(projectRoot, cliEntry);
26
+ const segments = rel.startsWith('..') || isAbsolute(rel) ? [] : rel.split(sep);
27
+ const candidates = [];
28
+ segments.forEach((segment, index) => {
29
+ if (segment === 'node_modules')
30
+ candidates.push([...segments.slice(0, index + 1), '.bin', 'mast'].join('/'));
31
+ });
32
+ if (!candidates.includes(ROOT_LOCAL_BIN))
33
+ candidates.push(ROOT_LOCAL_BIN);
34
+ return candidates;
35
+ }
36
+ // A path made only of these needs no quoting in a POSIX shell. The plain spelling is
37
+ // kept for them so a file written by 0.4.0 still compares as current.
38
+ const SHELL_SAFE = /^[A-Za-z0-9_@+.\/-]+$/;
39
+ function escapeForDoubleQuotes(text) {
40
+ return text.replace(/["\\$`]/g, (ch) => `\\${ch}`);
41
+ }
42
+ function localCommand(harness, localBin) {
43
+ if (SHELL_SAFE.test(localBin)) {
44
+ return harness === 'claude' ? `"${CLAUDE_PROJECT_DIR}"/${localBin}` : localBin;
45
+ }
46
+ const quoted = escapeForDoubleQuotes(localBin);
47
+ return harness === 'claude' ? `"${CLAUDE_PROJECT_DIR}/${quoted}"` : `"${quoted}"`;
48
+ }
49
+ /**
50
+ * The command line a harness runs for one hook. Every spelling ends in
51
+ * ` hook <harness> <event>`: `setup` finds its own entries by that suffix, so the part
52
+ * before it can change between installs without orphaning the old entry.
53
+ *
54
+ * A project-dependency install cannot be written into user-level settings: the path
55
+ * points into one project's node_modules and fails in every other project.
56
+ */
57
+ export function buildHookCommand(input) {
58
+ const tail = ` hook ${input.harness} ${input.event}`;
59
+ if (input.installKind === 'global')
60
+ return { ok: true, command: `mast${tail}` };
61
+ if (input.installKind === 'source')
62
+ return { ok: true, command: `node "${input.cliEntry}"${tail}` };
63
+ if (input.scope === 'global') {
64
+ return {
65
+ ok: false,
66
+ problem: 'this mast is a project dependency, and a user-level hook that points into one project\'s node_modules ' +
67
+ 'breaks in every other project. Install mast globally, or run without --global.',
68
+ };
69
+ }
70
+ return { ok: true, command: `${localCommand(input.harness, input.localBin ?? ROOT_LOCAL_BIN)}${tail}` };
71
+ }
72
+ /** True when `command` is the one `setup` installs for this harness and event, in any spelling. */
73
+ export function isMastCommand(command, harness, event) {
74
+ return command !== undefined && command.endsWith(` hook ${harness} ${event}`);
75
+ }
76
+ //# sourceMappingURL=setup-command.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-command.js","sourceRoot":"","sources":["../../src/cli/setup-command.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import type { Harness, HookEvent } from './hook.js';
2
+ interface HookDef {
3
+ readonly event: HookEvent;
4
+ /** The harness's own name for the event, which is also the key in its hooks object. */
5
+ readonly nativeEvent: string;
6
+ readonly matcher?: string;
7
+ readonly timeout: number;
8
+ }
9
+ /**
10
+ * The hooks each harness gets. The Claude session-start group has no matcher on purpose:
11
+ * that matches every source, including `compact`. VS Code has no search hook because its
12
+ * docs say matchers are ignored (it would run on every tool call) and the search tool's
13
+ * name is undocumented.
14
+ */
15
+ export declare const HOOK_DEFS: Readonly<Record<Harness, readonly HookDef[]>>;
16
+ export type Commands = Readonly<Partial<Record<HookEvent, string>>>;
17
+ export type PlanMode = {
18
+ readonly kind: 'install';
19
+ readonly commands: Commands;
20
+ } | {
21
+ readonly kind: 'remove';
22
+ };
23
+ export type ItemState = 'missing' | 'outdated' | 'current';
24
+ export interface PlanItem {
25
+ readonly label: string;
26
+ readonly state: ItemState;
27
+ }
28
+ export type PlanResult = {
29
+ readonly ok: true;
30
+ /** The whole file's next value, or null when the file should be deleted. */
31
+ readonly next: object | null;
32
+ /** False means the file must not be written at all. */
33
+ readonly changed: boolean;
34
+ /** Install mode: the state of each hook found. */
35
+ readonly items: readonly PlanItem[];
36
+ /** Remove mode: how many of mast's entries were taken out. */
37
+ readonly removedCount: number;
38
+ } | {
39
+ readonly ok: false;
40
+ readonly problem: string;
41
+ };
42
+ /**
43
+ * Claude Code: `hooks.<Event>` is a list of groups, each `{matcher?, hooks: [handler]}`.
44
+ * Within a group only mast's handler is touched; a group mast empties is removed, a group
45
+ * with other handlers keeps them.
46
+ */
47
+ export declare function planClaude(existing: unknown, mode: PlanMode): PlanResult;
48
+ export declare const planCursor: (existing: unknown, mode: PlanMode) => PlanResult;
49
+ export declare const planVscode: (existing: unknown, mode: PlanMode) => PlanResult;
50
+ export declare function planFor(harness: Harness, existing: unknown, mode: PlanMode): PlanResult;
51
+ export {};
52
+ //# sourceMappingURL=setup-plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-plan.d.ts","sourceRoot":"","sources":["../../src/cli/setup-plan.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAOpD,UAAU,OAAO;IACf,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,uFAAuF;IACvF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,OAAO,EAAE,CAAC,CAUnE,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;AACpE,MAAM,MAAM,QAAQ,GAChB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,GACzD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEhC,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAC3D,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;CAC3B;AAED,MAAM,MAAM,UAAU,GAClB;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,4EAA4E;IAC5E,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,kDAAkD;IAClD,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;IACpC,8DAA8D;IAC9D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B,GACD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AA2CrD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,GAAG,UAAU,CA6ExE;AA2ED,eAAO,MAAM,UAAU,GAAI,UAAU,OAAO,EAAE,MAAM,QAAQ,KAAG,UAAgD,CAAC;AAChH,eAAO,MAAM,UAAU,GAAI,UAAU,OAAO,EAAE,MAAM,QAAQ,KAAG,UAAgD,CAAC;AAEhH,wBAAgB,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,GAAG,UAAU,CAGvF"}
@@ -0,0 +1,221 @@
1
+ import { z } from 'zod';
2
+ import { isMastCommand } from './setup-command.js';
3
+ /** Seconds, in all three harnesses. Session start builds the primer; search must stay cheap. */
4
+ const SESSION_START_TIMEOUT = 30;
5
+ const SEARCH_TIMEOUT = 5;
6
+ /**
7
+ * The hooks each harness gets. The Claude session-start group has no matcher on purpose:
8
+ * that matches every source, including `compact`. VS Code has no search hook because its
9
+ * docs say matchers are ignored (it would run on every tool call) and the search tool's
10
+ * name is undocumented.
11
+ */
12
+ export const HOOK_DEFS = {
13
+ claude: [
14
+ { event: 'session-start', nativeEvent: 'SessionStart', timeout: SESSION_START_TIMEOUT },
15
+ { event: 'search', nativeEvent: 'PreToolUse', matcher: 'Grep|Glob', timeout: SEARCH_TIMEOUT },
16
+ ],
17
+ cursor: [
18
+ { event: 'session-start', nativeEvent: 'sessionStart', timeout: SESSION_START_TIMEOUT },
19
+ { event: 'search', nativeEvent: 'postToolUse', matcher: 'Grep', timeout: SEARCH_TIMEOUT },
20
+ ],
21
+ vscode: [{ event: 'session-start', nativeEvent: 'SessionStart', timeout: SESSION_START_TIMEOUT }],
22
+ };
23
+ // Zod is used to validate, not to rebuild: a parsed loose object puts its known keys
24
+ // first, which would reorder a user's file. `passThrough` validates and hands back the
25
+ // original value, so key order survives and only mast's entries are edited.
26
+ const rootShape = z.looseObject({ hooks: z.looseObject({}).optional() });
27
+ const handlerShape = z.looseObject({ command: z.string().optional() });
28
+ const groupShape = z.looseObject({ matcher: z.string().optional(), hooks: z.array(handlerShape) });
29
+ const flatEntryShape = z.looseObject({ command: z.string().optional() });
30
+ function passThrough(schema) {
31
+ return z.custom((value) => schema.safeParse(value).success);
32
+ }
33
+ function readTyped(schema, value, where) {
34
+ const result = schema.safeParse(value);
35
+ if (!result.success) {
36
+ const found = result.error.issues
37
+ .slice(0, 3)
38
+ .map((issue) => `${[where, ...issue.path.map(String)].join('.')}: ${issue.message}`);
39
+ return { ok: false, problem: `unexpected structure (${found.join('; ')})` };
40
+ }
41
+ return { ok: true, data: structuredClone(passThrough(schema).parse(value)) };
42
+ }
43
+ function readRoot(existing) {
44
+ return existing === null ? { ok: true, data: {} } : readTyped(rootShape, existing, 'file');
45
+ }
46
+ const fail = (problem) => ({ ok: false, problem });
47
+ function hooksOf(root) {
48
+ const hooks = root.hooks ?? {};
49
+ root.hooks = hooks;
50
+ return hooks;
51
+ }
52
+ /**
53
+ * Claude Code: `hooks.<Event>` is a list of groups, each `{matcher?, hooks: [handler]}`.
54
+ * Within a group only mast's handler is touched; a group mast empties is removed, a group
55
+ * with other handlers keeps them.
56
+ */
57
+ export function planClaude(existing, mode) {
58
+ const rootRead = readRoot(existing);
59
+ if (!rootRead.ok)
60
+ return fail(rootRead.problem);
61
+ const root = rootRead.data;
62
+ const items = [];
63
+ let removedCount = 0;
64
+ let changed = false;
65
+ for (const def of HOOK_DEFS.claude) {
66
+ const rawGroups = root.hooks?.[def.nativeEvent];
67
+ const read = rawGroups === undefined ? null : readTyped(z.array(groupShape), rawGroups, `hooks.${def.nativeEvent}`);
68
+ if (read !== null && !read.ok)
69
+ return fail(read.problem);
70
+ let groups = read === null ? [] : read.data;
71
+ const isMast = (command) => isMastCommand(command, 'claude', def.event);
72
+ const touched = new Set();
73
+ if (mode.kind === 'remove') {
74
+ for (const group of groups) {
75
+ const kept = group.hooks.filter((h) => !isMast(h.command));
76
+ if (kept.length === group.hooks.length)
77
+ continue;
78
+ removedCount += group.hooks.length - kept.length;
79
+ group.hooks = kept;
80
+ touched.add(group);
81
+ }
82
+ if (touched.size === 0)
83
+ continue;
84
+ groups = groups.filter((g) => !touched.has(g) || g.hooks.length > 0);
85
+ changed = true;
86
+ if (groups.length === 0)
87
+ delete hooksOf(root)[def.nativeEvent];
88
+ else
89
+ hooksOf(root)[def.nativeEvent] = groups;
90
+ continue;
91
+ }
92
+ const command = mode.commands[def.event];
93
+ if (command === undefined)
94
+ continue;
95
+ const handler = { type: 'command', command, timeout: def.timeout };
96
+ const found = groups.flatMap((group) => group.hooks.filter((h) => isMast(h.command)).map((h) => ({ group, h })));
97
+ const first = found[0];
98
+ const duplicates = found.slice(1);
99
+ const fresh = () => ({ ...(def.matcher === undefined ? {} : { matcher: def.matcher }), hooks: [{ ...handler }] });
100
+ let state = 'current';
101
+ if (first === undefined) {
102
+ groups.push(fresh());
103
+ state = 'missing';
104
+ }
105
+ else {
106
+ const sameHandler = first.h['type'] === handler.type && first.h.command === command && first.h['timeout'] === handler.timeout;
107
+ if (!sameHandler || first.group.matcher !== def.matcher) {
108
+ state = 'outdated';
109
+ const ownsGroup = first.group.hooks.every((h) => isMast(h.command));
110
+ if (ownsGroup || first.group.matcher === def.matcher) {
111
+ Object.assign(first.h, handler);
112
+ if (ownsGroup) {
113
+ if (def.matcher === undefined)
114
+ delete first.group.matcher;
115
+ else
116
+ first.group.matcher = def.matcher;
117
+ }
118
+ }
119
+ else {
120
+ // The group's matcher also scopes someone else's handler, so it cannot be edited.
121
+ first.group.hooks = first.group.hooks.filter((h) => h !== first.h);
122
+ touched.add(first.group);
123
+ groups.push(fresh());
124
+ }
125
+ }
126
+ }
127
+ for (const dup of duplicates) {
128
+ dup.group.hooks = dup.group.hooks.filter((h) => h !== dup.h);
129
+ touched.add(dup.group);
130
+ state = 'outdated';
131
+ }
132
+ groups = groups.filter((g) => !touched.has(g) || g.hooks.length > 0);
133
+ items.push({ label: def.nativeEvent, state });
134
+ if (state !== 'current') {
135
+ changed = true;
136
+ hooksOf(root)[def.nativeEvent] = groups;
137
+ }
138
+ }
139
+ return { ok: true, next: root, changed, items, removedCount };
140
+ }
141
+ /** Cursor (`hooks.json`) and VS Code (`mast.json`): `hooks.<event>` is a flat list of entries. */
142
+ function planFlat(harness, existing, mode) {
143
+ const rootRead = readRoot(existing);
144
+ if (!rootRead.ok)
145
+ return fail(rootRead.problem);
146
+ const root = rootRead.data;
147
+ const items = [];
148
+ let removedCount = 0;
149
+ let changed = false;
150
+ if (mode.kind === 'install' && harness === 'cursor' && !('version' in root)) {
151
+ root['version'] = 1;
152
+ items.push({ label: 'version', state: 'missing' });
153
+ changed = true;
154
+ }
155
+ for (const def of HOOK_DEFS[harness]) {
156
+ const rawEntries = root.hooks?.[def.nativeEvent];
157
+ const read = rawEntries === undefined ? null : readTyped(z.array(flatEntryShape), rawEntries, `hooks.${def.nativeEvent}`);
158
+ if (read !== null && !read.ok)
159
+ return fail(read.problem);
160
+ const entries = read === null ? [] : read.data;
161
+ const isMast = (e) => isMastCommand(e.command, harness, def.event);
162
+ if (mode.kind === 'remove') {
163
+ const kept = entries.filter((e) => !isMast(e));
164
+ if (kept.length === entries.length)
165
+ continue;
166
+ removedCount += entries.length - kept.length;
167
+ changed = true;
168
+ if (kept.length === 0)
169
+ delete hooksOf(root)[def.nativeEvent];
170
+ else
171
+ hooksOf(root)[def.nativeEvent] = kept;
172
+ continue;
173
+ }
174
+ const command = mode.commands[def.event];
175
+ if (command === undefined)
176
+ continue;
177
+ const wanted = harness === 'cursor'
178
+ ? { command, type: 'command', ...(def.matcher === undefined ? {} : { matcher: def.matcher }), timeout: def.timeout }
179
+ : { type: 'command', command, timeout: def.timeout };
180
+ const found = entries.filter(isMast);
181
+ const first = found[0];
182
+ let state = 'current';
183
+ let next = entries;
184
+ if (first === undefined) {
185
+ next = [...entries, wanted];
186
+ state = 'missing';
187
+ }
188
+ else {
189
+ const same = Object.entries(wanted).every(([key, value]) => first[key] === value) && 'matcher' in first === 'matcher' in wanted;
190
+ if (!same) {
191
+ Object.assign(first, wanted);
192
+ if (!('matcher' in wanted))
193
+ delete first['matcher'];
194
+ state = 'outdated';
195
+ }
196
+ if (found.length > 1) {
197
+ next = entries.filter((e) => e === first || !isMast(e));
198
+ state = 'outdated';
199
+ }
200
+ }
201
+ items.push({ label: def.nativeEvent, state });
202
+ if (state !== 'current') {
203
+ changed = true;
204
+ hooksOf(root)[def.nativeEvent] = next;
205
+ }
206
+ }
207
+ // mast.json belongs to mast outright: once its last entry goes, so does the file.
208
+ const hooksLeft = root.hooks === undefined ? 0 : Object.keys(root.hooks).length;
209
+ if (harness === 'vscode' && removedCount > 0 && Object.keys(root).length === 1 && hooksLeft === 0) {
210
+ return { ok: true, next: null, changed, items, removedCount };
211
+ }
212
+ return { ok: true, next: root, changed, items, removedCount };
213
+ }
214
+ export const planCursor = (existing, mode) => planFlat('cursor', existing, mode);
215
+ export const planVscode = (existing, mode) => planFlat('vscode', existing, mode);
216
+ export function planFor(harness, existing, mode) {
217
+ if (harness === 'claude')
218
+ return planClaude(existing, mode);
219
+ return harness === 'cursor' ? planCursor(existing, mode) : planVscode(existing, mode);
220
+ }
221
+ //# sourceMappingURL=setup-plan.js.map