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 +18 -4
- package/docs/copilot-plugin-install.md +19 -4
- package/package.json +2 -1
- package/plugins/heyspex/README.md +7 -4
- package/scripts/install-copilot-hooks.mjs +288 -0
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 \`
|
|
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
|
-
|
|
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
|
|
60
|
+
npx heyspex setup copilot
|
|
46
61
|
```
|
|
47
62
|
|
|
48
|
-
|
|
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
|
|
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.
|
|
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
|
|
41
|
-
npx
|
|
42
|
-
npx
|
|
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
|
-
|
|
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
|
+
}
|