@sallmarta/eye-hate-agent 1.0.8 → 1.0.10
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/README.md +14 -9
- package/bin/eha.js +283 -114
- package/docs/templates/reusable-prompts/00-project-docs-bootstrap.md +13 -9
- package/docs/templates/reusable-prompts/00-project-docs-refresh.md +25 -12
- package/package.json +4 -3
- package/src/engine/index.js +6 -2
- package/src/engine/install.js +161 -0
- package/src/engine/runtime-adapters.js +142 -3
- package/src/engine/state.js +84 -0
package/README.md
CHANGED
|
@@ -82,10 +82,10 @@ The EHA CLI provides a lightweight, frictionless setup and maintenance toolbelt:
|
|
|
82
82
|
|
|
83
83
|
| Command | Primary Purpose |
|
|
84
84
|
| :--- | :--- |
|
|
85
|
-
| `eha
|
|
86
|
-
| `eha init <agent>` | Directly initiates the EHA project setup for a specific agent (e.g. `copilot`, `claude`, `antigravity`). |
|
|
85
|
+
| `eha` | **Unified wizard**: banner → agent selection → scope selection (project/device) → install. |
|
|
87
86
|
| `eha doctor` | Performs a health check verifying that all generated files are present and intact. |
|
|
88
|
-
| `eha remove [agent]` | Safely deletes EHA's generated contract files for the specified agent (or all agents if omitted), along with configuration files. |
|
|
87
|
+
| `eha remove [agent]` | Safely deletes EHA's generated project-level contract files for the specified agent (or all agents if omitted), along with configuration files. |
|
|
88
|
+
| `eha uninstall` | Safely deletes EHA's generated device-level contract files from your machine. |
|
|
89
89
|
|
|
90
90
|
---
|
|
91
91
|
|
|
@@ -95,22 +95,27 @@ When a new version of EHA is released, simply run:
|
|
|
95
95
|
```bash
|
|
96
96
|
$ eha
|
|
97
97
|
```
|
|
98
|
-
in your repository. The engine will detect the version mismatch automatically, prompt you to
|
|
98
|
+
in your repository or from anywhere on your machine. The engine will detect the version mismatch automatically, prompt you to update the files, and update them to the latest standards seamlessly.
|
|
99
99
|
|
|
100
100
|
---
|
|
101
101
|
|
|
102
102
|
## Uninstallation
|
|
103
103
|
|
|
104
|
-
To completely remove EHA
|
|
104
|
+
To completely remove EHA:
|
|
105
105
|
|
|
106
|
-
### 1. Remove project files
|
|
107
|
-
To clean up
|
|
106
|
+
### 1. Remove project-level (Local) files
|
|
107
|
+
To clean up locally run the following command in your project root. Possible to specify a target agent (e.g. `claude`):
|
|
108
108
|
```bash
|
|
109
109
|
$ eha remove [agent]
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
### 2.
|
|
113
|
-
To
|
|
112
|
+
### 2. Remove device-level (Global) files
|
|
113
|
+
To clean up globally from your machine, run:
|
|
114
|
+
```bash
|
|
115
|
+
$ eha uninstall
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 3. Remove the CLI Completely
|
|
114
119
|
```bash
|
|
115
120
|
$ npm uninstall -g @sallmarta/eye-hate-agent
|
|
116
121
|
```
|
package/bin/eha.js
CHANGED
|
@@ -7,57 +7,24 @@ const chalk = require('chalk');
|
|
|
7
7
|
const readline = require('node:readline/promises');
|
|
8
8
|
const {
|
|
9
9
|
SUPPORTED_AGENT_IDS,
|
|
10
|
+
deviceManifestExists,
|
|
10
11
|
doctor,
|
|
11
12
|
findRepoRoot,
|
|
12
13
|
initProject,
|
|
14
|
+
installDevice,
|
|
13
15
|
listSupportedRuntimes,
|
|
14
16
|
listWorkflows,
|
|
15
17
|
readConfig,
|
|
18
|
+
readDeviceManifest,
|
|
16
19
|
readProjectManifest,
|
|
17
20
|
removeProject,
|
|
21
|
+
uninstallDevice,
|
|
18
22
|
} = require('../src/engine');
|
|
19
23
|
|
|
20
24
|
const pkg = require('../package.json');
|
|
21
25
|
|
|
22
26
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
23
27
|
|
|
24
|
-
async function promptAgentChoice(currentAgent) {
|
|
25
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
26
|
-
try {
|
|
27
|
-
const runtimes = listSupportedRuntimes();
|
|
28
|
-
const defaultIndex = 1;
|
|
29
|
-
|
|
30
|
-
console.log('');
|
|
31
|
-
console.log('Which agent?');
|
|
32
|
-
for (let i = 0; i < runtimes.length; i++) {
|
|
33
|
-
console.log(` ${i + 1}. ${runtimes[i].name}`);
|
|
34
|
-
}
|
|
35
|
-
console.log(` ${runtimes.length + 1}. All Agents`);
|
|
36
|
-
|
|
37
|
-
const maxChoice = runtimes.length + 1;
|
|
38
|
-
const answer = await rl.question(
|
|
39
|
-
`Choose [1-${maxChoice}] (default: ${defaultIndex}): `,
|
|
40
|
-
);
|
|
41
|
-
const trimmed = answer.trim();
|
|
42
|
-
|
|
43
|
-
if (!trimmed) return runtimes[defaultIndex - 1].id;
|
|
44
|
-
|
|
45
|
-
const num = parseInt(trimmed, 10);
|
|
46
|
-
if (num >= 1 && num <= runtimes.length) return runtimes[num - 1].id;
|
|
47
|
-
if (num === maxChoice) return 'all';
|
|
48
|
-
|
|
49
|
-
const normalized = trimmed.toLowerCase();
|
|
50
|
-
if (normalized === 'all') return 'all';
|
|
51
|
-
|
|
52
|
-
const match = runtimes.find(r => r.id === normalized);
|
|
53
|
-
if (match) return match.id;
|
|
54
|
-
|
|
55
|
-
return trimmed.toLowerCase();
|
|
56
|
-
} finally {
|
|
57
|
-
rl.close();
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
28
|
async function promptConfirm(message, defaultYes = false) {
|
|
62
29
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
63
30
|
try {
|
|
@@ -138,116 +105,281 @@ function printDoctorSummary(result) {
|
|
|
138
105
|
console.log('');
|
|
139
106
|
}
|
|
140
107
|
|
|
141
|
-
|
|
108
|
+
function printBanner() {
|
|
109
|
+
const ehaRed = chalk.hex('#A61E14').bold;
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(ehaRed(' ███████╗██╗ ██╗ █████╗ '));
|
|
112
|
+
console.log(ehaRed(' ██╔════╝██║ ██║██╔══██╗'));
|
|
113
|
+
console.log(ehaRed(' █████╗ ███████║███████║'));
|
|
114
|
+
console.log(ehaRed(' ██╔══╝ ██╔══██║██╔══██║'));
|
|
115
|
+
console.log(ehaRed(' ███████╗██║ ██║██║ ██║'));
|
|
116
|
+
console.log(ehaRed(' ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝'));
|
|
117
|
+
console.log(chalk.gray(` v${pkg.version}`));
|
|
118
|
+
console.log('');
|
|
119
|
+
}
|
|
142
120
|
|
|
143
|
-
async function
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
121
|
+
async function checkForUpdates() {
|
|
122
|
+
try {
|
|
123
|
+
const https = require('node:https');
|
|
124
|
+
const data = await new Promise((resolve, reject) => {
|
|
125
|
+
const req = https.get(
|
|
126
|
+
'https://registry.npmjs.org/@sallmarta/eye-hate-agent/latest',
|
|
127
|
+
{ timeout: 3000 },
|
|
128
|
+
(res) => {
|
|
129
|
+
let body = '';
|
|
130
|
+
res.on('data', (chunk) => (body += chunk));
|
|
131
|
+
res.on('end', () => resolve(JSON.parse(body)));
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
req.on('error', reject);
|
|
135
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const latest = data.version;
|
|
139
|
+
if (latest && latest !== pkg.version) {
|
|
140
|
+
console.log(
|
|
141
|
+
chalk.yellow(` Update available: ${pkg.version} → ${latest}`) +
|
|
142
|
+
chalk.gray(` — run npm i -g @sallmarta/eye-hate-agent`)
|
|
143
|
+
);
|
|
144
|
+
console.log('');
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// Silently ignore — no network, no problem
|
|
148
|
+
}
|
|
149
|
+
}
|
|
147
150
|
|
|
148
|
-
|
|
149
|
-
const
|
|
151
|
+
function parseAgentInput(input, runtimes) {
|
|
152
|
+
const trimmed = (input || '').trim();
|
|
153
|
+
if (!trimmed) return [runtimes[0].id];
|
|
154
|
+
if (trimmed === '0' || trimmed.toLowerCase() === 'all') return SUPPORTED_AGENT_IDS.slice();
|
|
150
155
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
156
|
+
const parts = trimmed.split(',').map(p => p.trim()).filter(Boolean);
|
|
157
|
+
const ids = new Set();
|
|
158
|
+
|
|
159
|
+
for (const part of parts) {
|
|
160
|
+
const num = parseInt(part, 10);
|
|
161
|
+
if (num === 0) return SUPPORTED_AGENT_IDS.slice();
|
|
162
|
+
if (num >= 1 && num <= runtimes.length) {
|
|
163
|
+
ids.add(runtimes[num - 1].id);
|
|
154
164
|
} else {
|
|
155
|
-
|
|
165
|
+
const normalized = part.toLowerCase();
|
|
166
|
+
if (normalized === 'all') return SUPPORTED_AGENT_IDS.slice();
|
|
167
|
+
const match = runtimes.find(r => r.id === normalized);
|
|
168
|
+
if (match) ids.add(match.id);
|
|
169
|
+
else ids.add(normalized);
|
|
156
170
|
}
|
|
157
171
|
}
|
|
158
172
|
|
|
159
|
-
|
|
173
|
+
return ids.size > 0 ? [...ids] : [runtimes[0].id];
|
|
174
|
+
}
|
|
160
175
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
176
|
+
async function promptAgentChoice() {
|
|
177
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
178
|
+
try {
|
|
179
|
+
const runtimes = listSupportedRuntimes();
|
|
180
|
+
|
|
181
|
+
console.log('');
|
|
182
|
+
console.log('Which agent(s)?');
|
|
183
|
+
for (let i = 0; i < runtimes.length; i++) {
|
|
184
|
+
console.log(` ${i + 1}. ${runtimes[i].name}`);
|
|
185
|
+
}
|
|
186
|
+
console.log(` 0. All Agents`);
|
|
187
|
+
|
|
188
|
+
const answer = await rl.question(
|
|
189
|
+
`Choose agent(s) — comma-separate for multiple (e.g. 1,3): `,
|
|
190
|
+
);
|
|
191
|
+
return parseAgentInput(answer, runtimes);
|
|
192
|
+
} finally {
|
|
193
|
+
rl.close();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function promptScope() {
|
|
198
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
199
|
+
try {
|
|
200
|
+
console.log('');
|
|
201
|
+
console.log('Where should EHA be installed?');
|
|
202
|
+
console.log(` 1. This project ${chalk.gray('(files in .claude/, .github/, .agents/)')}`);
|
|
203
|
+
console.log(` 2. Your device — all projects ${chalk.gray('(files in ~/.claude/, ~/.copilot/, ~/.gemini/)')}`);
|
|
204
|
+
|
|
205
|
+
const answer = await rl.question(`Choose [1-2]: `);
|
|
206
|
+
const trimmed = answer.trim();
|
|
207
|
+
|
|
208
|
+
if (trimmed === '2' || trimmed.toLowerCase() === 'device' || trimmed.toLowerCase() === 'global') {
|
|
209
|
+
return 'device';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return 'project';
|
|
213
|
+
} finally {
|
|
214
|
+
rl.close();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function runDeviceInstall(agentIds) {
|
|
219
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
220
|
+
|
|
221
|
+
if (isInteractive && deviceManifestExists()) {
|
|
222
|
+
const manifest = readDeviceManifest();
|
|
223
|
+
const installedAgentIds = (manifest.agents || []).map(a => a.id);
|
|
224
|
+
if (installedAgentIds.length > 0) {
|
|
225
|
+
const listStr = installedAgentIds.map(a => chalk.cyan(a)).join(', ');
|
|
226
|
+
const ver = manifest.packageVersion || 'unknown';
|
|
227
|
+
const confirmed = await promptConfirm(
|
|
228
|
+
`EHA is already installed on your device (${listStr}, v${ver}). Update / overwrite?`,
|
|
167
229
|
true,
|
|
168
230
|
);
|
|
169
|
-
if (!
|
|
231
|
+
if (!confirmed) {
|
|
170
232
|
console.log('Skipped.');
|
|
171
233
|
return;
|
|
172
234
|
}
|
|
173
235
|
}
|
|
236
|
+
}
|
|
174
237
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const result = initProject({ rootDir, agentId: id });
|
|
179
|
-
fileCount += result.fileCount;
|
|
180
|
-
}
|
|
238
|
+
console.log('');
|
|
239
|
+
console.log(chalk.blue('Installing EHA to your device...'));
|
|
240
|
+
console.log('');
|
|
181
241
|
|
|
242
|
+
const result = installDevice({ agentIds });
|
|
243
|
+
|
|
244
|
+
const agentNames = { claude: 'Claude', copilot: 'GitHub Copilot', antigravity: 'Antigravity' };
|
|
245
|
+
|
|
246
|
+
for (const agentId of result.agentIds) {
|
|
247
|
+
const agentResult = result.results[agentId];
|
|
248
|
+
console.log(` ${chalk.cyan(agentNames[agentId] || agentId)}:`);
|
|
249
|
+
for (const file of agentResult.files) {
|
|
250
|
+
const suffix = file.isSentinel ? ` (EHA rules block ${file.action})` : '';
|
|
251
|
+
console.log(` ${chalk.green('✓')} ${file.displayPath}${chalk.gray(suffix)}`);
|
|
252
|
+
}
|
|
182
253
|
console.log('');
|
|
183
|
-
console.log(chalk.green('✓ EHA is ready for all agents.'));
|
|
184
|
-
console.log(` Agents : ${SUPPORTED_AGENT_IDS.map(a => chalk.cyan(a)).join(', ')}`);
|
|
185
|
-
console.log(` Files : ${fileCount} file(s) generated`);
|
|
186
|
-
console.log('');
|
|
187
|
-
console.log('Open Agents in this project and run ' + chalk.cyan('/eha-help') + ' to get started or run ' + chalk.cyan('eha doctor') + ' to see all files.');
|
|
188
|
-
console.log('');
|
|
189
|
-
return;
|
|
190
254
|
}
|
|
191
255
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
256
|
+
console.log(chalk.green('✓ EHA installed to your device!'));
|
|
257
|
+
console.log(` Open your agent in any project and run ${chalk.cyan('/eha-help')} to get started.`);
|
|
258
|
+
console.log('');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function runProjectInstall(agentIds) {
|
|
262
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
263
|
+
|
|
264
|
+
let rootDir;
|
|
265
|
+
try {
|
|
266
|
+
rootDir = findRepoRoot(process.cwd());
|
|
267
|
+
} catch {
|
|
195
268
|
console.error(
|
|
196
|
-
chalk.red(
|
|
197
|
-
|
|
269
|
+
chalk.red('No project root found.') +
|
|
270
|
+
' Run ' + chalk.cyan('npm init -y') + ' or ' + chalk.cyan('git init') + ' first.',
|
|
198
271
|
);
|
|
199
272
|
process.exit(1);
|
|
200
273
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const installedAgents = config.agents ||
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
const confirm = await promptConfirm(msg, true);
|
|
216
|
-
if (!confirm) {
|
|
217
|
-
console.log('Skipped.');
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
} else if (installedAgents.length > 0) {
|
|
221
|
-
const listStr = installedAgents.map(a => chalk.cyan(a)).join(', ');
|
|
222
|
-
const confirm = await promptConfirm(
|
|
223
|
-
`EHA is set up for: ${listStr}. Add ${chalk.cyan(agentId)}?`,
|
|
224
|
-
true,
|
|
225
|
-
);
|
|
226
|
-
if (!confirm) {
|
|
227
|
-
console.log('Skipped.');
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
274
|
+
|
|
275
|
+
const config = readConfig(rootDir);
|
|
276
|
+
const installedAgents = config.agents || [];
|
|
277
|
+
|
|
278
|
+
if (isInteractive && installedAgents.length > 0) {
|
|
279
|
+
const listStr = installedAgents.map(a => chalk.cyan(a)).join(', ');
|
|
280
|
+
const confirmed = await promptConfirm(
|
|
281
|
+
`EHA is set up for: ${listStr}. Overwrite / add selected agents?`,
|
|
282
|
+
true,
|
|
283
|
+
);
|
|
284
|
+
if (!confirmed) {
|
|
285
|
+
console.log('Skipped.');
|
|
286
|
+
return;
|
|
230
287
|
}
|
|
231
288
|
}
|
|
232
289
|
|
|
233
|
-
|
|
234
|
-
|
|
290
|
+
console.log('');
|
|
291
|
+
console.log(chalk.blue('Installing EHA to this project...'));
|
|
292
|
+
|
|
293
|
+
let totalFiles = 0;
|
|
294
|
+
const allFiles = [];
|
|
295
|
+
for (const id of agentIds) {
|
|
296
|
+
const result = initProject({ rootDir, agentId: id });
|
|
297
|
+
totalFiles += result.fileCount;
|
|
298
|
+
allFiles.push(...result.files);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
console.log('');
|
|
302
|
+
console.log(chalk.green(`✓ EHA is ready.`));
|
|
303
|
+
console.log(` Agent${agentIds.length > 1 ? 's' : ''} : ${agentIds.map(a => chalk.cyan(a)).join(', ')}`);
|
|
304
|
+
console.log(` Files : ${totalFiles} file(s) generated`);
|
|
305
|
+
for (const f of allFiles) {
|
|
306
|
+
console.log(` ${chalk.gray(f)}`);
|
|
307
|
+
}
|
|
308
|
+
console.log('');
|
|
309
|
+
console.log(`Open your agent in this project and run ${chalk.cyan('/eha-help')} to get started.`);
|
|
310
|
+
console.log('');
|
|
235
311
|
}
|
|
236
312
|
|
|
237
313
|
// ─── CLI definition ────────────────────────────────────────────────────────────
|
|
238
314
|
|
|
239
315
|
program.name('eha').description('Eye Hate Agent (EHA) — AI workflow toolkit').version(pkg.version);
|
|
240
316
|
|
|
241
|
-
// Bare `eha` / `eyehateagent` with no subcommand runs the init wizard.
|
|
242
317
|
program.action(async () => {
|
|
243
|
-
|
|
318
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
319
|
+
|
|
320
|
+
if (!isInteractive) {
|
|
321
|
+
program.outputHelp();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
printBanner();
|
|
326
|
+
await checkForUpdates();
|
|
327
|
+
|
|
328
|
+
const agentIds = await promptAgentChoice();
|
|
329
|
+
|
|
330
|
+
for (const id of agentIds) {
|
|
331
|
+
if (!SUPPORTED_AGENT_IDS.includes(id)) {
|
|
332
|
+
console.error(
|
|
333
|
+
chalk.red(`Unsupported agent: ${id}.`) +
|
|
334
|
+
` Choose from: ${SUPPORTED_AGENT_IDS.join(', ')}`
|
|
335
|
+
);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const scope = await promptScope();
|
|
341
|
+
|
|
342
|
+
if (scope === 'device') {
|
|
343
|
+
await runDeviceInstall(agentIds);
|
|
344
|
+
} else {
|
|
345
|
+
await runProjectInstall(agentIds);
|
|
346
|
+
}
|
|
244
347
|
});
|
|
245
348
|
|
|
246
349
|
program
|
|
247
|
-
.command('init [agent]')
|
|
248
|
-
.description(
|
|
350
|
+
.command('init [agent]', { hidden: true })
|
|
351
|
+
.description('(hidden) Project-level install — alias for the unified wizard with scope=project')
|
|
249
352
|
.action(async (agentArg) => {
|
|
250
|
-
|
|
353
|
+
const rootDir = resolveRootDir();
|
|
354
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
355
|
+
|
|
356
|
+
let agentIds;
|
|
357
|
+
if (agentArg) {
|
|
358
|
+
const normalized = String(agentArg).trim().toLowerCase();
|
|
359
|
+
agentIds = normalized === 'all' ? SUPPORTED_AGENT_IDS.slice() : [normalized];
|
|
360
|
+
} else if (isInteractive) {
|
|
361
|
+
printBanner();
|
|
362
|
+
agentIds = await promptAgentChoice();
|
|
363
|
+
} else {
|
|
364
|
+
agentIds = [SUPPORTED_AGENT_IDS[0]];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let totalFiles = 0;
|
|
368
|
+
const allFiles = [];
|
|
369
|
+
for (const id of agentIds) {
|
|
370
|
+
const result = initProject({ rootDir, agentId: id });
|
|
371
|
+
totalFiles += result.fileCount;
|
|
372
|
+
allFiles.push(...result.files);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
console.log('');
|
|
376
|
+
console.log(chalk.green(`✓ EHA is ready.`));
|
|
377
|
+
console.log(` Agent${agentIds.length > 1 ? 's' : ''} : ${agentIds.map(a => chalk.cyan(a)).join(', ')}`);
|
|
378
|
+
console.log(` Files : ${totalFiles} file(s) generated`);
|
|
379
|
+
for (const f of allFiles) {
|
|
380
|
+
console.log(` ${chalk.gray(f)}`);
|
|
381
|
+
}
|
|
382
|
+
console.log('');
|
|
251
383
|
});
|
|
252
384
|
|
|
253
385
|
program
|
|
@@ -311,6 +443,37 @@ program
|
|
|
311
443
|
}
|
|
312
444
|
});
|
|
313
445
|
|
|
446
|
+
program
|
|
447
|
+
.command('uninstall')
|
|
448
|
+
.description('Remove EHA device-level files from your machine')
|
|
449
|
+
.action(async () => {
|
|
450
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
451
|
+
|
|
452
|
+
if (isInteractive) {
|
|
453
|
+
const confirmed = await promptConfirm(
|
|
454
|
+
'Remove all device-level EHA files from your machine?',
|
|
455
|
+
);
|
|
456
|
+
if (!confirmed) {
|
|
457
|
+
console.log('Aborted.');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const result = uninstallDevice();
|
|
463
|
+
|
|
464
|
+
if (result.removedFiles.length === 0) {
|
|
465
|
+
console.log(chalk.yellow('No device-level EHA installation found.'));
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
console.log('');
|
|
470
|
+
console.log(chalk.green('✓ EHA device-level files removed:'));
|
|
471
|
+
for (const f of result.removedFiles) {
|
|
472
|
+
console.log(` ${chalk.gray(f)}`);
|
|
473
|
+
}
|
|
474
|
+
console.log('');
|
|
475
|
+
});
|
|
476
|
+
|
|
314
477
|
program
|
|
315
478
|
.command('doctor')
|
|
316
479
|
.description('Show EHA status: config, agent, and generated files')
|
|
@@ -319,7 +482,13 @@ program
|
|
|
319
482
|
printDoctorSummary(doctor({ rootDir }));
|
|
320
483
|
});
|
|
321
484
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
485
|
+
if (require.main === module) {
|
|
486
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
487
|
+
console.error(chalk.red(error.message));
|
|
488
|
+
process.exitCode = 1;
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
module.exports = {
|
|
493
|
+
parseAgentInput,
|
|
494
|
+
};
|
|
@@ -98,21 +98,24 @@ Additionally, ask the user:
|
|
|
98
98
|
If yes, generate a boilerplate `foundation/changelog.md` with an initial unreleased section.
|
|
99
99
|
|
|
100
100
|
### For Brownfield Projects (with existing code):
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
101
|
+
You **MUST** check all four active development signals. Do NOT skip this step:
|
|
102
|
+
|
|
103
|
+
1. **Recent commits** — run `git log --oneline -20` or equivalent; if there are commits within the last 14 days, OR 10+ commits within the last 30 days, this signal is positive.
|
|
104
|
+
2. **Sprint/feature branches** — run `git branch -a` and look for naming patterns like `sprint/`, `phase/`, `release/`, `feature/`, `feat/`, `dev/`.
|
|
105
|
+
3. **Planning artifacts** — check for `TODO.md`, `ROADMAP.md`, `.github/ISSUE_TEMPLATE/`, issue tracker references in recent commits (e.g., `#123`, `fixes #`, `closes #`), or project board configs.
|
|
106
|
+
4. **TODO density** — grep the codebase for `TODO`, `FIXME`, `HACK` comments; if count ≥ 5, this signal is positive.
|
|
107
|
+
|
|
108
|
+
If **any one** signal is positive, you **MUST** ask the user:
|
|
109
|
+
"This project shows active development signals ([list which signals were positive and what was found]).
|
|
110
|
+
Would you like to set up `foundation/phases/` to track your development cycles?
|
|
109
111
|
If yes, describe the current and upcoming phases (or I can infer from your codebase)."
|
|
110
112
|
|
|
111
113
|
If the user provides phases:
|
|
112
114
|
- Create `foundation/phases/index.md` with the phase registry.
|
|
113
115
|
- Create individual phase files using brownfield naming: `phase-P{N}[-description].md` (e.g., `phase-P1-refactor.md`, `phase-P2-auth.md`).
|
|
114
116
|
|
|
115
|
-
If the user declines
|
|
117
|
+
If the user declines: Skip phases entirely.
|
|
118
|
+
If all four signals are negative: Skip the phases prompt but note in your output that all four active development signals were negative and no phases were offered.
|
|
116
119
|
|
|
117
120
|
Additionally, check for release signals (e.g., git tags, version updates in `package.json`, release branches). If found, ask the user:
|
|
118
121
|
"Would you like to set up a changelog (`foundation/changelog.md`) to track historical releases?"
|
|
@@ -124,6 +127,7 @@ Before finishing, check that:
|
|
|
124
127
|
2. `foundation/architecture.md` and `development/testing.md` do not conflict.
|
|
125
128
|
3. The generated documents strictly match the approved Taxonomy Tier, conditional choices, and structural definitions cataloged in the master registry.
|
|
126
129
|
4. If phases were generated, verify `foundation/phases/index.md` correctly registry-links to all individual phase files (`phase-*.md`), and each phase file has complete stable headings.
|
|
130
|
+
5. For brownfield projects: if any active development signal was positive during Step 2.5, confirm that the user was prompted about setting up phases. If this prompt was skipped, **stop and prompt the user now before finishing**.
|
|
127
131
|
|
|
128
132
|
## Inputs
|
|
129
133
|
Use the project brief, codebase, and constraints provided below to begin your analysis.
|
|
@@ -66,11 +66,21 @@ Proceed to the applicable action path.
|
|
|
66
66
|
- i18n config, locale files → development/internationalization
|
|
67
67
|
- README, inline comments, decision rationale → foundation/prd, architecture
|
|
68
68
|
23. Mark all codebase-inferred facts as `Inferred from codebase` until the user confirms them.
|
|
69
|
-
24. **Active Development & Phases Detection.** When refreshing a project that does not yet have `foundation/phases/`, check
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
24. **Active Development & Phases Detection (MANDATORY).** When refreshing a project that does not yet have `foundation/phases/`, you **MUST** check all four active development signals before proceeding to doc refresh. Do NOT skip this step. The signals are:
|
|
70
|
+
|
|
71
|
+
1. **Recent commits** — run `git log --oneline -20` or equivalent; if there are commits within the last 14 days, OR 10+ commits within the last 30 days, this signal is positive.
|
|
72
|
+
2. **Sprint/feature branches** — run `git branch -a` and look for naming patterns like `sprint/`, `phase/`, `release/`, `feature/`, `feat/`, `dev/`.
|
|
73
|
+
3. **Planning artifacts** — check for `TODO.md`, `ROADMAP.md`, `.github/ISSUE_TEMPLATE/`, issue tracker references in recent commits (e.g., `#123`, `fixes #`, `closes #`), or project board configs.
|
|
74
|
+
4. **TODO density** — grep the codebase for `TODO`, `FIXME`, `HACK` comments; if count ≥ 5, this signal is positive.
|
|
75
|
+
|
|
76
|
+
If **any one** signal is positive, you **MUST** prompt the user:
|
|
77
|
+
"This project shows active development signals ([list which signals were positive and what was found]).
|
|
78
|
+
Would you like to set up `foundation/phases/` to track your development cycles?
|
|
72
79
|
If yes, describe the current and upcoming phases (or I can infer from your codebase)."
|
|
80
|
+
|
|
73
81
|
If the user agrees, create `foundation/phases/index.md` and individual phase files using brownfield naming (`phase-P{N}[-description].md`, e.g., `phase-P1-refactor.md`).
|
|
82
|
+
If the user declines, skip phases creation entirely — but still report the detection outcome in the Output Contract.
|
|
83
|
+
If all four signals are negative, skip the phases prompt but note in your output that all four active development signals were negative and no phases were offered.
|
|
74
84
|
25. **Phases Update Workflow.** When `foundation/phases/` already exists, treat it as a living operational document:
|
|
75
85
|
- Update sprint tracker in the active phase file when sprint-related changes are detected.
|
|
76
86
|
- Mark completed phases by updating their status.
|
|
@@ -82,15 +92,16 @@ Proceed to the applicable action path.
|
|
|
82
92
|
1. Run Step 0 (Doc State Detection).
|
|
83
93
|
2. Read the change summary (if provided) or the user's intent.
|
|
84
94
|
3. **Scan the codebase** — inspect source code, configs, tests, CI/CD pipelines, and package manifests for current truth. This step is NOT optional.
|
|
85
|
-
4.
|
|
86
|
-
5. Read
|
|
87
|
-
6. Read
|
|
88
|
-
7. Read
|
|
89
|
-
8.
|
|
90
|
-
9.
|
|
91
|
-
10.
|
|
92
|
-
11. Refresh
|
|
93
|
-
12.
|
|
95
|
+
4. **Phases Detection Gate** — If `foundation/phases/` does not exist, execute Rule 24 (Active Development & Phases Detection) now. Run all four signal checks using the codebase data from step 3. If any signal is positive, prompt the user about setting up phases before continuing. If `foundation/phases/` already exists, proceed to step 5.
|
|
96
|
+
5. Read the owning project docs (if Active SDD or Mixed state).
|
|
97
|
+
6. Read `docs/project-docs/index.md` and `docs/project-docs/technical-guidelines/index.md` when present.
|
|
98
|
+
7. Read legacy/reference folders when present.
|
|
99
|
+
8. Read relevant guideline docs when the change touches technical rules.
|
|
100
|
+
9. Identify impacted dependent docs.
|
|
101
|
+
10. Cross-reference codebase findings against doc/legacy claims — resolve conflicts by prompting the user (see rule 21).
|
|
102
|
+
11. Refresh/create the owning docs first (using combined codebase + docs evidence).
|
|
103
|
+
12. Refresh summary or index docs second.
|
|
104
|
+
13. Run a consistency pass.
|
|
94
105
|
|
|
95
106
|
## Ownership Examples
|
|
96
107
|
|
|
@@ -122,6 +133,7 @@ Your result should state:
|
|
|
122
133
|
4. any remaining consistency risks or open questions
|
|
123
134
|
5. which codebase-vs-doc conflicts were resolved and how (per user direction)
|
|
124
135
|
6. the auto-detected tier (for Legacy Only / Non-SDD states), if applicable
|
|
136
|
+
7. whether active development signals were detected, which signals were positive/negative, and whether the user was prompted about `foundation/phases/` setup (include the user's response: accepted, declined, or not yet answered)
|
|
125
137
|
|
|
126
138
|
## Final Pass
|
|
127
139
|
|
|
@@ -132,6 +144,7 @@ Before finishing, check that:
|
|
|
132
144
|
3. no stale summary remains in `foundation/status.md`, `docs/project-docs/index.md`, `technical-guidelines/index.md`, or other index docs
|
|
133
145
|
4. codebase-inferred facts are clearly marked and do not silently override user-confirmed truths
|
|
134
146
|
5. the auto-detected tier (for Legacy Only / Non-SDD states) is stated in the output so the user can override it if needed
|
|
147
|
+
6. if `foundation/phases/` did not exist at the start of this refresh and any active development signal was positive, confirm that the user was prompted about setting up phases — if this prompt was skipped, **stop and prompt the user now before finishing**
|
|
135
148
|
|
|
136
149
|
## Inputs
|
|
137
150
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sallmarta/eye-hate-agent",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.10",
|
|
4
|
+
"description": "A simple Spec-Driven Development (SDD) for AI-agent-assisted project workflows.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|
|
7
7
|
},
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"README.md"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"test": "node --test"
|
|
15
|
+
"test": "node --test",
|
|
16
|
+
"postinstall": "node -e \"try{var c=require('chalk');console.log('\\n'+c.green('✓ EHA installed!')+' Run '+c.cyan('eha')+' to set up.\\n')}catch(e){console.log('\\nEHA installed! Run eha to set up.\\n')}\""
|
|
16
17
|
},
|
|
17
18
|
"keywords": [
|
|
18
19
|
"ai-agent",
|
package/src/engine/index.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
const { getWorkflow, listWorkflows } = require('./workflow-registry');
|
|
2
2
|
const { listSkills } = require('./skill-registry');
|
|
3
|
-
const { findRepoRoot, readConfig } = require('./state');
|
|
3
|
+
const { findRepoRoot, readConfig, deviceManifestExists, readDeviceManifest } = require('./state');
|
|
4
4
|
const { listSupportedRuntimes } = require('./runtime-adapters');
|
|
5
|
-
const { SUPPORTED_AGENT_IDS, doctor, initProject, readProjectManifest, removeProject } = require('./install');
|
|
5
|
+
const { SUPPORTED_AGENT_IDS, doctor, initProject, installDevice, readProjectManifest, removeProject, uninstallDevice } = require('./install');
|
|
6
6
|
|
|
7
7
|
module.exports = {
|
|
8
8
|
SUPPORTED_AGENT_IDS,
|
|
9
|
+
deviceManifestExists,
|
|
9
10
|
doctor,
|
|
10
11
|
findRepoRoot,
|
|
11
12
|
getWorkflow,
|
|
12
13
|
initProject,
|
|
14
|
+
installDevice,
|
|
13
15
|
listSkills,
|
|
14
16
|
listSupportedRuntimes,
|
|
15
17
|
listWorkflows,
|
|
16
18
|
readConfig,
|
|
19
|
+
readDeviceManifest,
|
|
17
20
|
readProjectManifest,
|
|
18
21
|
removeProject,
|
|
22
|
+
uninstallDevice,
|
|
19
23
|
};
|
package/src/engine/install.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
2
|
const path = require('node:path');
|
|
3
|
+
const os = require('node:os');
|
|
3
4
|
const { version: EHA_PACKAGE_VERSION } = require('../../package.json');
|
|
4
5
|
|
|
5
6
|
const { listWorkflows } = require('./workflow-registry');
|
|
@@ -16,6 +17,11 @@ const {
|
|
|
16
17
|
writeConfig,
|
|
17
18
|
writeJson,
|
|
18
19
|
writeText,
|
|
20
|
+
getDeviceManifestPath,
|
|
21
|
+
readDeviceManifest,
|
|
22
|
+
writeDeviceManifest,
|
|
23
|
+
upsertSentinelBlock,
|
|
24
|
+
removeSentinelBlock,
|
|
19
25
|
} = require('./state');
|
|
20
26
|
|
|
21
27
|
function resolveAgentId(agentId) {
|
|
@@ -235,10 +241,165 @@ function readProjectManifest(rootDir) {
|
|
|
235
241
|
return readManifest(manifestPath);
|
|
236
242
|
}
|
|
237
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Install EHA files to the user's device (global/user-level directories).
|
|
246
|
+
*
|
|
247
|
+
* @param {Object} options
|
|
248
|
+
* @param {string[]} options.agentIds - Array of agent IDs to install
|
|
249
|
+
* @param {string} [options.homeDir] - Override home dir (for testing)
|
|
250
|
+
* @returns {Object} Result with per-agent file lists and summary
|
|
251
|
+
*/
|
|
252
|
+
function installDevice({ agentIds, homeDir }) {
|
|
253
|
+
const home = homeDir || os.homedir();
|
|
254
|
+
const workflows = listWorkflows();
|
|
255
|
+
const skills = listSkills();
|
|
256
|
+
const results = {};
|
|
257
|
+
|
|
258
|
+
for (const agentId of agentIds) {
|
|
259
|
+
const normalizedId = resolveAgentId(agentId);
|
|
260
|
+
const adapter = getRuntimeAdapter(normalizedId);
|
|
261
|
+
|
|
262
|
+
if (typeof adapter.generateDeviceFiles !== 'function') {
|
|
263
|
+
throw new Error(`Agent '${normalizedId}' does not support device-level installation.`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const files = adapter.generateDeviceFiles(home, workflows, skills);
|
|
267
|
+
const written = [];
|
|
268
|
+
|
|
269
|
+
for (const file of files) {
|
|
270
|
+
if (file.isSentinel) {
|
|
271
|
+
const action = upsertSentinelBlock(file.absolutePath, file.content);
|
|
272
|
+
written.push({
|
|
273
|
+
absolutePath: file.absolutePath,
|
|
274
|
+
displayPath: file.absolutePath.replace(home, '~'),
|
|
275
|
+
action,
|
|
276
|
+
isSentinel: true,
|
|
277
|
+
});
|
|
278
|
+
} else {
|
|
279
|
+
ensureDir(path.dirname(file.absolutePath));
|
|
280
|
+
writeText(file.absolutePath, file.content);
|
|
281
|
+
written.push({
|
|
282
|
+
absolutePath: file.absolutePath,
|
|
283
|
+
displayPath: file.absolutePath.replace(home, '~'),
|
|
284
|
+
action: 'written',
|
|
285
|
+
isSentinel: false,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
results[normalizedId] = {
|
|
291
|
+
agentId: normalizedId,
|
|
292
|
+
files: written,
|
|
293
|
+
fileCount: written.length,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Write/update device manifest
|
|
298
|
+
const existingManifest = readDeviceManifest(home);
|
|
299
|
+
const existingAgents = existingManifest.agents || [];
|
|
300
|
+
|
|
301
|
+
const updatedAgents = [...existingAgents];
|
|
302
|
+
for (const agentId of agentIds) {
|
|
303
|
+
const normalizedId = resolveAgentId(agentId);
|
|
304
|
+
const idx = updatedAgents.findIndex(a => a.id === normalizedId);
|
|
305
|
+
const entry = {
|
|
306
|
+
id: normalizedId,
|
|
307
|
+
files: results[normalizedId].files.map(f => f.absolutePath),
|
|
308
|
+
updatedAt: new Date().toISOString(),
|
|
309
|
+
packageVersion: EHA_PACKAGE_VERSION,
|
|
310
|
+
};
|
|
311
|
+
if (idx !== -1) updatedAgents[idx] = entry;
|
|
312
|
+
else updatedAgents.push(entry);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const allFiles = new Set();
|
|
316
|
+
for (const agent of updatedAgents) {
|
|
317
|
+
for (const f of agent.files || []) allFiles.add(f);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
writeDeviceManifest({
|
|
321
|
+
manifestVersion: 1,
|
|
322
|
+
agents: updatedAgents,
|
|
323
|
+
files: [...allFiles],
|
|
324
|
+
installedAt: existingManifest.installedAt || new Date().toISOString(),
|
|
325
|
+
updatedAt: new Date().toISOString(),
|
|
326
|
+
packageVersion: EHA_PACKAGE_VERSION,
|
|
327
|
+
}, home);
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
homeDir: home,
|
|
331
|
+
agentIds: agentIds.map(a => resolveAgentId(a)),
|
|
332
|
+
results,
|
|
333
|
+
totalFiles: Object.values(results).reduce((sum, r) => sum + r.fileCount, 0),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Remove EHA device-level files.
|
|
339
|
+
* If agentId is provided, removes only that agent's files.
|
|
340
|
+
* If agentId is null, removes all device-level EHA files.
|
|
341
|
+
*/
|
|
342
|
+
function uninstallDevice({ agentId = null, homeDir } = {}) {
|
|
343
|
+
const home = homeDir || os.homedir();
|
|
344
|
+
const manifest = readDeviceManifest(home);
|
|
345
|
+
const removedFiles = [];
|
|
346
|
+
|
|
347
|
+
if (!manifest.agents || manifest.agents.length === 0) {
|
|
348
|
+
return { homeDir: home, removedFiles, message: 'No device-level EHA installation found.' };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const agentsToRemove = agentId
|
|
352
|
+
? manifest.agents.filter(a => a.id === resolveAgentId(agentId))
|
|
353
|
+
: manifest.agents;
|
|
354
|
+
|
|
355
|
+
for (const agent of agentsToRemove) {
|
|
356
|
+
for (const filePath of agent.files || []) {
|
|
357
|
+
const basename = path.basename(filePath);
|
|
358
|
+
// Sentinel files: GEMINI.md is current; CLAUDE.md is legacy (pre-1.0.10 installs)
|
|
359
|
+
if (basename === 'CLAUDE.md' || basename === 'GEMINI.md') {
|
|
360
|
+
const removed = removeSentinelBlock(filePath, home);
|
|
361
|
+
if (removed) removedFiles.push(filePath.replace(home, '~'));
|
|
362
|
+
} else {
|
|
363
|
+
removeFileIfExists(filePath);
|
|
364
|
+
removeEmptyParents(path.dirname(filePath), home);
|
|
365
|
+
removedFiles.push(filePath.replace(home, '~'));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (agentId) {
|
|
371
|
+
const remainingAgents = manifest.agents.filter(a => a.id !== resolveAgentId(agentId));
|
|
372
|
+
if (remainingAgents.length === 0) {
|
|
373
|
+
removeFileIfExists(getDeviceManifestPath(home));
|
|
374
|
+
removeEmptyParents(path.dirname(getDeviceManifestPath(home)), home);
|
|
375
|
+
} else {
|
|
376
|
+
const allFiles = new Set();
|
|
377
|
+
for (const a of remainingAgents) {
|
|
378
|
+
for (const f of a.files || []) allFiles.add(f);
|
|
379
|
+
}
|
|
380
|
+
writeDeviceManifest({
|
|
381
|
+
manifestVersion: 1,
|
|
382
|
+
agents: remainingAgents,
|
|
383
|
+
files: [...allFiles],
|
|
384
|
+
installedAt: manifest.installedAt,
|
|
385
|
+
updatedAt: new Date().toISOString(),
|
|
386
|
+
packageVersion: EHA_PACKAGE_VERSION,
|
|
387
|
+
}, home);
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
removeFileIfExists(getDeviceManifestPath(home));
|
|
391
|
+
removeEmptyParents(path.dirname(getDeviceManifestPath(home)), home);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return { homeDir: home, removedFiles };
|
|
395
|
+
}
|
|
396
|
+
|
|
238
397
|
module.exports = {
|
|
239
398
|
SUPPORTED_AGENT_IDS,
|
|
240
399
|
doctor,
|
|
241
400
|
initProject,
|
|
401
|
+
installDevice,
|
|
242
402
|
readProjectManifest,
|
|
243
403
|
removeProject,
|
|
404
|
+
uninstallDevice,
|
|
244
405
|
};
|
|
@@ -200,6 +200,57 @@ When a user asks to run an EHA workflow, use the matching prompt file below.
|
|
|
200
200
|
${workflowTable}`;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
function buildDeviceRulesContent(agentId, workflows) {
|
|
204
|
+
const rulesContent = loadRuleContent(agentId);
|
|
205
|
+
|
|
206
|
+
let routingSection = '';
|
|
207
|
+
if (agentId === 'claude') {
|
|
208
|
+
const routes = workflows
|
|
209
|
+
.map(w => `- \`${w.commandName}\` → \`~/.claude/commands/eha/eha-${w.commandName}.md\``)
|
|
210
|
+
.join('\n');
|
|
211
|
+
routingSection = `\n\n# EHA Workflow Routing\n\nWhen a user asks to run an EHA workflow, use the matching command file:\n\n${routes}`;
|
|
212
|
+
} else if (agentId === 'antigravity') {
|
|
213
|
+
const routes = workflows
|
|
214
|
+
.map(w => `- \`${w.commandName}\` → \`~/.gemini/config/global_workflows/eha-${w.commandName}.md\``)
|
|
215
|
+
.join('\n');
|
|
216
|
+
routingSection = `\n\n# EHA Workflow Routing\n\nWhen a user asks to run an EHA workflow, use the matching workflow file:\n\n${routes}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return `${rulesContent}${routingSection}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function buildCopilotDeviceSkillFile(skill) {
|
|
223
|
+
return `---
|
|
224
|
+
name: "eha-${skill.commandName}"
|
|
225
|
+
description: "EHA skill — ${skill.commandName}"
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
${EHA_COMPACT_RULES}
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
${loadSkillContent(skill)}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function buildCopilotDeviceInstructionsFile(workflows) {
|
|
236
|
+
const workflowTable = workflows
|
|
237
|
+
.map(w => `- \`${w.commandName}\` → \`~/.copilot/prompts/eha-${w.commandName}.prompt.md\``)
|
|
238
|
+
.join('\n');
|
|
239
|
+
|
|
240
|
+
return `---
|
|
241
|
+
description: "EHA workflow routing and agent rules for GitHub Copilot"
|
|
242
|
+
applyTo: "**"
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
${loadRuleContent('copilot')}
|
|
246
|
+
|
|
247
|
+
# EHA Workflow Routing
|
|
248
|
+
|
|
249
|
+
When a user asks to run an EHA workflow, use the matching prompt file:
|
|
250
|
+
|
|
251
|
+
${workflowTable}`;
|
|
252
|
+
}
|
|
253
|
+
|
|
203
254
|
const RUNTIME_ADAPTERS = {
|
|
204
255
|
claude: {
|
|
205
256
|
id: 'claude',
|
|
@@ -225,6 +276,36 @@ const RUNTIME_ADAPTERS = {
|
|
|
225
276
|
content: buildClaudeRuleFile(),
|
|
226
277
|
});
|
|
227
278
|
|
|
279
|
+
return files;
|
|
280
|
+
},
|
|
281
|
+
generateDeviceFiles(homeDir, workflows, skills) {
|
|
282
|
+
const files = [];
|
|
283
|
+
|
|
284
|
+
// Commands → ~/.claude/commands/eha/
|
|
285
|
+
for (const workflow of workflows) {
|
|
286
|
+
files.push({
|
|
287
|
+
absolutePath: path.join(homeDir, '.claude', 'commands', 'eha', `eha-${workflow.commandName}.md`),
|
|
288
|
+
content: buildClaudeCommandFile(workflow),
|
|
289
|
+
isSentinel: false,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Skills → ~/.claude/skills/eha-<name>/SKILL.md
|
|
294
|
+
for (const skill of skills) {
|
|
295
|
+
files.push({
|
|
296
|
+
absolutePath: path.join(homeDir, '.claude', 'skills', `eha-${skill.commandName}`, 'SKILL.md'),
|
|
297
|
+
content: buildClaudeSkillFile(skill),
|
|
298
|
+
isSentinel: false,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Rules → ~/.claude/rules/eha-agent-rules.md (own file, not sentinel)
|
|
303
|
+
files.push({
|
|
304
|
+
absolutePath: path.join(homeDir, '.claude', 'rules', 'eha-agent-rules.md'),
|
|
305
|
+
content: buildDeviceRulesContent('claude', workflows),
|
|
306
|
+
isSentinel: false,
|
|
307
|
+
});
|
|
308
|
+
|
|
228
309
|
return files;
|
|
229
310
|
},
|
|
230
311
|
},
|
|
@@ -247,7 +328,7 @@ const RUNTIME_ADAPTERS = {
|
|
|
247
328
|
|
|
248
329
|
for (const skill of skills) {
|
|
249
330
|
files.push({
|
|
250
|
-
relativePath: path.join('.github', '
|
|
331
|
+
relativePath: path.join('.github', 'skills', `eha-${skill.commandName}`, 'SKILL.md'),
|
|
251
332
|
content: buildCopilotSkillFile(skill),
|
|
252
333
|
});
|
|
253
334
|
}
|
|
@@ -257,18 +338,48 @@ const RUNTIME_ADAPTERS = {
|
|
|
257
338
|
content: buildCopilotRuleFile(),
|
|
258
339
|
});
|
|
259
340
|
|
|
341
|
+
return files;
|
|
342
|
+
},
|
|
343
|
+
generateDeviceFiles(homeDir, workflows, skills) {
|
|
344
|
+
const files = [];
|
|
345
|
+
|
|
346
|
+
// Prompts → ~/.copilot/prompts/eha-<name>.prompt.md
|
|
347
|
+
for (const workflow of workflows) {
|
|
348
|
+
files.push({
|
|
349
|
+
absolutePath: path.join(homeDir, '.copilot', 'prompts', `eha-${workflow.commandName}.prompt.md`),
|
|
350
|
+
content: buildCopilotPromptFile(workflow),
|
|
351
|
+
isSentinel: false,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Skills → ~/.copilot/skills/eha-<name>/SKILL.md
|
|
356
|
+
for (const skill of skills) {
|
|
357
|
+
files.push({
|
|
358
|
+
absolutePath: path.join(homeDir, '.copilot', 'skills', `eha-${skill.commandName}`, 'SKILL.md'),
|
|
359
|
+
content: buildCopilotDeviceSkillFile(skill),
|
|
360
|
+
isSentinel: false,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Instructions → ~/.copilot/instructions/eha.instructions.md (own file, NOT sentinel)
|
|
365
|
+
files.push({
|
|
366
|
+
absolutePath: path.join(homeDir, '.copilot', 'instructions', 'eha.instructions.md'),
|
|
367
|
+
content: buildCopilotDeviceInstructionsFile(workflows),
|
|
368
|
+
isSentinel: false,
|
|
369
|
+
});
|
|
370
|
+
|
|
260
371
|
return files;
|
|
261
372
|
},
|
|
262
373
|
},
|
|
263
374
|
antigravity: {
|
|
264
375
|
id: 'antigravity',
|
|
265
376
|
name: 'Antigravity',
|
|
266
|
-
description: 'Generates Antigravity-compatible skills in .agents/skills
|
|
377
|
+
description: 'Generates Antigravity-compatible workflows in .agents/workflows/, skills in .agents/skills/, and rules in .agents/rules/',
|
|
267
378
|
generateFiles(rootDir, workflows, skills) {
|
|
268
379
|
const files = [];
|
|
269
380
|
for (const workflow of workflows) {
|
|
270
381
|
files.push({
|
|
271
|
-
relativePath: path.join('.agents', '
|
|
382
|
+
relativePath: path.join('.agents', 'workflows', `eha-${workflow.commandName}`, 'SKILL.md'),
|
|
272
383
|
content: buildAntigravityCommandFile(workflow),
|
|
273
384
|
});
|
|
274
385
|
}
|
|
@@ -284,6 +395,34 @@ const RUNTIME_ADAPTERS = {
|
|
|
284
395
|
content: buildAntigravityRuleFile(),
|
|
285
396
|
});
|
|
286
397
|
|
|
398
|
+
return files;
|
|
399
|
+
},
|
|
400
|
+
generateDeviceFiles(homeDir, workflows, skills) {
|
|
401
|
+
const files = [];
|
|
402
|
+
|
|
403
|
+
// Workflows → ~/.gemini/config/global_workflows/eha-<name>.md
|
|
404
|
+
for (const workflow of workflows) {
|
|
405
|
+
files.push({
|
|
406
|
+
absolutePath: path.join(homeDir, '.gemini', 'config', 'global_workflows', `eha-${workflow.commandName}.md`),
|
|
407
|
+
content: buildAntigravityCommandFile(workflow),
|
|
408
|
+
isSentinel: false,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
for (const skill of skills) {
|
|
412
|
+
files.push({
|
|
413
|
+
absolutePath: path.join(homeDir, '.gemini', 'skills', `eha-${skill.commandName}`, 'SKILL.md'),
|
|
414
|
+
content: buildAntigravitySkillFile(skill),
|
|
415
|
+
isSentinel: false,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Rules → ~/.gemini/GEMINI.md (sentinel block)
|
|
420
|
+
files.push({
|
|
421
|
+
absolutePath: path.join(homeDir, '.gemini', 'GEMINI.md'),
|
|
422
|
+
content: buildDeviceRulesContent('antigravity', workflows),
|
|
423
|
+
isSentinel: true,
|
|
424
|
+
});
|
|
425
|
+
|
|
287
426
|
return files;
|
|
288
427
|
},
|
|
289
428
|
},
|
package/src/engine/state.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
2
|
const path = require('node:path');
|
|
3
|
+
const os = require('node:os');
|
|
3
4
|
|
|
4
5
|
const ROOT_MARKERS = ['package.json', '.git'];
|
|
5
6
|
const DEFAULT_CONFIG = {
|
|
@@ -122,17 +123,100 @@ function removeEmptyParents(startPath, stopPath) {
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
const EHA_SENTINEL_START = '<!-- EHA:START — managed by eye-hate-agent, do not edit manually -->';
|
|
127
|
+
const EHA_SENTINEL_END = '<!-- EHA:END -->';
|
|
128
|
+
|
|
129
|
+
function getDeviceManifestPath(homeDir) {
|
|
130
|
+
return path.join(homeDir || os.homedir(), '.eha', 'manifest.json');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readDeviceManifest(homeDir) {
|
|
134
|
+
const manifestPath = getDeviceManifestPath(homeDir);
|
|
135
|
+
return readJsonIfExists(manifestPath) || {
|
|
136
|
+
manifestVersion: 1,
|
|
137
|
+
agents: [],
|
|
138
|
+
files: [],
|
|
139
|
+
installedAt: null,
|
|
140
|
+
updatedAt: null,
|
|
141
|
+
packageVersion: null,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function writeDeviceManifest(manifest, homeDir) {
|
|
146
|
+
const manifestPath = getDeviceManifestPath(homeDir);
|
|
147
|
+
writeJson(manifestPath, manifest);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function deviceManifestExists(homeDir) {
|
|
151
|
+
return fs.existsSync(getDeviceManifestPath(homeDir));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function upsertSentinelBlock(filePath, content) {
|
|
155
|
+
const block = `${EHA_SENTINEL_START}\n${content}\n${EHA_SENTINEL_END}`;
|
|
156
|
+
|
|
157
|
+
if (!fs.existsSync(filePath)) {
|
|
158
|
+
ensureDir(path.dirname(filePath));
|
|
159
|
+
fs.writeFileSync(filePath, block + '\n');
|
|
160
|
+
return 'created';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const existing = fs.readFileSync(filePath, 'utf8');
|
|
164
|
+
const startIdx = existing.indexOf(EHA_SENTINEL_START);
|
|
165
|
+
const endIdx = existing.indexOf(EHA_SENTINEL_END);
|
|
166
|
+
|
|
167
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
168
|
+
const before = existing.substring(0, startIdx);
|
|
169
|
+
const after = existing.substring(endIdx + EHA_SENTINEL_END.length);
|
|
170
|
+
fs.writeFileSync(filePath, before + block + after);
|
|
171
|
+
return 'updated';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
175
|
+
fs.writeFileSync(filePath, existing + separator + block + '\n');
|
|
176
|
+
return 'appended';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function removeSentinelBlock(filePath, stopPath) {
|
|
180
|
+
if (!fs.existsSync(filePath)) return false;
|
|
181
|
+
|
|
182
|
+
const existing = fs.readFileSync(filePath, 'utf8');
|
|
183
|
+
const startIdx = existing.indexOf(EHA_SENTINEL_START);
|
|
184
|
+
const endIdx = existing.indexOf(EHA_SENTINEL_END);
|
|
185
|
+
|
|
186
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return false;
|
|
187
|
+
|
|
188
|
+
const before = existing.substring(0, startIdx);
|
|
189
|
+
const after = existing.substring(endIdx + EHA_SENTINEL_END.length);
|
|
190
|
+
const result = (before + after).replace(/\n{3,}/g, '\n\n').trim();
|
|
191
|
+
|
|
192
|
+
if (!result) {
|
|
193
|
+
fs.unlinkSync(filePath);
|
|
194
|
+
removeEmptyParents(path.dirname(filePath), stopPath || os.homedir());
|
|
195
|
+
} else {
|
|
196
|
+
fs.writeFileSync(filePath, result + '\n');
|
|
197
|
+
}
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
125
201
|
module.exports = {
|
|
202
|
+
EHA_SENTINEL_START,
|
|
203
|
+
EHA_SENTINEL_END,
|
|
204
|
+
deviceManifestExists,
|
|
126
205
|
ensureDir,
|
|
127
206
|
findRepoRoot,
|
|
128
207
|
getBundledAssetPath,
|
|
208
|
+
getDeviceManifestPath,
|
|
129
209
|
getEnginePaths,
|
|
130
210
|
getPackageRoot,
|
|
131
211
|
readConfig,
|
|
212
|
+
readDeviceManifest,
|
|
132
213
|
readJsonIfExists,
|
|
133
214
|
removeEmptyParents,
|
|
134
215
|
removeFileIfExists,
|
|
216
|
+
removeSentinelBlock,
|
|
217
|
+
upsertSentinelBlock,
|
|
135
218
|
writeConfig,
|
|
219
|
+
writeDeviceManifest,
|
|
136
220
|
writeJson,
|
|
137
221
|
writeText,
|
|
138
222
|
};
|