heyspex 0.2.1 → 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.
package/bin/hs.mjs CHANGED
@@ -60,6 +60,7 @@ const PKG_ROOT = path.resolve(__dirname, "..");
60
60
  export const SPAWN_TARGETS = {
61
61
  "setup:claude": { path: "scripts/install-claude-code-hooks.mjs" },
62
62
  "setup:cursor": { path: "scripts/install-cursor-hooks.mjs" },
63
+ "setup:copilot": { path: "scripts/install-copilot-hooks.mjs" },
63
64
  "skills:sync": {
64
65
  path: "scripts/sync-skills.mjs",
65
66
  devOnly: true,
@@ -565,7 +566,7 @@ function usage() {
565
566
  " hs gateway validate [slug] [--json] Validate user-authored gateway skills (.heyspex/skills/user/)",
566
567
  " hs mode on|off|status|toggle Master switch: HeySpex layer on or off (restart Claude Code)",
567
568
  " hs dev [mode:local|mode:cloud|mode:prod] Dev/prod mode + local/cloud reliance (status when bare)",
568
- " hs setup claude|cursor [--check|--uninstall] Install agent lifecycle hooks",
569
+ " hs setup claude|cursor|copilot [--check|--uninstall] Install agent lifecycle hooks (copilot also: --print, --hooks)",
569
570
  " hs skills sync|check Propagate skills to Cursor/Codex/Claude (cwd-safe)",
570
571
  " hs skills add <module-path> --name <n> --trigger </slash> [--export <n>] [--description <t>] Register a skill (extension seam)",
571
572
  " hs skills remove <name> Remove a customer-registered skill",
@@ -1070,20 +1071,33 @@ function main(argv) {
1070
1071
  const SPAWN_KEYS = {
1071
1072
  claude: "setup:claude",
1072
1073
  cursor: "setup:cursor",
1074
+ copilot: "setup:copilot",
1073
1075
  };
1074
1076
  if (!agent || !Object.hasOwn(SPAWN_KEYS, agent)) {
1075
1077
  process.stderr.write(
1076
- `hs setup: expected \`claude\` or \`cursor\` (got \`${command ?? ""}\`)\n` +
1078
+ `hs setup: expected \`claude\`, \`cursor\` or \`copilot\` (got \`${command ?? ""}\`)\n` +
1077
1079
  " hs setup claude [--check|--uninstall] Claude Code lifecycle hooks\n" +
1078
- " hs setup cursor [--check|--uninstall] Cursor CLI lifecycle hooks\n",
1080
+ " hs setup cursor [--check|--uninstall] Cursor CLI lifecycle hooks\n" +
1081
+ " hs setup copilot [--check|--uninstall|--print|--hooks] GitHub Copilot (plugin; --hooks for the cloud agent)\n",
1079
1082
  );
1080
1083
  process.exit(2);
1081
1084
  }
1082
1085
  const setupFlags = rest.filter(
1083
1086
  (a) => a === "--check" || a === "--uninstall",
1084
1087
  );
1088
+ if (agent === "copilot") {
1089
+ // Copilot takes more flags: --print, --hooks, --json, --target <dir>.
1090
+ setupFlags.length = 0;
1091
+ for (let i = 0; i < rest.length; i++) {
1092
+ const a = rest[i];
1093
+ if (["--check", "--uninstall", "--print", "--hooks", "--json"].includes(a)) setupFlags.push(a);
1094
+ else if (a === "--target" && rest[i + 1]) setupFlags.push(a, rest[++i]);
1095
+ else if (a.startsWith("--target=")) setupFlags.push(a);
1096
+ }
1097
+ }
1085
1098
  const script = spawnTargetPath(SPAWN_KEYS[agent]);
1086
- const target = vaultRoot || process.cwd();
1099
+ // Copilot sets up the repo you are standing in, never the (possibly sibling) vault root.
1100
+ const target = agent === "copilot" ? process.cwd() : vaultRoot || process.cwd();
1087
1101
  const child = spawn(process.execPath, [script, ...setupFlags], {
1088
1102
  cwd: target,
1089
1103
  stdio: "inherit",
@@ -11,6 +11,21 @@ Source of truth for the Copilot commands and file layout:
11
11
  Hook behaviour under Copilot: <https://docs.github.com/en/copilot/reference/hooks-configuration>.
12
12
  Both pages were read on 2026-10-16. The plugin itself is documented in `node_modules/heyspex/plugins/heyspex/README.md`.
13
13
 
14
+ ## 0. The short version
15
+
16
+ From the root of your repo, after `npm install heyspex`:
17
+
18
+ ```bash
19
+ npx heyspex setup copilot # plugin route (Copilot CLI): registers the marketplace and installs the plugin
20
+ npx heyspex setup copilot --hooks # hooks route (Copilot cloud agent / no plugin): writes .github/hooks/heyspex.json
21
+ npx heyspex setup copilot --check # which route is active; warns if both are
22
+ npx heyspex setup copilot --uninstall
23
+ ```
24
+
25
+ To have an agent do it, paste it the output of `npx heyspex setup copilot --print` (this document,
26
+ preceded by the exact next command). If `copilot` is not on PATH, the plugin route prints the two
27
+ commands to run instead. Pick ONE route, never both. The manual commands below remain the fallback.
28
+
14
29
  ## 1. Prerequisites
15
30
 
16
31
  The heyspex-app repository is not required and is not available to customers. Everything below
@@ -42,10 +57,10 @@ every hook runs twice. Choose one.
42
57
  package and installs the plugin:
43
58
 
44
59
  ```bash
45
- npx hs install --target . --copilot-plugin-apply
60
+ npx heyspex setup copilot
46
61
  ```
47
62
 
48
- Without `npx hs`, the equivalent Copilot commands are:
63
+ (`npx hs install --target . --copilot-plugin-apply` does the same.) By hand, the equivalent Copilot commands are:
49
64
 
50
65
  ```bash
51
66
  copilot plugin marketplace add ./node_modules/heyspex
@@ -63,10 +78,10 @@ copilot plugin list
63
78
  so a checkout without `npm install` is never blocked.
64
79
 
65
80
  ```bash
66
- npx hs install --target . --copilot-hooks
81
+ npx heyspex setup copilot --hooks
67
82
  ```
68
83
 
69
- If you pass `--copilot-hooks` together with `--copilot-plugin[-apply]`, the installer warns and
84
+ (`npx hs install --target . --copilot-hooks` does the same.) `setup copilot --hooks` refuses when the plugin is already installed. If you pass `--copilot-hooks` together with `--copilot-plugin[-apply]`, the installer warns and
70
85
  skips the hooks file.
71
86
 
72
87
  Then restart the Copilot session (`/restart`) so hooks load, and verify:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heyspex",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "HeySpex CLI + MCP server — connect Claude Code, Cursor, and any MCP client to your HeySpex spec vault",
5
5
  "keywords": [
6
6
  "heyspex",
@@ -72,6 +72,7 @@
72
72
  "LICENSE",
73
73
  "scripts/install-claude-code-hooks.mjs",
74
74
  "scripts/install-cursor-hooks.mjs",
75
+ "scripts/install-copilot-hooks.mjs",
75
76
  "scripts/lib/customer-kit.mjs",
76
77
  "scripts/lib/hook-dispositions.mjs",
77
78
  "scripts/lib/template-pin.mjs",
@@ -37,12 +37,15 @@ plugin source `./plugins/heyspex`), the hooks and the MCP server. From the root
37
37
  ```bash
38
38
  npm install heyspex # or: npm install ./heyspex-<version>.tgz
39
39
  npx hs login
40
- npx hs install --target . --plugin-apply # Claude Code
41
- npx hs install --target . --copilot-plugin-apply # GitHub Copilot CLI (plugin) ...
42
- npx hs install --target . --copilot-hooks # ... OR repo hooks in .github/hooks, never both
40
+ npx heyspex setup copilot # GitHub Copilot CLI: plugin route ...
41
+ npx heyspex setup copilot --hooks # ... OR cloud agent: .github/hooks/heyspex.json, never both
42
+ npx heyspex setup copilot --print # paste this to your agent: next command + the full guide
43
+ npx hs install --target . --plugin-apply # Claude Code
43
44
  ```
44
45
 
45
- By hand: `claude plugin marketplace add ./node_modules/heyspex` /
46
+ `setup copilot --check` shows which route is active; `--uninstall` removes the hooks file and prints `copilot plugin uninstall heyspex`.
47
+
48
+ Fallback, by hand: `claude plugin marketplace add ./node_modules/heyspex` /
46
49
  `copilot plugin marketplace add ./node_modules/heyspex`, then `... plugin install heyspex@heyspex-marketplace`.
47
50
  Restart the session (`/restart` in Copilot), then check `copilot plugin list` and `npx hs doctor`.
48
51
  Full walkthrough: `node_modules/heyspex/docs/copilot-plugin-install.md`. The sections below describe the same plugin from the
@@ -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
+ }