heyspex 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/bin/hs.mjs +181 -15
  3. package/dist/cli/hs-check.mjs +88 -46
  4. package/dist/cli/hs-claim.mjs +84 -39
  5. package/dist/cli/hs-connect.mjs +583 -129
  6. package/dist/cli/hs-defer.mjs +251 -30
  7. package/dist/cli/hs-dev.mjs +20 -8
  8. package/dist/cli/hs-env.mjs +62 -34
  9. package/dist/cli/hs-index-rebuild.mjs +288 -83
  10. package/dist/cli/hs-index-reconcile.mjs +397 -111
  11. package/dist/cli/hs-init.mjs +2143 -91
  12. package/dist/cli/hs-install.mjs +628 -106
  13. package/dist/cli/hs-join.mjs +686 -236
  14. package/dist/cli/hs-vault-pull.mjs +3189 -179
  15. package/dist/cli/hs-vault-push.mjs +6433 -0
  16. package/dist/cli/hs-vault.mjs +396 -178
  17. package/dist/cli/hs-workspace.mjs +289 -151
  18. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -28
  19. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -62
  20. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -29
  21. package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -28
  22. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -29
  23. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -70
  24. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -35
  25. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -27
  26. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -23
  27. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
  28. package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -26
  29. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -32
  30. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -91
  31. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +22 -22
  32. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +56 -56
  33. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +23 -23
  34. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +24 -22
  35. package/dist/cli-templates/harness-commands/cursor/hs-next.md +23 -23
  36. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +64 -64
  37. package/dist/cli-templates/harness-commands/cursor/hs-push.md +29 -29
  38. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +21 -21
  39. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +17 -17
  40. package/dist/cli-templates/harness-commands/cursor/hs-review.md +17 -17
  41. package/dist/cli-templates/harness-commands/cursor/hs-view.md +24 -22
  42. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +26 -26
  43. package/dist/cli-templates/harness-commands/cursor/prototype.md +86 -86
  44. package/docs/copilot-plugin-install.md +177 -0
  45. package/docs/driving-heyspex-from-copilot.md +165 -0
  46. package/package.json +21 -2
  47. package/plugins/heyspex/.claude-plugin/plugin.json +12 -12
  48. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -15
  49. package/plugins/heyspex/README.md +312 -0
  50. package/plugins/heyspex/bin/run-hook.mjs +20 -2
  51. package/plugins/heyspex/hooks/cursor-hooks.json +81 -0
  52. package/plugins/heyspex/hooks/hooks.json +94 -37
  53. package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -410
  54. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +5 -0
  55. package/scripts/claude-hooks/_run.mjs +316 -23
  56. package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
  57. package/scripts/claude-hooks/claim-liveness-shared.mjs +2 -0
  58. package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
  59. package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
  60. package/scripts/claude-hooks/inject-communication-style.mjs +20 -0
  61. package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -247
  62. package/scripts/claude-hooks/session-end.mjs +2 -0
  63. package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
  64. package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
  65. package/scripts/claude-hooks/wip-checkpoint.mjs +5 -1
  66. package/scripts/cursor-hooks/manifest.mjs +46 -0
  67. package/scripts/heyspex-mode.mjs +20 -0
  68. package/scripts/hs-mode.mjs +1 -1
  69. package/scripts/hs-view.mjs +180 -17
  70. package/scripts/install-claude-code-hooks.mjs +3 -1
  71. package/scripts/install-copilot-hooks.mjs +288 -0
  72. package/scripts/lib/console-url-resolver.mjs +3 -2
  73. package/scripts/lib/customer-kit.mjs +327 -10
  74. package/scripts/lib/dev-mode.mjs +7 -6
  75. package/scripts/lib/directive-obligations.mjs +349 -0
  76. package/scripts/lib/hook-dispositions.mjs +210 -0
  77. package/scripts/lib/hook-vault-header.mjs +30 -0
  78. package/scripts/lib/hs-source-branch.mjs +196 -0
  79. package/scripts/lib/hs-source-log.mjs +2 -1
  80. package/scripts/lib/hs-source-preflight.mjs +18 -4
  81. package/scripts/lib/hs-source-push.mjs +386 -356
  82. package/scripts/lib/window-placement.mjs +109 -0
  83. package/spexharness/src/adapters/claude-code.mjs +11 -3
  84. package/spexharness/src/adapters/copilot.mjs +236 -0
  85. package/spexharness/src/adapters/detect-harness.mjs +34 -0
  86. package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
  87. package/spexharness/src/gates/deep-research-cost-gate.mjs +4 -3
  88. package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
  89. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
  90. package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
  91. package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
  92. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
  93. package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
  94. package/spexharness/src/intent-classifier.mjs +4 -2
  95. package/spexharness/src/intent-directive.mjs +626 -541
  96. package/spexharness/src/rule-table.mjs +31 -1
  97. package/vendor/heyspex-mcp/account-switch.mjs +6 -5
  98. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +31 -3
  99. package/vendor/heyspex-mcp/capture-console-client.mjs +37 -2
  100. package/vendor/heyspex-mcp/capture-focus.mjs +26 -2
  101. package/vendor/heyspex-mcp/capture-outbox.mjs +10 -4
  102. package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
  103. package/vendor/heyspex-mcp/capture-tools.mjs +56 -11
  104. package/vendor/heyspex-mcp/claim-tools.mjs +25 -2
  105. package/vendor/heyspex-mcp/cli-verbs.mjs +19 -21
  106. package/vendor/heyspex-mcp/cli.mjs +7 -1
  107. package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
  108. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +197 -7
  109. package/vendor/heyspex-mcp/console-fetch.mjs +192 -28
  110. package/vendor/heyspex-mcp/console-transport.mjs +7 -1
  111. package/vendor/heyspex-mcp/credentials.mjs +9 -8
  112. package/vendor/heyspex-mcp/dev-tools.mjs +7 -0
  113. package/vendor/heyspex-mcp/filing-tools.mjs +35 -0
  114. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +2 -1
  115. package/vendor/heyspex-mcp/folder-trust-store.mjs +2 -1
  116. package/vendor/heyspex-mcp/http-transport.mjs +2 -0
  117. package/vendor/heyspex-mcp/list-tools.mjs +1 -1
  118. package/vendor/heyspex-mcp/mcp-config.mjs +14 -1
  119. package/vendor/heyspex-mcp/memory-tools.mjs +523 -523
  120. package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
  121. package/vendor/heyspex-mcp/occupied-keys.mjs +1 -0
  122. package/vendor/heyspex-mcp/on-deck-brief.mjs +19 -2
  123. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -1708
  124. package/vendor/heyspex-mcp/plan-push.mjs +3 -2
  125. package/vendor/heyspex-mcp/preflight-gate.mjs +1 -0
  126. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -325
  127. package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -1159
  128. package/vendor/heyspex-mcp/promote-capture.mjs +2 -0
  129. package/vendor/heyspex-mcp/promote-research.mjs +6 -0
  130. package/vendor/heyspex-mcp/read-path.mjs +162 -6
  131. package/vendor/heyspex-mcp/request-context.mjs +25 -1
  132. package/vendor/heyspex-mcp/research-handlers.mjs +6 -4
  133. package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
  134. package/vendor/heyspex-mcp/session-owner.mjs +3 -2
  135. package/vendor/heyspex-mcp/session-tools.mjs +10 -0
  136. package/vendor/heyspex-mcp/spec-edit-tools.mjs +8 -1
  137. package/vendor/heyspex-mcp/split-research.mjs +3 -0
  138. package/vendor/heyspex-mcp/submit-for-review.mjs +18 -1
  139. package/vendor/heyspex-mcp/summary-write-tools.mjs +5 -1
  140. package/vendor/heyspex-mcp/test-helpers.mjs +18 -3
  141. package/vendor/heyspex-mcp/tool-catalog.mjs +7 -1
  142. package/vendor/heyspex-mcp/tool-dispatch.mjs +18 -0
  143. package/vendor/heyspex-mcp/vault-io.mjs +720 -724
  144. package/vendor/heyspex-mcp/vault-resolve.mjs +197 -9
  145. package/vendor/heyspex-mcp/whoami.mjs +2 -1
  146. package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
@@ -0,0 +1,288 @@
1
+ // `hs setup copilot` -- install HeySpex into GitHub Copilot CLI / cloud agent.
2
+ //
3
+ // Works from the published npm package only (node_modules/heyspex, or
4
+ // `npx heyspex setup copilot`) and from a platform checkout. The package root
5
+ // is resolved from THIS file's location, never from the cwd.
6
+ //
7
+ // Usage:
8
+ // node scripts/install-copilot-hooks.mjs # PLUGIN route (default)
9
+ // node scripts/install-copilot-hooks.mjs --hooks # HOOKS route: .github/hooks/heyspex.json
10
+ // node scripts/install-copilot-hooks.mjs --check # report active route; exit 1 if misconfigured
11
+ // node scripts/install-copilot-hooks.mjs --uninstall
12
+ // node scripts/install-copilot-hooks.mjs --print # agent-facing instructions to stdout
13
+ // --target <dir> repo to set up (default: cwd's repo root, walking up to .git / heyspex.yaml)
14
+ // --json (with --check) machine-readable report
15
+ //
16
+ // Single-source rule: the plugin OR the repo hooks file, never both (every hook would fire twice).
17
+ //
18
+ // Exit codes:
19
+ // 0 success / nothing to do / `copilot` missing (commands printed for the user to run)
20
+ // 1 failure: plugin route could not complete, --check found a problem, unexpected error
21
+ // 2 refused: --hooks while the heyspex plugin is installed for Copilot, or bad arguments
22
+ //
23
+ // Never prints a stack trace; every path ends with a next step.
24
+
25
+ import fs from 'node:fs';
26
+ import os from 'node:os';
27
+ import path from 'node:path';
28
+ import { spawnSync } from 'node:child_process';
29
+ import { fileURLToPath } from 'node:url';
30
+
31
+ import { detectDoubleFire, invokedDirectly, renderGithubHooksText } from './lib/customer-kit.mjs';
32
+
33
+ export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
34
+ export const HOOKS_REL = ['.github', 'hooks', 'heyspex.json'];
35
+ const FALLBACK = { marketplace: 'heyspex-marketplace', plugin: 'heyspex' };
36
+
37
+ const fsDeps = {
38
+ readFile: (p) => {
39
+ try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
40
+ },
41
+ listDir: (p) => {
42
+ try { return fs.readdirSync(p); } catch { return []; }
43
+ },
44
+ };
45
+
46
+ /** Tiny .mjs twin of install.ts resolvePluginSpec: names come from the package's marketplace.json. */
47
+ export function resolvePluginSpec(platformRoot) {
48
+ const manifest = path.join(platformRoot, '.claude-plugin', 'marketplace.json');
49
+ const raw = fsDeps.readFile(manifest);
50
+ const out = { manifest, present: raw !== null, marketplace: FALLBACK.marketplace, plugin: FALLBACK.plugin };
51
+ if (raw !== null) {
52
+ try {
53
+ const j = JSON.parse(raw);
54
+ if (typeof j.name === 'string' && j.name.trim()) out.marketplace = j.name.trim();
55
+ const p = Array.isArray(j.plugins) ? j.plugins.find((x) => typeof x?.name === 'string' && x.name.trim()) : null;
56
+ if (p) out.plugin = p.name.trim();
57
+ } catch { /* keep fallback names */ }
58
+ }
59
+ out.spec = `${out.plugin}@${out.marketplace}`;
60
+ return out;
61
+ }
62
+
63
+ /** Walk up from `start` to the nearest dir holding .git or heyspex.yaml; else `start`. */
64
+ export function findRepoRoot(start) {
65
+ let dir = path.resolve(start);
66
+ for (;;) {
67
+ if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'heyspex.yaml'))) return dir;
68
+ const up = path.dirname(dir);
69
+ if (up === dir) return path.resolve(start);
70
+ dir = up;
71
+ }
72
+ }
73
+
74
+ function parseArgs(argv) {
75
+ const a = { hooks: false, check: false, uninstall: false, print: false, json: false, target: null, bad: null };
76
+ for (let i = 0; i < argv.length; i++) {
77
+ const x = argv[i];
78
+ if (x === '--hooks') a.hooks = true;
79
+ else if (x === '--check') a.check = true;
80
+ else if (x === '--uninstall') a.uninstall = true;
81
+ else if (x === '--print') a.print = true;
82
+ else if (x === '--json') a.json = true;
83
+ else if (x === '--target') {
84
+ a.target = argv[++i] ?? null;
85
+ if (!a.target) a.bad = '--target needs a directory';
86
+ } else if (x.startsWith('--target=')) a.target = x.slice(9);
87
+ else a.bad = `unknown argument ${x}`;
88
+ }
89
+ return a;
90
+ }
91
+
92
+ function copilotRun(args) {
93
+ try {
94
+ // Windows needs a shell to resolve copilot.cmd; args are constants or the package path, so quote the latter.
95
+ const win = process.platform === 'win32';
96
+ const argv = win ? args.map((x) => (/[\s&()^]/.test(x) ? `"${x}"` : x)) : args;
97
+ const r = spawnSync('copilot', argv, { encoding: 'utf8', shell: win, timeout: 120000 });
98
+ return { status: r.error ? 127 : (r.status ?? 1), out: `${r.stdout ?? ''}${r.stderr ?? ''}`, missing: !!r.error };
99
+ } catch {
100
+ return { status: 127, out: '', missing: true };
101
+ }
102
+ }
103
+
104
+ function copilotOnPath() {
105
+ const r = copilotRun(['--version']);
106
+ return !r.missing && r.status === 0;
107
+ }
108
+
109
+ const q = (s) => `'${s.replace(/'/g, "''")}'`;
110
+ const nextCommand = () => 'npx heyspex setup copilot';
111
+
112
+ /** Is the heyspex plugin installed for Copilot? Combines detectDoubleFire evidence with `copilot plugin list`. */
113
+ function pluginEvidence(targetRoot, copilotAvailable) {
114
+ const df = detectDoubleFire({ targetRoot, home: os.homedir(), deps: fsDeps });
115
+ // Only Copilot-side evidence counts: a Claude Code plugin install (~/.claude) is not a Copilot install.
116
+ const evidence = df.plugin === true ? df.evidence.filter((e) => /[/\\]\.copilot[/\\]/.test(e)) : [];
117
+ let plugin = evidence.length > 0;
118
+ if (copilotAvailable) {
119
+ const r = copilotRun(['plugin', 'list']);
120
+ if (r.status === 0 && /heyspex/i.test(r.out)) {
121
+ plugin = true;
122
+ evidence.push('`copilot plugin list` names heyspex');
123
+ }
124
+ }
125
+ return { plugin, evidence };
126
+ }
127
+
128
+ /** Parse the hooks file; ours = every command row is a HeySpex row. */
129
+ function hooksFileState(target) {
130
+ const file = path.join(target, ...HOOKS_REL);
131
+ const raw = fsDeps.readFile(file);
132
+ if (raw === null) return { file, exists: false, ours: false, current: false };
133
+ let ours = false;
134
+ try {
135
+ const j = JSON.parse(raw);
136
+ const cmds = Object.values(j?.hooks ?? {}).flat().map((r) => r?.bash ?? r?.command).filter((c) => typeof c === 'string');
137
+ ours = cmds.length > 0 && cmds.every((c) => /claude-hooks\/_run\.mjs/.test(c));
138
+ } catch { /* foreign / unparseable */ }
139
+ return { file, exists: true, ours, current: raw.replace(/\r\n/g, '\n') === renderGithubHooksText() };
140
+ }
141
+
142
+ function manualCommands(spec) {
143
+ return [
144
+ `copilot plugin marketplace add ${q(PACKAGE_ROOT)}`,
145
+ `copilot plugin install ${spec}`,
146
+ ];
147
+ }
148
+
149
+ function doPrint(out) {
150
+ out(`Next: run \`${nextCommand()}\` (plugin) or \`${nextCommand()} --hooks\` (cloud agent) in this repo, then /restart.`);
151
+ const doc = fsDeps.readFile(path.join(PACKAGE_ROOT, 'docs', 'copilot-plugin-install.md'));
152
+ if (doc !== null) { out(''); out(doc.replace(/\s+$/, '')); return 0; }
153
+ const spec = resolvePluginSpec(PACKAGE_ROOT);
154
+ out('');
155
+ out('# Installing HeySpex in GitHub Copilot CLI (built-in fallback; docs/copilot-plugin-install.md not found)');
156
+ out('Run these, in order, and stop at the first failure:');
157
+ for (const c of manualCommands(spec.spec)) out(` ${c}`);
158
+ out(' npx hs doctor');
159
+ return 0;
160
+ }
161
+
162
+ function doCheck(a, target, out) {
163
+ const spec = resolvePluginSpec(PACKAGE_ROOT);
164
+ const hasCopilot = copilotOnPath();
165
+ const ev = pluginEvidence(target, hasCopilot);
166
+ const hooks = hooksFileState(target);
167
+ const route = ev.plugin && hooks.exists ? 'both' : ev.plugin ? 'plugin' : hooks.exists ? 'hooks' : 'none';
168
+ const problems = [];
169
+ if (route === 'both') problems.push('double-fire: the plugin AND .github/hooks/heyspex.json are active, so every hook runs twice. Run `npx heyspex setup copilot --uninstall` (hooks file) or `copilot plugin uninstall heyspex`.');
170
+ if (!spec.present) problems.push(`marketplace manifest missing: ${spec.manifest}`);
171
+ if (route === 'hooks' && !hooks.ours) problems.push('.github/hooks/heyspex.json is not a HeySpex file');
172
+ if (route === 'hooks' && hooks.ours && !hooks.current) problems.push('.github/hooks/heyspex.json is stale; re-run `npx heyspex setup copilot --hooks`');
173
+ if (route === 'none') problems.push(`no route active; run \`${nextCommand()}\` (plugin) or \`${nextCommand()} --hooks\``);
174
+ const report = { route, plugin: ev.plugin, pluginEvidence: ev.evidence, hooksFile: hooks.exists, hooksFilePath: hooks.file, hooksCurrent: hooks.current, marketplaceManifest: spec.present, marketplacePath: spec.manifest, copilotOnPath: hasCopilot, problems };
175
+ if (a.json) out(JSON.stringify(report, null, 2));
176
+ else {
177
+ out(`hs setup copilot --check (target ${target})`);
178
+ out(` route: ${route}${route === 'hooks' ? ' (hooks route: .github/hooks/heyspex.json)' : route === 'plugin' ? ' (plugin route)' : ''}`);
179
+ out(` plugin installed: ${ev.plugin ? 'yes' : 'no'}${ev.evidence.length ? ` (${ev.evidence.join('; ')})` : ''}`);
180
+ out(` hooks file: ${hooks.exists ? `present${hooks.ours ? '' : ' (not HeySpex)'}${hooks.current ? ', current' : ', stale'}` : 'absent'}`);
181
+ out(` marketplace manifest in package: ${spec.present ? 'present' : 'MISSING'}`);
182
+ out(` copilot CLI on PATH: ${hasCopilot ? 'yes' : 'no'}`);
183
+ for (const p of problems) out(` PROBLEM: ${p}`);
184
+ if (!problems.length) out(' ok');
185
+ }
186
+ return problems.length ? 1 : 0;
187
+ }
188
+
189
+ function doHooks(target, out, err) {
190
+ const ev = pluginEvidence(target, copilotOnPath());
191
+ if (ev.plugin) {
192
+ err(`hs setup copilot --hooks refused: the heyspex plugin is already installed for Copilot (${ev.evidence.join('; ')}).`);
193
+ err('Plugin OR hooks, never both (every hook would fire twice). Remove the plugin first: `copilot plugin uninstall heyspex`.');
194
+ return 2;
195
+ }
196
+ const hooks = hooksFileState(target);
197
+ if (hooks.exists && !hooks.ours) {
198
+ err(`hs setup copilot --hooks refused: ${hooks.file} exists and is not a HeySpex file; not overwriting it.`);
199
+ return 2;
200
+ }
201
+ if (hooks.exists && hooks.current) {
202
+ out(`already current: ${hooks.file}`);
203
+ } else {
204
+ fs.mkdirSync(path.dirname(hooks.file), { recursive: true });
205
+ fs.writeFileSync(hooks.file, renderGithubHooksText(), 'utf8');
206
+ out(`${hooks.exists ? 'updated' : 'wrote'} ${hooks.file}`);
207
+ }
208
+ out('Commit .github/hooks/heyspex.json so the Copilot cloud agent and teammates get it. Commands resolve node_modules/heyspex first and exit 0 when it is absent.');
209
+ out('Next: `npx heyspex doctor`, then /restart in your Copilot session.');
210
+ return 0;
211
+ }
212
+
213
+ function doPlugin(target, out, err) {
214
+ const spec = resolvePluginSpec(PACKAGE_ROOT);
215
+ if (!spec.present) {
216
+ err(`hs setup copilot: marketplace manifest not found at ${spec.manifest}`);
217
+ err('The heyspex package looks incomplete. Reinstall it: `npm install heyspex@latest`, then re-run.');
218
+ return 1;
219
+ }
220
+ const hooks = hooksFileState(target);
221
+ if (hooks.exists) {
222
+ out(`WARNING: ${hooks.file} already exists. Plugin OR hooks, never both (every hook would fire twice). Remove it with \`npx heyspex setup copilot --uninstall\` after the plugin is installed.`);
223
+ }
224
+ const cmds = manualCommands(spec.spec);
225
+ if (!copilotOnPath()) {
226
+ out('`copilot` is not on PATH. Install GitHub Copilot CLI, then run these two commands:');
227
+ for (const c of cmds) out(` ${c}`);
228
+ out('Then /restart in Copilot and verify with `copilot plugin list` and `npx heyspex doctor`.');
229
+ return 0;
230
+ }
231
+ const add = copilotRun(['plugin', 'marketplace', 'add', PACKAGE_ROOT]);
232
+ if (add.status !== 0 && !/already/i.test(add.out)) {
233
+ err(`copilot plugin marketplace add failed:\n${add.out.trim().slice(-600)}`);
234
+ err('Run these by hand and report the output:');
235
+ for (const c of cmds) err(` ${c}`);
236
+ return 1;
237
+ }
238
+ const inst = copilotRun(['plugin', 'install', spec.spec]);
239
+ if (inst.status !== 0 && !/already/i.test(inst.out)) {
240
+ err(`copilot plugin install failed:\n${inst.out.trim().slice(-600)}`);
241
+ err('Run this by hand and report the output:');
242
+ err(` ${cmds[1]}`);
243
+ return 1;
244
+ }
245
+ out(`installed ${spec.spec} (marketplace from ${PACKAGE_ROOT})`);
246
+ out('Next: /restart in Copilot, then `copilot plugin list` and `npx heyspex doctor`.');
247
+ return 0;
248
+ }
249
+
250
+ function doUninstall(target, out) {
251
+ const hooks = hooksFileState(target);
252
+ if (!hooks.exists) out(`no hooks file at ${hooks.file}`);
253
+ else if (!hooks.ours) out(`left ${hooks.file} alone: it is not a HeySpex-only file`);
254
+ else { fs.rmSync(hooks.file, { force: true }); out(`removed ${hooks.file}`); }
255
+ const cmd = 'copilot plugin uninstall heyspex';
256
+ if (copilotOnPath()) {
257
+ const r = copilotRun(['plugin', 'uninstall', 'heyspex']);
258
+ out(r.status === 0 ? `ran \`${cmd}\`` : `\`${cmd}\` did not complete (it may not be installed): ${r.out.trim().slice(-200)}`);
259
+ } else out(`To remove the plugin, run: ${cmd}`);
260
+ return 0;
261
+ }
262
+
263
+ export function main(argv = process.argv.slice(2), io = {}) {
264
+ const out = io.out ?? ((s) => process.stdout.write(`${s}\n`));
265
+ const err = io.err ?? ((s) => process.stderr.write(`${s}\n`));
266
+ try {
267
+ const a = parseArgs(argv);
268
+ if (a.bad) {
269
+ err(`hs setup copilot: ${a.bad}`);
270
+ err('usage: hs setup copilot [--hooks] [--check [--json]] [--uninstall] [--print] [--target <dir>]');
271
+ return 2;
272
+ }
273
+ if (a.print) return doPrint(out);
274
+ const target = a.target ? path.resolve(a.target) : findRepoRoot(process.env.HS_SETUP_TARGET?.trim() || process.cwd());
275
+ if (a.check) return doCheck(a, target, out);
276
+ if (a.uninstall) return doUninstall(target, out);
277
+ if (a.hooks) return doHooks(target, out, err);
278
+ return doPlugin(target, out, err);
279
+ } catch (e) {
280
+ err(`hs setup copilot: unexpected failure: ${e?.message ?? e}`);
281
+ err('Manual fallback: copilot plugin marketplace add <node_modules/heyspex>; copilot plugin install heyspex@heyspex-marketplace');
282
+ return 1;
283
+ }
284
+ }
285
+
286
+ if (invokedDirectly(import.meta.url, 'install-copilot-hooks.mjs')) {
287
+ process.exit(main());
288
+ }
@@ -54,6 +54,7 @@
54
54
  * isn't installed in their execution context. Stays SYNCHRONOUS and does zero
55
55
  * network I/O: it runs on every console request.
56
56
  */
57
+ import { heyspexHome } from './heyspex-home.mjs';
57
58
  import { readFileSync } from 'node:fs';
58
59
  import { homedir } from 'node:os';
59
60
  import { join } from 'node:path';
@@ -120,7 +121,7 @@ function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
120
121
  * consumers (some of which run standalone, outside the npm workspace's
121
122
  * hoisted `node_modules`) can call it with zero new dependencies.
122
123
  */
123
- export function readCredentialsConsoleUrl({ homeDir = homedir(), readFile = defaultReadFile } = {}) {
124
+ export function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile = defaultReadFile } = {}) {
124
125
  try {
125
126
  const raw = readFile(join(homeDir, '.heyspex', 'credentials.yaml'));
126
127
  const match = String(raw ?? '').match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
@@ -241,7 +242,7 @@ function resolveModeEnvelope({ env, homeDir }) {
241
242
  export function resolveConsoleUrl({
242
243
  repoRoot,
243
244
  env = process.env,
244
- homeDir = homedir(),
245
+ homeDir = heyspexHome(),
245
246
  strict = false,
246
247
  devMode,
247
248
  log = (line) => process.stderr.write(line),