@exchanet/enet 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/.enet/pdca-t.md +110 -0
- package/package.json +1 -1
- package/src/commands/install.js +145 -38
- package/src/commands/update.js +196 -20
- package/src/utils/agent-detector.js +53 -26
- package/src/utils/registry.js +28 -0
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,35 @@ 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
|
+
const record = await readInstallRecord(methodId)
|
|
24
|
+
const alreadyInstalled = new Set(record?.agents ?? [])
|
|
25
|
+
if (alreadyInstalled.size > 0) {
|
|
26
|
+
console.log(chalk.dim(` Already installed for: ${[...alreadyInstalled].join(', ')}\n`))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 2. Determine target agents
|
|
21
30
|
let targetAgents = []
|
|
22
31
|
|
|
23
32
|
if (options.agent) {
|
|
24
|
-
// --agent flag: install for
|
|
33
|
+
// --agent flag: skip detection, install for this specific agent only
|
|
25
34
|
if (!AGENTS[options.agent]) {
|
|
26
35
|
console.log(chalk.red(` ✗ Unknown agent: "${options.agent}"`))
|
|
27
36
|
console.log(chalk.dim(` Valid: ${Object.keys(AGENTS).filter(k => k !== 'generic').join(', ')}\n`))
|
|
@@ -36,57 +45,147 @@ export async function installCommand(methodId, options) {
|
|
|
36
45
|
if (detected.length === 0) {
|
|
37
46
|
console.log(chalk.yellow(' ⚠ No AI agents detected on this system.'))
|
|
38
47
|
console.log(chalk.dim(` Use ${chalk.white('--agent <name>')} to force an agent.`))
|
|
39
|
-
console.log(chalk.dim(` Valid:
|
|
48
|
+
console.log(chalk.dim(` Valid: ${Object.keys(AGENTS).filter(k => k !== 'generic').join(', ')}\n`))
|
|
40
49
|
process.exit(1)
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
// 3. Show checkbox selection — always, even with 1 agent detected
|
|
53
|
+
// This is the core UX fix: user always chooses, never surprised
|
|
54
|
+
if (options.all) {
|
|
55
|
+
// --all flag skips the prompt
|
|
45
56
|
targetAgents = detected
|
|
46
57
|
} 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()
|
|
58
|
+
targetAgents = await checkboxSelect(detected, method)
|
|
59
|
+
}
|
|
58
60
|
|
|
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
|
-
}
|
|
61
|
+
if (targetAgents.length === 0) {
|
|
62
|
+
console.log(chalk.dim('\n Nothing selected. Cancelled.\n'))
|
|
63
|
+
process.exit(0)
|
|
68
64
|
}
|
|
69
65
|
}
|
|
70
66
|
|
|
71
67
|
console.log()
|
|
72
68
|
|
|
73
|
-
// Install for each
|
|
69
|
+
// 4. Install for each selected agent
|
|
74
70
|
let schemaInstalled = false
|
|
75
71
|
for (const agent of targetAgents) {
|
|
76
72
|
await installForAgent(method, agent, options, schemaInstalled)
|
|
77
|
-
schemaInstalled = true
|
|
73
|
+
schemaInstalled = true
|
|
78
74
|
}
|
|
79
75
|
|
|
80
76
|
printHints(methodId)
|
|
81
77
|
}
|
|
82
78
|
|
|
79
|
+
// ─────────────────────────────────────────────────────────────────
|
|
80
|
+
// Checkbox selection UI
|
|
81
|
+
// ─────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
async function checkboxSelect(detected, method) {
|
|
84
|
+
// Build list: detected agents that have an adapter in this method come first,
|
|
85
|
+
// then show unavailable ones as disabled so user knows what exists
|
|
86
|
+
const available = detected.filter(a => method.adapters[a.key] || method.adapters['generic'])
|
|
87
|
+
const unavailable = detected.filter(a => !method.adapters[a.key] && !method.adapters['generic'])
|
|
88
|
+
|
|
89
|
+
if (available.length === 0) {
|
|
90
|
+
console.log(chalk.yellow(' ⚠ No adapters available for your detected agents.'))
|
|
91
|
+
console.log(chalk.dim(` Available adapters in this method: ${Object.keys(method.adapters).join(', ')}\n`))
|
|
92
|
+
process.exit(1)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Initial state: all available agents pre-checked
|
|
96
|
+
const items = available.map(a => ({
|
|
97
|
+
agent: a,
|
|
98
|
+
checked: true,
|
|
99
|
+
usesGeneric: !method.adapters[a.key]
|
|
100
|
+
}))
|
|
101
|
+
|
|
102
|
+
console.log(chalk.white(' Agents detected on your system:\n'))
|
|
103
|
+
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
let cursor = 0
|
|
106
|
+
|
|
107
|
+
const render = () => {
|
|
108
|
+
// Move cursor up to redraw (after first render)
|
|
109
|
+
const lines = items.length + 6
|
|
110
|
+
if (render.drawn) process.stdout.write(`\x1B[${lines}A`)
|
|
111
|
+
render.drawn = true
|
|
112
|
+
|
|
113
|
+
items.forEach((item, i) => {
|
|
114
|
+
const isCursor = i === cursor
|
|
115
|
+
const box = item.checked ? chalk.green('[✓]') : chalk.dim('[ ]')
|
|
116
|
+
const arrow = isCursor ? chalk.cyan(' ❯ ') : ' '
|
|
117
|
+
const name = isCursor ? chalk.white(item.agent.name) : chalk.dim(item.agent.name)
|
|
118
|
+
const tag = item.usesGeneric ? chalk.dim(' (generic adapter)') : ''
|
|
119
|
+
process.stdout.write(`${arrow}${box} ${name}${tag}\n`)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
if (unavailable.length > 0) {
|
|
123
|
+
process.stdout.write(chalk.dim(`\n Not available for: ${unavailable.map(a => a.name).join(', ')}\n`))
|
|
124
|
+
} else {
|
|
125
|
+
process.stdout.write('\n')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
process.stdout.write(chalk.dim(' ↑↓ navigate · Space toggle · A select all · Enter confirm\n\n'))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
render()
|
|
132
|
+
|
|
133
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
134
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true)
|
|
135
|
+
process.stdin.resume()
|
|
136
|
+
|
|
137
|
+
process.stdin.on('data', (key) => {
|
|
138
|
+
const k = key.toString()
|
|
139
|
+
|
|
140
|
+
if (k === '\u001b[A') { // arrow up
|
|
141
|
+
cursor = (cursor - 1 + items.length) % items.length
|
|
142
|
+
render()
|
|
143
|
+
} else if (k === '\u001b[B') { // arrow down
|
|
144
|
+
cursor = (cursor + 1) % items.length
|
|
145
|
+
render()
|
|
146
|
+
} else if (k === ' ') { // space: toggle current
|
|
147
|
+
items[cursor].checked = !items[cursor].checked
|
|
148
|
+
render()
|
|
149
|
+
} else if (k === 'a' || k === 'A') { // A: toggle all
|
|
150
|
+
const allChecked = items.every(i => i.checked)
|
|
151
|
+
items.forEach(i => { i.checked = !allChecked })
|
|
152
|
+
render()
|
|
153
|
+
} else if (k === '\r' || k === '\n') { // Enter: confirm
|
|
154
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false)
|
|
155
|
+
process.stdin.pause()
|
|
156
|
+
rl.close()
|
|
157
|
+
const selected = items.filter(i => i.checked).map(i => i.agent)
|
|
158
|
+
console.log(chalk.dim(`\n Installing for: ${selected.map(a => a.name).join(', ')}\n`))
|
|
159
|
+
resolve(selected)
|
|
160
|
+
} else if (k === '\u0003') { // Ctrl+C
|
|
161
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false)
|
|
162
|
+
process.stdin.pause()
|
|
163
|
+
rl.close()
|
|
164
|
+
console.log('\n')
|
|
165
|
+
process.exit(0)
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─────────────────────────────────────────────────────────────────
|
|
172
|
+
// Install for one agent
|
|
173
|
+
// ─────────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
83
175
|
async function installForAgent(method, agent, options, skipExtras = false) {
|
|
84
176
|
const isGlobal = options.global || false
|
|
177
|
+
|
|
178
|
+
// Use the agent-specific adapter if available, fall back to generic
|
|
85
179
|
const adapterKey = method.adapters[agent.key] ? agent.key : 'generic'
|
|
86
180
|
const adapterPath = method.adapters[adapterKey]
|
|
87
181
|
|
|
182
|
+
if (!adapterPath) {
|
|
183
|
+
console.log(chalk.yellow(` ⚠ ${agent.name} — no adapter found, skipping`))
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
88
187
|
if (isGlobal && !agent.globalInstallDir) {
|
|
89
|
-
console.log(chalk.yellow(` ⚠ ${agent.name}
|
|
188
|
+
console.log(chalk.yellow(` ⚠ ${agent.name} — global install not supported, skipping`))
|
|
90
189
|
return
|
|
91
190
|
}
|
|
92
191
|
|
|
@@ -98,7 +197,7 @@ async function installForAgent(method, agent, options, skipExtras = false) {
|
|
|
98
197
|
|
|
99
198
|
await fs.ensureDir(path.dirname(installPath))
|
|
100
199
|
|
|
101
|
-
// Windsurf
|
|
200
|
+
// Windsurf: append to .windsurfrules instead of overwriting
|
|
102
201
|
if (agent.key === 'windsurf' && await fs.pathExists(installPath)) {
|
|
103
202
|
const existing = await fs.readFile(installPath, 'utf8')
|
|
104
203
|
if (existing.includes(method.name)) {
|
|
@@ -110,7 +209,7 @@ async function installForAgent(method, agent, options, skipExtras = false) {
|
|
|
110
209
|
await fs.writeFile(installPath, content)
|
|
111
210
|
}
|
|
112
211
|
|
|
113
|
-
spinner.succeed(chalk.green(`${agent.name}
|
|
212
|
+
spinner.succeed(chalk.green(`${agent.name}`))
|
|
114
213
|
console.log(chalk.dim(` → ${installPath}`))
|
|
115
214
|
console.log(chalk.dim(` ${agent.configNote}\n`))
|
|
116
215
|
|
|
@@ -119,7 +218,7 @@ async function installForAgent(method, agent, options, skipExtras = false) {
|
|
|
119
218
|
return
|
|
120
219
|
}
|
|
121
220
|
|
|
122
|
-
// Download extras (schema, etc.) once
|
|
221
|
+
// Download extras (schema, etc.) — only once across all agents
|
|
123
222
|
if (!skipExtras && method.extras) {
|
|
124
223
|
for (const [key, filePath] of Object.entries(method.extras)) {
|
|
125
224
|
const extraSpinner = ora(`Fetching ${key}...`).start()
|
|
@@ -136,18 +235,26 @@ async function installForAgent(method, agent, options, skipExtras = false) {
|
|
|
136
235
|
}
|
|
137
236
|
}
|
|
138
237
|
|
|
238
|
+
// ─────────────────────────────────────────────────────────────────
|
|
239
|
+
// Post-install hints
|
|
240
|
+
// ─────────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
139
242
|
function printHints(methodId) {
|
|
140
243
|
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(
|
|
244
|
+
console.log(chalk.dim(' Next steps:'))
|
|
245
|
+
console.log(chalk.dim(' 1. Give your agent a spectech (stack + modules needed)'))
|
|
246
|
+
console.log(chalk.dim(' 2. Agent declares architecture — confirm it'))
|
|
247
|
+
console.log(chalk.dim(' 3. Agent builds Core → modules → Admin Panel'))
|
|
145
248
|
console.log()
|
|
146
249
|
console.log(chalk.dim(` ${chalk.white('enet new module <name>')} scaffold your first module`))
|
|
147
250
|
console.log(chalk.dim(` ${chalk.white('enet validate')} check manifests at any time\n`))
|
|
148
251
|
}
|
|
149
252
|
if (methodId === 'pdca-t') {
|
|
150
|
-
console.log(chalk.dim(
|
|
253
|
+
console.log(chalk.dim(' Next steps:'))
|
|
254
|
+
console.log(chalk.dim(' 1. Start any coding task — the method activates automatically'))
|
|
255
|
+
console.log(chalk.dim(' 2. Your agent will follow the 8-phase quality cycle'))
|
|
256
|
+
console.log(chalk.dim(' 3. Every delivery includes a full test report'))
|
|
257
|
+
console.log()
|
|
151
258
|
console.log(chalk.dim(` Works best alongside ${chalk.white('enet install modular-design')}.\n`))
|
|
152
259
|
}
|
|
153
260
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import chalk from 'chalk'
|
|
2
2
|
import ora from 'ora'
|
|
3
3
|
import fs from 'fs-extra'
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import readline from 'readline'
|
|
6
|
+
import { getAllMethods, getMethod, readInstallRecord, writeInstallRecord } from '../utils/registry.js'
|
|
7
|
+
import { detectSystemAgents, getInstallPath, AGENTS } from '../utils/agent-detector.js'
|
|
8
|
+
import { installForAgent } from './install.js'
|
|
6
9
|
|
|
7
10
|
export async function updateCommand(methodId, options) {
|
|
8
11
|
const spinner = ora('Fetching registry...').start()
|
|
9
|
-
const [allMethods,
|
|
12
|
+
const [allMethods, detectedAgents] = await Promise.all([getAllMethods(), detectSystemAgents()])
|
|
10
13
|
spinner.stop()
|
|
11
14
|
|
|
12
15
|
const targets = methodId
|
|
@@ -18,30 +21,203 @@ export async function updateCommand(methodId, options) {
|
|
|
18
21
|
process.exit(1)
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
console.log(chalk.
|
|
24
|
+
console.log(chalk.bold('\n ◆ enet update\n'))
|
|
22
25
|
|
|
23
|
-
let
|
|
26
|
+
let totalUpdated = 0, totalAdded = 0, totalSkipped = 0
|
|
24
27
|
|
|
25
28
|
for (const method of targets) {
|
|
26
|
-
const
|
|
27
|
-
if (!await fs.pathExists(installPath)) { skipped++; continue }
|
|
29
|
+
const record = await readInstallRecord(method.id)
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
if (!record || record.agents.length === 0) {
|
|
32
|
+
totalSkipped++
|
|
33
|
+
console.log(chalk.dim(` ${method.name} — not installed, skipping`))
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(chalk.white(` ${method.name}`))
|
|
38
|
+
console.log(chalk.dim(` Installed for: ${record.agents.join(', ')}\n`))
|
|
39
|
+
|
|
40
|
+
// Agents already installed for this method
|
|
41
|
+
const installedAgents = record.agents
|
|
42
|
+
.map(key => AGENTS[key] ? { key, ...AGENTS[key] } : null)
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
|
|
45
|
+
// Agents newly detected but not yet installed for this method
|
|
46
|
+
const newAgents = detectedAgents.filter(a =>
|
|
47
|
+
!record.agents.includes(a.key) &&
|
|
48
|
+
(method.adapters[a.key] || method.adapters['generic'])
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// 1. Update already-installed adapters
|
|
52
|
+
if (!options.addOnly) {
|
|
53
|
+
for (const agent of installedAgents) {
|
|
54
|
+
const ok = await updateOneAdapter(method, agent, options)
|
|
55
|
+
if (ok) totalUpdated++
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Offer to add newly detected agents
|
|
60
|
+
if (newAgents.length > 0 && !options.updateOnly) {
|
|
61
|
+
console.log(chalk.yellow(`\n New agents detected since last install:\n`))
|
|
62
|
+
|
|
63
|
+
let agentsToAdd = []
|
|
64
|
+
if (options.all) {
|
|
65
|
+
agentsToAdd = newAgents
|
|
66
|
+
console.log(chalk.dim(` Adding all: ${newAgents.map(a => a.name).join(', ')}\n`))
|
|
67
|
+
} else {
|
|
68
|
+
agentsToAdd = await checkboxSelectNew(newAgents, method)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const agent of agentsToAdd) {
|
|
72
|
+
const ok = await installForAgent(method, agent, options, true)
|
|
73
|
+
if (ok) { totalAdded++; record.agents.push(agent.key) }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await writeInstallRecord(method.id, { agents: record.agents, version: method.version })
|
|
77
|
+
} else if (newAgents.length === 0 && !options.addOnly) {
|
|
78
|
+
console.log(chalk.dim(` No new agents detected.\n`))
|
|
38
79
|
}
|
|
39
80
|
}
|
|
40
81
|
|
|
82
|
+
console.log(chalk.dim(' ─────────────────────────────'))
|
|
83
|
+
if (totalUpdated > 0) console.log(chalk.green(` ✓ ${totalUpdated} adapter${totalUpdated !== 1 ? 's' : ''} updated`))
|
|
84
|
+
if (totalAdded > 0) console.log(chalk.green(` ✓ ${totalAdded} new adapter${totalAdded !== 1 ? 's' : ''} installed`))
|
|
85
|
+
if (totalSkipped > 0) console.log(chalk.dim(` ${totalSkipped} method${totalSkipped !== 1 ? 's' : ''} not installed, skipped`))
|
|
86
|
+
if (totalUpdated === 0 && totalAdded === 0 && totalSkipped === 0) {
|
|
87
|
+
console.log(chalk.dim(' Everything is up to date.'))
|
|
88
|
+
}
|
|
41
89
|
console.log()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function updateOneAdapter(method, agent, options = {}) {
|
|
93
|
+
const isGlobal = options.global || false
|
|
94
|
+
const adapterKey = method.adapters[agent.key] ? agent.key : 'generic'
|
|
95
|
+
const adapterPath = method.adapters[adapterKey]
|
|
96
|
+
|
|
97
|
+
if (!adapterPath) {
|
|
98
|
+
console.log(chalk.dim(` ${agent.name} — no adapter in registry, skipping`))
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const installPath = getInstallPath(agent, method.id, { global: isGlobal })
|
|
103
|
+
const exists = await fs.pathExists(installPath)
|
|
104
|
+
const action = exists ? 'Updating' : 'Restoring'
|
|
105
|
+
const s = ora(`${action} ${agent.name}...`).start()
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const { fetchFromGitHub } = await import('../utils/registry.js')
|
|
109
|
+
const content = await fetchFromGitHub(method.repo, adapterPath)
|
|
110
|
+
await fs.ensureDir(path.dirname(installPath))
|
|
111
|
+
|
|
112
|
+
if (agent.key === 'windsurf' && exists) {
|
|
113
|
+
const existing = await fs.readFile(installPath, 'utf8')
|
|
114
|
+
if (existing.includes(method.name)) {
|
|
115
|
+
const marker = '\n\n---\n\n'
|
|
116
|
+
const idx = existing.indexOf(method.name)
|
|
117
|
+
const before = existing.substring(0, existing.lastIndexOf(marker, idx) + marker.length)
|
|
118
|
+
await fs.writeFile(installPath, before + content)
|
|
119
|
+
} else {
|
|
120
|
+
await fs.appendFile(installPath, `\n\n---\n\n${content}`)
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
await fs.writeFile(installPath, content)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
s.succeed(chalk.green(`${agent.name} — ${action.toLowerCase()}d`))
|
|
127
|
+
console.log(chalk.dim(` → ${installPath}\n`))
|
|
128
|
+
return true
|
|
129
|
+
} catch (err) {
|
|
130
|
+
s.fail(chalk.red(`${agent.name} — ${err.message}`))
|
|
131
|
+
return false
|
|
46
132
|
}
|
|
47
133
|
}
|
|
134
|
+
|
|
135
|
+
async function checkboxSelectNew(newAgents, method) {
|
|
136
|
+
const items = newAgents.map(a => ({
|
|
137
|
+
agent: a, checked: true, usesGeneric: !method.adapters[a.key]
|
|
138
|
+
}))
|
|
139
|
+
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
let cursor = 0
|
|
142
|
+
const lineCount = () => items.length + 4
|
|
143
|
+
|
|
144
|
+
const render = () => {
|
|
145
|
+
if (render.drawn) process.stdout.write(`\x1B[${lineCount()}A`)
|
|
146
|
+
render.drawn = true
|
|
147
|
+
items.forEach((item, i) => {
|
|
148
|
+
const isCursor = i === cursor
|
|
149
|
+
const box = item.checked ? chalk.green('[✓]') : chalk.dim('[ ]')
|
|
150
|
+
const arrow = isCursor ? chalk.cyan(' ❯ ') : ' '
|
|
151
|
+
const name = isCursor ? chalk.white(item.agent.name) : chalk.dim(item.agent.name)
|
|
152
|
+
const tag = item.usesGeneric ? chalk.dim(' (generic adapter)') : ''
|
|
153
|
+
process.stdout.write(`${arrow}${box} ${name}${tag}\n`)
|
|
154
|
+
})
|
|
155
|
+
process.stdout.write('\n')
|
|
156
|
+
process.stdout.write(chalk.dim(' ↑↓ navigate · Space toggle · A all · Enter confirm\n\n'))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
render()
|
|
160
|
+
|
|
161
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
162
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true)
|
|
163
|
+
process.stdin.resume()
|
|
164
|
+
|
|
165
|
+
process.stdin.on('data', (key) => {
|
|
166
|
+
const k = key.toString()
|
|
167
|
+
if (k === '\u001b[A') { cursor = (cursor - 1 + items.length) % items.length; render() }
|
|
168
|
+
else if (k === '\u001b[B') { cursor = (cursor + 1) % items.length; render() }
|
|
169
|
+
else if (k === ' ') { items[cursor].checked = !items[cursor].checked; render() }
|
|
170
|
+
else if (k === 'a' || k === 'A') {
|
|
171
|
+
const all = items.every(i => i.checked)
|
|
172
|
+
items.forEach(i => { i.checked = !all }); render()
|
|
173
|
+
}
|
|
174
|
+
else if (k === '\r' || k === '\n') {
|
|
175
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false)
|
|
176
|
+
process.stdin.pause(); rl.close()
|
|
177
|
+
resolve(items.filter(i => i.checked).map(i => i.agent))
|
|
178
|
+
}
|
|
179
|
+
else if (k === '\u0003') {
|
|
180
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false)
|
|
181
|
+
process.stdin.pause(); rl.close()
|
|
182
|
+
console.log('\n'); process.exit(0)
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Dónde van estos archivos en el repo
|
|
192
|
+
```
|
|
193
|
+
exchanet/enet/
|
|
194
|
+
├── src/
|
|
195
|
+
│ ├── commands/
|
|
196
|
+
│ │ ├── install.js ← cambios A, B, C + checkboxSelect con alreadyInstalled
|
|
197
|
+
│ │ └── update.js ← reemplazar completo
|
|
198
|
+
│ └── utils/
|
|
199
|
+
│ └── registry.js ← añadir al final las 2 funciones nuevas
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Una vez subidos, el flujo queda así:
|
|
203
|
+
```
|
|
204
|
+
$ enet install pdca-t # primera vez
|
|
205
|
+
Already installed for: copilot ← lee .enet/installed.json
|
|
206
|
+
|
|
207
|
+
Select adapters to install:
|
|
208
|
+
❯ [✓] Cursor — new
|
|
209
|
+
[✓] Antigravity — new
|
|
210
|
+
[✓] Claude Code — new
|
|
211
|
+
[✓] GitHub Copilot — installed ← sabe que ya está
|
|
212
|
+
|
|
213
|
+
$ enet update pdca-t # después de instalar Windsurf
|
|
214
|
+
Method PDCA-T
|
|
215
|
+
Installed for: cursor, claudecode
|
|
216
|
+
|
|
217
|
+
Updating Cursor... ✓ ← re-descarga adapter
|
|
218
|
+
Updating Claude Code... ✓
|
|
219
|
+
|
|
220
|
+
New agents detected since last install:
|
|
221
|
+
❯ [✓] Windsurf — new
|
|
222
|
+
|
|
223
|
+
→ instala Windsurf y actualiza installed.json
|
|
@@ -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
|
}
|
package/src/utils/registry.js
CHANGED
|
@@ -83,3 +83,31 @@ export async function fetchFromGitHub(repo, filePath) {
|
|
|
83
83
|
|
|
84
84
|
return res.text()
|
|
85
85
|
}
|
|
86
|
+
|
|
87
|
+
// ── Install state ─────────────────────────────────────────────────────────────
|
|
88
|
+
// Stored in <project>/.enet/installed.json
|
|
89
|
+
// { "pdca-t": { "agents": ["cursor", "claudecode"], "version": "3.0.0", "updatedAt": "..." } }
|
|
90
|
+
|
|
91
|
+
function getInstallRecordFile() {
|
|
92
|
+
return path.join(process.cwd(), '.enet', 'installed.json')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function readInstallRecord(methodId) {
|
|
96
|
+
try {
|
|
97
|
+
const file = getInstallRecordFile()
|
|
98
|
+
if (!await fs.pathExists(file)) return null
|
|
99
|
+
const data = await fs.readJson(file)
|
|
100
|
+
return data[methodId] ?? null
|
|
101
|
+
} catch { return null }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function writeInstallRecord(methodId, record) {
|
|
105
|
+
try {
|
|
106
|
+
const file = getInstallRecordFile()
|
|
107
|
+
await fs.ensureDir(path.dirname(file))
|
|
108
|
+
let data = {}
|
|
109
|
+
if (await fs.pathExists(file)) data = await fs.readJson(file).catch(() => ({}))
|
|
110
|
+
data[methodId] = { agents: record.agents, version: record.version ?? null, updatedAt: new Date().toISOString() }
|
|
111
|
+
await fs.writeJson(file, data, { spaces: 2 })
|
|
112
|
+
} catch { /* non-fatal */ }
|
|
113
|
+
}
|