@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exchanet/enet",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "enet — exchanet methods manager. Install, scaffold and manage AI coding methods.",
5
5
  "bin": {
6
6
  "enet": "src/index.js"
@@ -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(` ✗ Unknown method: "${methodId}"`))
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
- // Determine target agents
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 a specific agent only
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: cursor, windsurf, antigravity, claudecode, copilot, generic\n`))
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
- if (detected.length === 1 || options.global) {
44
- // Only one detected, or --global flag: install for all without asking
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
- // Multiple agents detected: ask the user
48
- console.log(chalk.white(`\n Detected ${detected.length} agents on this system:\n`))
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
- const choice = parseInt(answer.trim())
60
- if (choice === detected.length + 1) {
61
- targetAgents = detected
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 target agent
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 // only install schema once
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} does not support global install skipping`))
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 global appends to global_rules.md
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} — installed`))
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(` 1. Give your agent a spectech (stack + modules needed)`))
143
- console.log(chalk.dim(` 2. Agent declares architecture — confirm it`))
144
- console.log(chalk.dim(` 3. Agent builds Core → modules → Admin Panel`))
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(` PDCA-T adds quality validation to your workflow.`))
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
  }
@@ -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 { getAllMethods, getMethod, fetchFromGitHub } from '../utils/registry.js'
5
- import { detectAgent, getInstallPath } from '../utils/agent-detector.js'
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, agent] = await Promise.all([getAllMethods(), detectAgent()])
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.white(`\n Updating methods...\n`))
24
+ console.log(chalk.bold('\n enet update\n'))
22
25
 
23
- let updated = 0, skipped = 0
26
+ let totalUpdated = 0, totalAdded = 0, totalSkipped = 0
24
27
 
25
28
  for (const method of targets) {
26
- const installPath = getInstallPath(agent, method.id)
27
- if (!await fs.pathExists(installPath)) { skipped++; continue }
29
+ const record = await readInstallRecord(method.id)
28
30
 
29
- const s = ora(`Updating ${method.name}...`).start()
30
- try {
31
- const adapterPath = method.adapters[agent.key] ?? method.adapters.generic
32
- const content = await fetchFromGitHub(method.repo, adapterPath)
33
- await fs.writeFile(installPath, content)
34
- s.succeed(chalk.green(`${method.name} updated`))
35
- updated++
36
- } catch (err) {
37
- s.fail(chalk.yellow(`${method.name} — ${err.message}`))
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
- if (updated === 0 && skipped > 0) {
43
- console.log(chalk.dim(` No methods installed to update.\n`))
44
- } else {
45
- console.log(chalk.dim(` ${updated} updated, ${skipped} not installed\n`))
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 global_rules.md'
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', 'method-modular-design'),
47
+ globalInstallDir: path.join(HOME, '.gemini', 'antigravity', 'skills'),
39
48
  globalFilename: 'SKILL.md',
40
49
  filename: 'enet-{id}.md',
41
- configNote: 'Skill saved set activation to Always On in Antigravity'
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 ~/.claude/CLAUDE.md — Claude Code reads this automatically'
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 Agent',
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: 'Saved to .enet/ — paste contents into your agent\'s context'
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
- if (await fs.pathExists(signal)) {
84
- found.push({ key, ...agent })
85
- break
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
  }
@@ -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
+ }