@exchanet/enet 1.0.8 → 1.0.9
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/.enet/pdca-t.md +110 -0
- package/package.json +1 -1
- package/src/commands/install.js +140 -38
- package/src/utils/agent-detector.js +53 -26
package/.enet/pdca-t.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Method PDCA-T — Systematic quality cycle for AI-assisted coding
|
|
3
|
+
trigger: always_on
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# METHOD PDCA-T — Active for all tasks in this project
|
|
7
|
+
|
|
8
|
+
You are operating under the PDCA-T quality methodology. Apply this 8-phase cycle to every coding task without exception.
|
|
9
|
+
|
|
10
|
+
## PHASE 1 — PLANNING
|
|
11
|
+
Before writing any code:
|
|
12
|
+
- State the exact objective in one sentence
|
|
13
|
+
- Define what IS and IS NOT in scope
|
|
14
|
+
- Ask clarifying questions if anything is ambiguous
|
|
15
|
+
- Identify external dependencies
|
|
16
|
+
- Define the acceptance criterion
|
|
17
|
+
|
|
18
|
+
Do not advance until the objective is unambiguous.
|
|
19
|
+
|
|
20
|
+
## PHASE 2 — REQUIREMENTS ANALYSIS
|
|
21
|
+
- List Functional Requirements: `FR-NN: [what the system must do]`
|
|
22
|
+
- List Non-Functional Requirements: `NFR-NN: [constraint or quality attribute with metric]`
|
|
23
|
+
- Build Risk Register: `RISK-NN: [risk] | Probability | Impact | Mitigation`
|
|
24
|
+
|
|
25
|
+
## PHASE 3 — ARCHITECTURE DESIGN
|
|
26
|
+
Before any implementation:
|
|
27
|
+
- Write ADRs: `ADR-NN: [title] | Context | Decision | Alternatives | Consequences`
|
|
28
|
+
- Define interface contracts (function signatures + full docstrings) before implementing bodies
|
|
29
|
+
- Define module structure: domain / infrastructure / interfaces
|
|
30
|
+
|
|
31
|
+
## PHASE 4 — MICRO-TASK CYCLE (≤ 50 lines per task)
|
|
32
|
+
|
|
33
|
+
**4.1** — Check available skills in `.cursor/skills/` and reusable context
|
|
34
|
+
|
|
35
|
+
**4.2** — Write tests FIRST. Required categories:
|
|
36
|
+
- Happy path · Error cases · Edge cases · Security · Performance (if applicable)
|
|
37
|
+
- Structure: Arrange / Act / Assert
|
|
38
|
+
- Naming: `test_[function]_[scenario]_[expected_outcome]`
|
|
39
|
+
|
|
40
|
+
**4.3** — Implement code. Standards:
|
|
41
|
+
- Full type hints on every parameter and return value
|
|
42
|
+
- Docstring with Args, Returns, Raises
|
|
43
|
+
- Single responsibility per function
|
|
44
|
+
- `Decimal` not `float` for monetary values
|
|
45
|
+
- Specific exception types — never bare `except:`
|
|
46
|
+
- Zero hardcoded configuration
|
|
47
|
+
- Structured logging with context fields
|
|
48
|
+
|
|
49
|
+
**4.4** — Self-review before running tests:
|
|
50
|
+
```
|
|
51
|
+
□ Type hints complete? □ All inputs validated?
|
|
52
|
+
□ Docstring written? □ No hardcoded secrets?
|
|
53
|
+
□ Single responsibility? □ Semantic names?
|
|
54
|
+
□ No code duplication? □ Errors logged with context?
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**4.5** — Execute tests and show REAL complete output:
|
|
58
|
+
```bash
|
|
59
|
+
pytest tests/ -v --cov=src --cov-report=term-missing --tb=short
|
|
60
|
+
```
|
|
61
|
+
Never summarize. Never say "tests pass". Show the exact output.
|
|
62
|
+
|
|
63
|
+
**4.6** — Validate:
|
|
64
|
+
- All tests pass (100%)? If not → fix code, explain, re-run
|
|
65
|
+
- Coverage ≥ 99%? If not → identify uncovered lines → add tests → re-run
|
|
66
|
+
- Repeat until both conditions are met
|
|
67
|
+
|
|
68
|
+
## PHASE 5 — INTEGRAL VALIDATION
|
|
69
|
+
After all micro-tasks:
|
|
70
|
+
- **Security:** No OWASP Top 10 issues · inputs validated · outputs sanitized · no hardcoded secrets · minimum privilege
|
|
71
|
+
- **Tests:** 100% passed · 0 failed · coverage ≥ 99% · all categories present
|
|
72
|
+
- **Code quality:** Type hints 100% · cyclomatic complexity < 10 · no duplication · SRP · docstrings 100%
|
|
73
|
+
- **Performance:** No N+1 · indexes on filter fields · pagination in collections · timeouts configured
|
|
74
|
+
- **Architecture:** No circular imports · layers respected · low coupling · inward dependencies only
|
|
75
|
+
|
|
76
|
+
## PHASE 6 — TECHNICAL DEBT MANAGEMENT
|
|
77
|
+
Register every known issue before delivery:
|
|
78
|
+
```
|
|
79
|
+
DEBT-XXX: [Short title]
|
|
80
|
+
Type: Technical | Test | Documentation | Architecture | Security | Performance
|
|
81
|
+
Description: [What and why]
|
|
82
|
+
Impact: High | Medium | Low — [justification]
|
|
83
|
+
Effort: Xh
|
|
84
|
+
Priority: High | Medium | Low
|
|
85
|
+
Plan: [Specific action and target version]
|
|
86
|
+
```
|
|
87
|
+
Do not write TODO/FIXME in code — register as DEBT-XXX instead.
|
|
88
|
+
|
|
89
|
+
## PHASE 7 — REFINEMENT TO ≥ 99%
|
|
90
|
+
If any metric is below target:
|
|
91
|
+
`Identify → Classify → Plan → Execute → Verify → Confirm ≥ 99%`
|
|
92
|
+
Never advance to Phase 8 without confirming ≥ 99% on all 5 validation dimensions.
|
|
93
|
+
|
|
94
|
+
## PHASE 8 — DELIVERY REPORT
|
|
95
|
+
Always close every task with:
|
|
96
|
+
1. Implementation summary (2-3 sentences)
|
|
97
|
+
2. Test table: total / passed / failed / coverage / time
|
|
98
|
+
3. Full unedited pytest output
|
|
99
|
+
4. Key technical decisions with justifications
|
|
100
|
+
5. Technical debt registered (DEBT-XXX list)
|
|
101
|
+
6. CI/CD checklist (all items confirmed)
|
|
102
|
+
7. Suggested next steps
|
|
103
|
+
|
|
104
|
+
## ABSOLUTE RULES — NEVER VIOLATE
|
|
105
|
+
1. Tests BEFORE implementation — always, no exceptions
|
|
106
|
+
2. Show REAL test output — never summarize or omit
|
|
107
|
+
3. No hardcoded secrets — environment variables from commit 1
|
|
108
|
+
4. Coverage ≥ 99% before any delivery
|
|
109
|
+
5. ADRs for non-trivial decisions
|
|
110
|
+
6. All known issues as DEBT-XXX with priority and plan
|
package/package.json
CHANGED
package/src/commands/install.js
CHANGED
|
@@ -2,26 +2,30 @@ import chalk from 'chalk'
|
|
|
2
2
|
import ora from 'ora'
|
|
3
3
|
import fs from 'fs-extra'
|
|
4
4
|
import path from 'path'
|
|
5
|
+
import readline from 'readline'
|
|
5
6
|
import { detectSystemAgents, getInstallPath, AGENTS } from '../utils/agent-detector.js'
|
|
6
7
|
import { getMethod, fetchFromGitHub } from '../utils/registry.js'
|
|
7
8
|
|
|
8
9
|
export async function installCommand(methodId, options) {
|
|
9
|
-
// Load method from registry
|
|
10
|
+
// 1. Load method from registry
|
|
10
11
|
const spinner = ora('Fetching registry...').start()
|
|
11
12
|
const method = await getMethod(methodId).catch(() => null)
|
|
12
13
|
spinner.stop()
|
|
13
14
|
|
|
14
15
|
if (!method) {
|
|
15
|
-
console.log(chalk.red(
|
|
16
|
+
console.log(chalk.red(`\n ✗ Unknown method: "${methodId}"`))
|
|
16
17
|
console.log(chalk.dim(` Run ${chalk.white('enet list')} to see available methods.\n`))
|
|
17
18
|
process.exit(1)
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
console.log(chalk.bold(`\n ◆ ${method.name}`))
|
|
22
|
+
console.log(chalk.dim(` ${method.description}\n`))
|
|
23
|
+
|
|
24
|
+
// 2. Determine target agents
|
|
21
25
|
let targetAgents = []
|
|
22
26
|
|
|
23
27
|
if (options.agent) {
|
|
24
|
-
// --agent flag: install for
|
|
28
|
+
// --agent flag: skip detection, install for this specific agent only
|
|
25
29
|
if (!AGENTS[options.agent]) {
|
|
26
30
|
console.log(chalk.red(` ✗ Unknown agent: "${options.agent}"`))
|
|
27
31
|
console.log(chalk.dim(` Valid: ${Object.keys(AGENTS).filter(k => k !== 'generic').join(', ')}\n`))
|
|
@@ -36,57 +40,147 @@ export async function installCommand(methodId, options) {
|
|
|
36
40
|
if (detected.length === 0) {
|
|
37
41
|
console.log(chalk.yellow(' ⚠ No AI agents detected on this system.'))
|
|
38
42
|
console.log(chalk.dim(` Use ${chalk.white('--agent <name>')} to force an agent.`))
|
|
39
|
-
console.log(chalk.dim(` Valid:
|
|
43
|
+
console.log(chalk.dim(` Valid: ${Object.keys(AGENTS).filter(k => k !== 'generic').join(', ')}\n`))
|
|
40
44
|
process.exit(1)
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
// 3. Show checkbox selection — always, even with 1 agent detected
|
|
48
|
+
// This is the core UX fix: user always chooses, never surprised
|
|
49
|
+
if (options.all) {
|
|
50
|
+
// --all flag skips the prompt
|
|
45
51
|
targetAgents = detected
|
|
46
52
|
} else {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
detected.forEach((a, i) => console.log(chalk.dim(` [${i + 1}] ${a.name}`)))
|
|
50
|
-
console.log(chalk.dim(` [${detected.length + 1}] All of the above\n`))
|
|
51
|
-
|
|
52
|
-
const { createInterface } = await import('readline')
|
|
53
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
54
|
-
const answer = await new Promise(resolve => {
|
|
55
|
-
rl.question(chalk.white(' Install for which agent(s)? '), resolve)
|
|
56
|
-
})
|
|
57
|
-
rl.close()
|
|
53
|
+
targetAgents = await checkboxSelect(detected, method)
|
|
54
|
+
}
|
|
58
55
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
} else if (choice >= 1 && choice <= detected.length) {
|
|
63
|
-
targetAgents = [detected[choice - 1]]
|
|
64
|
-
} else {
|
|
65
|
-
console.log(chalk.red('\n Invalid choice. Cancelled.\n'))
|
|
66
|
-
process.exit(1)
|
|
67
|
-
}
|
|
56
|
+
if (targetAgents.length === 0) {
|
|
57
|
+
console.log(chalk.dim('\n Nothing selected. Cancelled.\n'))
|
|
58
|
+
process.exit(0)
|
|
68
59
|
}
|
|
69
60
|
}
|
|
70
61
|
|
|
71
62
|
console.log()
|
|
72
63
|
|
|
73
|
-
// Install for each
|
|
64
|
+
// 4. Install for each selected agent
|
|
74
65
|
let schemaInstalled = false
|
|
75
66
|
for (const agent of targetAgents) {
|
|
76
67
|
await installForAgent(method, agent, options, schemaInstalled)
|
|
77
|
-
schemaInstalled = true
|
|
68
|
+
schemaInstalled = true
|
|
78
69
|
}
|
|
79
70
|
|
|
80
71
|
printHints(methodId)
|
|
81
72
|
}
|
|
82
73
|
|
|
74
|
+
// ─────────────────────────────────────────────────────────────────
|
|
75
|
+
// Checkbox selection UI
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
async function checkboxSelect(detected, method) {
|
|
79
|
+
// Build list: detected agents that have an adapter in this method come first,
|
|
80
|
+
// then show unavailable ones as disabled so user knows what exists
|
|
81
|
+
const available = detected.filter(a => method.adapters[a.key] || method.adapters['generic'])
|
|
82
|
+
const unavailable = detected.filter(a => !method.adapters[a.key] && !method.adapters['generic'])
|
|
83
|
+
|
|
84
|
+
if (available.length === 0) {
|
|
85
|
+
console.log(chalk.yellow(' ⚠ No adapters available for your detected agents.'))
|
|
86
|
+
console.log(chalk.dim(` Available adapters in this method: ${Object.keys(method.adapters).join(', ')}\n`))
|
|
87
|
+
process.exit(1)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Initial state: all available agents pre-checked
|
|
91
|
+
const items = available.map(a => ({
|
|
92
|
+
agent: a,
|
|
93
|
+
checked: true,
|
|
94
|
+
usesGeneric: !method.adapters[a.key]
|
|
95
|
+
}))
|
|
96
|
+
|
|
97
|
+
console.log(chalk.white(' Agents detected on your system:\n'))
|
|
98
|
+
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
let cursor = 0
|
|
101
|
+
|
|
102
|
+
const render = () => {
|
|
103
|
+
// Move cursor up to redraw (after first render)
|
|
104
|
+
const lines = items.length + 6
|
|
105
|
+
if (render.drawn) process.stdout.write(`\x1B[${lines}A`)
|
|
106
|
+
render.drawn = true
|
|
107
|
+
|
|
108
|
+
items.forEach((item, i) => {
|
|
109
|
+
const isCursor = i === cursor
|
|
110
|
+
const box = item.checked ? chalk.green('[✓]') : chalk.dim('[ ]')
|
|
111
|
+
const arrow = isCursor ? chalk.cyan(' ❯ ') : ' '
|
|
112
|
+
const name = isCursor ? chalk.white(item.agent.name) : chalk.dim(item.agent.name)
|
|
113
|
+
const tag = item.usesGeneric ? chalk.dim(' (generic adapter)') : ''
|
|
114
|
+
process.stdout.write(`${arrow}${box} ${name}${tag}\n`)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
if (unavailable.length > 0) {
|
|
118
|
+
process.stdout.write(chalk.dim(`\n Not available for: ${unavailable.map(a => a.name).join(', ')}\n`))
|
|
119
|
+
} else {
|
|
120
|
+
process.stdout.write('\n')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
process.stdout.write(chalk.dim(' ↑↓ navigate · Space toggle · A select all · Enter confirm\n\n'))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
render()
|
|
127
|
+
|
|
128
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
129
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true)
|
|
130
|
+
process.stdin.resume()
|
|
131
|
+
|
|
132
|
+
process.stdin.on('data', (key) => {
|
|
133
|
+
const k = key.toString()
|
|
134
|
+
|
|
135
|
+
if (k === '\u001b[A') { // arrow up
|
|
136
|
+
cursor = (cursor - 1 + items.length) % items.length
|
|
137
|
+
render()
|
|
138
|
+
} else if (k === '\u001b[B') { // arrow down
|
|
139
|
+
cursor = (cursor + 1) % items.length
|
|
140
|
+
render()
|
|
141
|
+
} else if (k === ' ') { // space: toggle current
|
|
142
|
+
items[cursor].checked = !items[cursor].checked
|
|
143
|
+
render()
|
|
144
|
+
} else if (k === 'a' || k === 'A') { // A: toggle all
|
|
145
|
+
const allChecked = items.every(i => i.checked)
|
|
146
|
+
items.forEach(i => { i.checked = !allChecked })
|
|
147
|
+
render()
|
|
148
|
+
} else if (k === '\r' || k === '\n') { // Enter: confirm
|
|
149
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false)
|
|
150
|
+
process.stdin.pause()
|
|
151
|
+
rl.close()
|
|
152
|
+
const selected = items.filter(i => i.checked).map(i => i.agent)
|
|
153
|
+
console.log(chalk.dim(`\n Installing for: ${selected.map(a => a.name).join(', ')}\n`))
|
|
154
|
+
resolve(selected)
|
|
155
|
+
} else if (k === '\u0003') { // Ctrl+C
|
|
156
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false)
|
|
157
|
+
process.stdin.pause()
|
|
158
|
+
rl.close()
|
|
159
|
+
console.log('\n')
|
|
160
|
+
process.exit(0)
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─────────────────────────────────────────────────────────────────
|
|
167
|
+
// Install for one agent
|
|
168
|
+
// ─────────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
83
170
|
async function installForAgent(method, agent, options, skipExtras = false) {
|
|
84
171
|
const isGlobal = options.global || false
|
|
172
|
+
|
|
173
|
+
// Use the agent-specific adapter if available, fall back to generic
|
|
85
174
|
const adapterKey = method.adapters[agent.key] ? agent.key : 'generic'
|
|
86
175
|
const adapterPath = method.adapters[adapterKey]
|
|
87
176
|
|
|
177
|
+
if (!adapterPath) {
|
|
178
|
+
console.log(chalk.yellow(` ⚠ ${agent.name} — no adapter found, skipping`))
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
88
182
|
if (isGlobal && !agent.globalInstallDir) {
|
|
89
|
-
console.log(chalk.yellow(` ⚠ ${agent.name}
|
|
183
|
+
console.log(chalk.yellow(` ⚠ ${agent.name} — global install not supported, skipping`))
|
|
90
184
|
return
|
|
91
185
|
}
|
|
92
186
|
|
|
@@ -98,7 +192,7 @@ async function installForAgent(method, agent, options, skipExtras = false) {
|
|
|
98
192
|
|
|
99
193
|
await fs.ensureDir(path.dirname(installPath))
|
|
100
194
|
|
|
101
|
-
// Windsurf
|
|
195
|
+
// Windsurf: append to .windsurfrules instead of overwriting
|
|
102
196
|
if (agent.key === 'windsurf' && await fs.pathExists(installPath)) {
|
|
103
197
|
const existing = await fs.readFile(installPath, 'utf8')
|
|
104
198
|
if (existing.includes(method.name)) {
|
|
@@ -110,7 +204,7 @@ async function installForAgent(method, agent, options, skipExtras = false) {
|
|
|
110
204
|
await fs.writeFile(installPath, content)
|
|
111
205
|
}
|
|
112
206
|
|
|
113
|
-
spinner.succeed(chalk.green(`${agent.name}
|
|
207
|
+
spinner.succeed(chalk.green(`${agent.name}`))
|
|
114
208
|
console.log(chalk.dim(` → ${installPath}`))
|
|
115
209
|
console.log(chalk.dim(` ${agent.configNote}\n`))
|
|
116
210
|
|
|
@@ -119,7 +213,7 @@ async function installForAgent(method, agent, options, skipExtras = false) {
|
|
|
119
213
|
return
|
|
120
214
|
}
|
|
121
215
|
|
|
122
|
-
// Download extras (schema, etc.) once
|
|
216
|
+
// Download extras (schema, etc.) — only once across all agents
|
|
123
217
|
if (!skipExtras && method.extras) {
|
|
124
218
|
for (const [key, filePath] of Object.entries(method.extras)) {
|
|
125
219
|
const extraSpinner = ora(`Fetching ${key}...`).start()
|
|
@@ -136,18 +230,26 @@ async function installForAgent(method, agent, options, skipExtras = false) {
|
|
|
136
230
|
}
|
|
137
231
|
}
|
|
138
232
|
|
|
233
|
+
// ─────────────────────────────────────────────────────────────────
|
|
234
|
+
// Post-install hints
|
|
235
|
+
// ─────────────────────────────────────────────────────────────────
|
|
236
|
+
|
|
139
237
|
function printHints(methodId) {
|
|
140
238
|
if (methodId === 'modular-design') {
|
|
141
|
-
console.log(chalk.dim(' Next:'))
|
|
142
|
-
console.log(chalk.dim(
|
|
143
|
-
console.log(chalk.dim(
|
|
144
|
-
console.log(chalk.dim(
|
|
239
|
+
console.log(chalk.dim(' Next steps:'))
|
|
240
|
+
console.log(chalk.dim(' 1. Give your agent a spectech (stack + modules needed)'))
|
|
241
|
+
console.log(chalk.dim(' 2. Agent declares architecture — confirm it'))
|
|
242
|
+
console.log(chalk.dim(' 3. Agent builds Core → modules → Admin Panel'))
|
|
145
243
|
console.log()
|
|
146
244
|
console.log(chalk.dim(` ${chalk.white('enet new module <name>')} scaffold your first module`))
|
|
147
245
|
console.log(chalk.dim(` ${chalk.white('enet validate')} check manifests at any time\n`))
|
|
148
246
|
}
|
|
149
247
|
if (methodId === 'pdca-t') {
|
|
150
|
-
console.log(chalk.dim(
|
|
248
|
+
console.log(chalk.dim(' Next steps:'))
|
|
249
|
+
console.log(chalk.dim(' 1. Start any coding task — the method activates automatically'))
|
|
250
|
+
console.log(chalk.dim(' 2. Your agent will follow the 8-phase quality cycle'))
|
|
251
|
+
console.log(chalk.dim(' 3. Every delivery includes a full test report'))
|
|
252
|
+
console.log()
|
|
151
253
|
console.log(chalk.dim(` Works best alongside ${chalk.white('enet install modular-design')}.\n`))
|
|
152
254
|
}
|
|
153
255
|
}
|
|
@@ -8,7 +8,10 @@ export const AGENTS = {
|
|
|
8
8
|
cursor: {
|
|
9
9
|
name: 'Cursor',
|
|
10
10
|
systemSignals: [
|
|
11
|
-
path.join(HOME, '.cursor')
|
|
11
|
+
path.join(HOME, '.cursor'),
|
|
12
|
+
path.join(HOME, 'Library', 'Application Support', 'Cursor'),
|
|
13
|
+
path.join(HOME, 'AppData', 'Roaming', 'Cursor'),
|
|
14
|
+
path.join(HOME, '.config', 'Cursor'),
|
|
12
15
|
],
|
|
13
16
|
projectSignals: ['.cursor/rules', '.cursor'],
|
|
14
17
|
projectInstallDir: '.cursor/rules',
|
|
@@ -16,82 +19,105 @@ export const AGENTS = {
|
|
|
16
19
|
filename: 'enet-{id}.md',
|
|
17
20
|
configNote: 'Rule auto-applies to all files (alwaysApply: true)'
|
|
18
21
|
},
|
|
22
|
+
|
|
19
23
|
windsurf: {
|
|
20
24
|
name: 'Windsurf',
|
|
21
25
|
systemSignals: [
|
|
22
|
-
path.join(HOME, '.codeium', 'windsurf')
|
|
26
|
+
path.join(HOME, '.codeium', 'windsurf'),
|
|
27
|
+
path.join(HOME, 'Library', 'Application Support', 'Windsurf'),
|
|
28
|
+
path.join(HOME, 'AppData', 'Roaming', 'Windsurf'),
|
|
23
29
|
],
|
|
24
30
|
projectSignals: ['.windsurfrules', '.windsurf'],
|
|
25
31
|
projectInstallDir: '.',
|
|
26
32
|
globalInstallDir: path.join(HOME, '.codeium', 'windsurf', 'memories'),
|
|
27
33
|
globalFilename: 'global_rules.md',
|
|
28
34
|
filename: '.windsurfrules',
|
|
29
|
-
configNote: 'Appended to
|
|
35
|
+
configNote: 'Appended to .windsurfrules in project root'
|
|
30
36
|
},
|
|
37
|
+
|
|
31
38
|
antigravity: {
|
|
32
39
|
name: 'Antigravity (Google)',
|
|
33
40
|
systemSignals: [
|
|
34
|
-
path.join(HOME, '.gemini', 'antigravity')
|
|
41
|
+
path.join(HOME, '.gemini', 'antigravity'),
|
|
42
|
+
path.join(HOME, 'Library', 'Application Support', 'Google', 'Antigravity'),
|
|
43
|
+
path.join(HOME, 'AppData', 'Roaming', 'Google', 'Antigravity'),
|
|
35
44
|
],
|
|
36
45
|
projectSignals: ['.agent/rules', '.agent'],
|
|
37
46
|
projectInstallDir: '.agent/rules',
|
|
38
|
-
globalInstallDir: path.join(HOME, '.gemini', 'antigravity', 'skills'
|
|
47
|
+
globalInstallDir: path.join(HOME, '.gemini', 'antigravity', 'skills'),
|
|
39
48
|
globalFilename: 'SKILL.md',
|
|
40
49
|
filename: 'enet-{id}.md',
|
|
41
|
-
configNote: '
|
|
50
|
+
configNote: 'Rule placed in .agent/rules/ — activates automatically in Antigravity'
|
|
42
51
|
},
|
|
52
|
+
|
|
43
53
|
claudecode: {
|
|
44
54
|
name: 'Claude Code',
|
|
45
55
|
systemSignals: [
|
|
46
|
-
path.join(HOME, '.claude')
|
|
56
|
+
path.join(HOME, '.claude'),
|
|
47
57
|
],
|
|
48
58
|
projectSignals: ['CLAUDE.md', '.claude'],
|
|
49
59
|
projectInstallDir: '.',
|
|
50
60
|
globalInstallDir: path.join(HOME, '.claude'),
|
|
51
61
|
globalFilename: 'CLAUDE.md',
|
|
52
62
|
filename: 'CLAUDE.md',
|
|
53
|
-
configNote: 'Written to
|
|
63
|
+
configNote: 'Written to CLAUDE.md — Claude Code reads this automatically'
|
|
54
64
|
},
|
|
65
|
+
|
|
55
66
|
copilot: {
|
|
56
67
|
name: 'GitHub Copilot',
|
|
57
|
-
systemSignals: [
|
|
68
|
+
systemSignals: [
|
|
69
|
+
path.join(HOME, '.vscode', 'extensions'),
|
|
70
|
+
path.join(HOME, 'Library', 'Application Support', 'Code', 'User', 'extensions'),
|
|
71
|
+
path.join(HOME, 'AppData', 'Roaming', 'Code', 'User', 'extensions'),
|
|
72
|
+
path.join(HOME, '.vscode-server', 'extensions'),
|
|
73
|
+
],
|
|
74
|
+
systemSignalFilter: (signalPath) => {
|
|
75
|
+
try {
|
|
76
|
+
const entries = fs.readdirSync(signalPath)
|
|
77
|
+
return entries.some(e => e.toLowerCase().startsWith('github.copilot'))
|
|
78
|
+
} catch {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
},
|
|
58
82
|
projectSignals: ['.github/copilot-instructions.md'],
|
|
59
83
|
projectInstallDir: '.github',
|
|
60
84
|
globalInstallDir: null,
|
|
61
85
|
filename: 'copilot-instructions.md',
|
|
62
86
|
configNote: 'Written to .github/copilot-instructions.md'
|
|
63
87
|
},
|
|
88
|
+
|
|
64
89
|
generic: {
|
|
65
|
-
name: 'Generic
|
|
90
|
+
name: 'Generic / Other agent',
|
|
66
91
|
systemSignals: [],
|
|
67
92
|
projectSignals: [],
|
|
68
93
|
projectInstallDir: '.enet',
|
|
69
94
|
globalInstallDir: null,
|
|
70
95
|
filename: '{id}.md',
|
|
71
|
-
configNote:
|
|
96
|
+
configNote: "Saved to .enet/ — paste contents into your agent's context"
|
|
72
97
|
}
|
|
73
98
|
}
|
|
74
99
|
|
|
75
|
-
/**
|
|
76
|
-
* Detects ALL agents installed on the system by checking known global paths.
|
|
77
|
-
*/
|
|
78
100
|
export async function detectSystemAgents() {
|
|
79
101
|
const found = []
|
|
80
102
|
for (const [key, agent] of Object.entries(AGENTS)) {
|
|
81
103
|
if (key === 'generic') continue
|
|
82
104
|
for (const signal of agent.systemSignals) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
105
|
+
const exists = await fs.pathExists(signal)
|
|
106
|
+
if (!exists) continue
|
|
107
|
+
if (agent.systemSignalFilter) {
|
|
108
|
+
if (agent.systemSignalFilter(signal)) {
|
|
109
|
+
found.push({ key, ...agent })
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
continue
|
|
86
113
|
}
|
|
114
|
+
found.push({ key, ...agent })
|
|
115
|
+
break
|
|
87
116
|
}
|
|
88
117
|
}
|
|
89
118
|
return found
|
|
90
119
|
}
|
|
91
120
|
|
|
92
|
-
/**
|
|
93
|
-
* Detects ALL agents present in the current project.
|
|
94
|
-
*/
|
|
95
121
|
export async function detectProjectAgents(cwd = process.cwd()) {
|
|
96
122
|
const found = []
|
|
97
123
|
for (const [key, agent] of Object.entries(AGENTS)) {
|
|
@@ -106,22 +132,23 @@ export async function detectProjectAgents(cwd = process.cwd()) {
|
|
|
106
132
|
return found
|
|
107
133
|
}
|
|
108
134
|
|
|
109
|
-
/**
|
|
110
|
-
* Returns the first detected agent (legacy, used by status/doctor).
|
|
111
|
-
*/
|
|
112
135
|
export async function detectAgent(cwd = process.cwd()) {
|
|
113
136
|
const agents = await detectProjectAgents(cwd)
|
|
114
137
|
return agents[0] ?? { key: 'generic', ...AGENTS.generic }
|
|
115
138
|
}
|
|
116
139
|
|
|
117
|
-
/**
|
|
118
|
-
* Returns the install path for a method adapter.
|
|
119
|
-
*/
|
|
120
140
|
export function getInstallPath(agent, methodId, { global = false, cwd = process.cwd() } = {}) {
|
|
121
141
|
if (global) {
|
|
142
|
+
if (!agent.globalInstallDir) return null
|
|
143
|
+
|
|
144
|
+
if (agent.key === 'antigravity') {
|
|
145
|
+
return path.join(agent.globalInstallDir, `method-${methodId}`, 'SKILL.md')
|
|
146
|
+
}
|
|
147
|
+
|
|
122
148
|
const filename = agent.globalFilename ?? agent.filename.replace('{id}', methodId)
|
|
123
149
|
return path.join(agent.globalInstallDir, filename)
|
|
124
150
|
}
|
|
151
|
+
|
|
125
152
|
const filename = agent.filename.replace('{id}', methodId)
|
|
126
153
|
return path.join(cwd, agent.projectInstallDir, filename)
|
|
127
154
|
}
|