@exchanet/enet 1.0.10 → 1.0.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exchanet/enet",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "enet — exchanet methods manager. Install, scaffold and manage AI coding methods.",
5
5
  "bin": {
6
6
  "enet": "src/index.js"
@@ -4,7 +4,7 @@ import fs from 'fs-extra'
4
4
  import path from 'path'
5
5
  import readline from 'readline'
6
6
  import { detectSystemAgents, getInstallPath, AGENTS } from '../utils/agent-detector.js'
7
- import { getMethod, fetchFromGitHub } from '../utils/registry.js'
7
+ import { getMethod, fetchFromGitHub, readInstallRecord, writeInstallRecord } from '../utils/registry.js'
8
8
 
9
9
  export async function installCommand(methodId, options) {
10
10
  // 1. Load method from registry
@@ -20,17 +20,20 @@ export async function installCommand(methodId, options) {
20
20
 
21
21
  console.log(chalk.bold(`\n ◆ ${method.name}`))
22
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
23
 
29
- // 2. Determine target agents
24
+ // 2. Read existing install record — know what is already installed
25
+ const record = await readInstallRecord(methodId)
26
+ const alreadyInstalled = new Set(record?.agents ?? [])
27
+
28
+ if (alreadyInstalled.size > 0) {
29
+ console.log(chalk.dim(` Already installed for: ${[...alreadyInstalled].join(', ')}\n`))
30
+ }
31
+
32
+ // 3. Determine target agents
30
33
  let targetAgents = []
31
34
 
32
35
  if (options.agent) {
33
- // --agent flag: skip detection, install for this specific agent only
36
+ // --agent flag: bypass detection and checkbox entirely
34
37
  if (!AGENTS[options.agent]) {
35
38
  console.log(chalk.red(` ✗ Unknown agent: "${options.agent}"`))
36
39
  console.log(chalk.dim(` Valid: ${Object.keys(AGENTS).filter(k => k !== 'generic').join(', ')}\n`))
@@ -39,23 +42,20 @@ export async function installCommand(methodId, options) {
39
42
  targetAgents = [{ key: options.agent, ...AGENTS[options.agent] }]
40
43
 
41
44
  } else {
42
- // Detect all agents installed on the system
43
45
  const detected = await detectSystemAgents()
44
46
 
45
47
  if (detected.length === 0) {
46
48
  console.log(chalk.yellow(' ⚠ No AI agents detected on this system.'))
47
- console.log(chalk.dim(` Use ${chalk.white('--agent <name>')} to force an agent.`))
49
+ console.log(chalk.dim(` Use ${chalk.white('--agent <n>')} to force an agent.`))
48
50
  console.log(chalk.dim(` Valid: ${Object.keys(AGENTS).filter(k => k !== 'generic').join(', ')}\n`))
49
51
  process.exit(1)
50
52
  }
51
53
 
52
- // 3. Show checkbox selection — always, even with 1 agent detected
53
- // This is the core UX fix: user always chooses, never surprised
54
54
  if (options.all) {
55
- // --all flag skips the prompt
56
55
  targetAgents = detected
57
56
  } else {
58
- targetAgents = await checkboxSelect(detected, method)
57
+ // Always show checkbox — even with 1 agent, even on re-run
58
+ targetAgents = await checkboxSelect(detected, method, alreadyInstalled)
59
59
  }
60
60
 
61
61
  if (targetAgents.length === 0) {
@@ -66,66 +66,74 @@ export async function installCommand(methodId, options) {
66
66
 
67
67
  console.log()
68
68
 
69
- // 4. Install for each selected agent
69
+ // 4. Install each selected agent
70
+ const newlyInstalled = []
70
71
  let schemaInstalled = false
72
+
71
73
  for (const agent of targetAgents) {
72
- await installForAgent(method, agent, options, schemaInstalled)
74
+ const ok = await installForAgent(method, agent, options, schemaInstalled)
73
75
  schemaInstalled = true
76
+ if (ok) newlyInstalled.push(agent.key)
74
77
  }
75
78
 
79
+ // 5. Persist updated install record
80
+ const updatedAgents = [...new Set([...alreadyInstalled, ...newlyInstalled])]
81
+ await writeInstallRecord(methodId, { agents: updatedAgents, version: method.version })
82
+
76
83
  printHints(methodId)
77
84
  }
78
85
 
79
86
  // ─────────────────────────────────────────────────────────────────
80
87
  // Checkbox selection UI
88
+ //
89
+ // Shows all detected agents with tags:
90
+ // — installed already present on disk for this method
91
+ // — new detected but never installed for this method
81
92
  // ─────────────────────────────────────────────────────────────────
82
93
 
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'])
94
+ async function checkboxSelect(detected, method, alreadyInstalled = new Set()) {
95
+ const available = detected.filter(a => method.adapters[a.key] || method.adapters['generic'])
87
96
  const unavailable = detected.filter(a => !method.adapters[a.key] && !method.adapters['generic'])
88
97
 
89
98
  if (available.length === 0) {
90
99
  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`))
100
+ console.log(chalk.dim(` Adapters in this method: ${Object.keys(method.adapters).join(', ')}\n`))
92
101
  process.exit(1)
93
102
  }
94
103
 
95
- // Initial state: all available agents pre-checked
96
104
  const items = available.map(a => ({
97
- agent: a,
98
- checked: true,
105
+ agent: a,
106
+ checked: true,
107
+ installed: alreadyInstalled.has(a.key),
99
108
  usesGeneric: !method.adapters[a.key]
100
109
  }))
101
110
 
102
- console.log(chalk.white(' Agents detected on your system:\n'))
111
+ console.log(chalk.white(' Select adapters to install:\n'))
103
112
 
104
113
  return new Promise((resolve) => {
105
114
  let cursor = 0
106
115
 
116
+ const lineCount = () => items.length + (unavailable.length > 0 ? 3 : 2) + 2
117
+
107
118
  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`)
119
+ if (render.drawn) process.stdout.write(`\x1B[${lineCount()}A`)
111
120
  render.drawn = true
112
121
 
113
122
  items.forEach((item, i) => {
114
123
  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`)
124
+ const box = item.checked ? chalk.green('[✓]') : chalk.dim('[ ]')
125
+ const arrow = isCursor ? chalk.cyan(' ❯ ') : ' '
126
+ const name = isCursor ? chalk.white(item.agent.name) : chalk.dim(item.agent.name)
127
+ const status = item.installed ? chalk.dim(' installed') : chalk.yellow(' — new')
128
+ const generic = item.usesGeneric ? chalk.dim(' (generic adapter)') : ''
129
+ process.stdout.write(`${arrow}${box} ${name}${status}${generic}\n`)
120
130
  })
121
131
 
122
132
  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')
133
+ process.stdout.write(chalk.dim(`\n No adapter for: ${unavailable.map(a => a.name).join(', ')}\n`))
126
134
  }
127
-
128
- process.stdout.write(chalk.dim(' ↑↓ navigate · Space toggle · A select all · Enter confirm\n\n'))
135
+ process.stdout.write('\n')
136
+ process.stdout.write(chalk.dim(' ↑↓ navigate · Space toggle · A all · Enter confirm · Ctrl+C cancel\n\n'))
129
137
  }
130
138
 
131
139
  render()
@@ -136,28 +144,25 @@ async function checkboxSelect(detected, method) {
136
144
 
137
145
  process.stdin.on('data', (key) => {
138
146
  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
147
+ if (k === '\u001b[A') { cursor = (cursor - 1 + items.length) % items.length; render() }
148
+ else if (k === '\u001b[B') { cursor = (cursor + 1) % items.length; render() }
149
+ else if (k === ' ') { items[cursor].checked = !items[cursor].checked; render() }
150
+ else if (k === 'a' || k === 'A') {
151
+ const all = items.every(i => i.checked)
152
+ items.forEach(i => { i.checked = !all })
148
153
  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
+ }
155
+ else if (k === '\r' || k === '\n') {
154
156
  if (process.stdin.isTTY) process.stdin.setRawMode(false)
155
157
  process.stdin.pause()
156
158
  rl.close()
157
159
  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`))
160
+ if (selected.length > 0) {
161
+ console.log(chalk.dim(`\n Installing for: ${selected.map(a => a.name).join(', ')}\n`))
162
+ }
159
163
  resolve(selected)
160
- } else if (k === '\u0003') { // Ctrl+C
164
+ }
165
+ else if (k === '\u0003') {
161
166
  if (process.stdin.isTTY) process.stdin.setRawMode(false)
162
167
  process.stdin.pause()
163
168
  rl.close()
@@ -170,41 +175,44 @@ async function checkboxSelect(detected, method) {
170
175
 
171
176
  // ─────────────────────────────────────────────────────────────────
172
177
  // Install for one agent
178
+ // Exported so update.js can reuse it without duplication
179
+ // Returns true on success, false on failure/skip
173
180
  // ─────────────────────────────────────────────────────────────────
174
181
 
175
- async function installForAgent(method, agent, options, skipExtras = false) {
176
- const isGlobal = options.global || false
177
-
178
- // Use the agent-specific adapter if available, fall back to generic
179
- const adapterKey = method.adapters[agent.key] ? agent.key : 'generic'
182
+ export async function installForAgent(method, agent, options = {}, skipExtras = false) {
183
+ const isGlobal = options.global || false
184
+ const adapterKey = method.adapters[agent.key] ? agent.key : 'generic'
180
185
  const adapterPath = method.adapters[adapterKey]
181
186
 
182
187
  if (!adapterPath) {
183
188
  console.log(chalk.yellow(` ⚠ ${agent.name} — no adapter found, skipping`))
184
- return
189
+ return false
185
190
  }
186
191
 
187
192
  if (isGlobal && !agent.globalInstallDir) {
188
193
  console.log(chalk.yellow(` ⚠ ${agent.name} — global install not supported, skipping`))
189
- return
194
+ return false
190
195
  }
191
196
 
192
- const spinner = ora(`Installing for ${agent.name}${isGlobal ? ' (global)' : ''}...`).start()
197
+ const spinner = ora(`${agent.name}${isGlobal ? ' (global)' : ''}...`).start()
193
198
 
194
199
  try {
195
- const content = await fetchFromGitHub(method.repo, adapterPath)
200
+ const content = await fetchFromGitHub(method.repo, adapterPath)
196
201
  const installPath = getInstallPath(agent, method.id, { global: isGlobal })
197
202
 
198
203
  await fs.ensureDir(path.dirname(installPath))
199
204
 
200
- // Windsurf: append to .windsurfrules instead of overwriting
205
+ // Windsurf: append to .windsurfrules replace existing section if already present
201
206
  if (agent.key === 'windsurf' && await fs.pathExists(installPath)) {
202
207
  const existing = await fs.readFile(installPath, 'utf8')
203
208
  if (existing.includes(method.name)) {
204
- spinner.warn(chalk.yellow(`${agent.name} already installed`))
205
- return
209
+ const marker = '\n\n---\n\n'
210
+ const idx = existing.indexOf(method.name)
211
+ const before = existing.substring(0, existing.lastIndexOf(marker, idx) + marker.length)
212
+ await fs.writeFile(installPath, before + content)
213
+ } else {
214
+ await fs.appendFile(installPath, `\n\n---\n\n${content}`)
206
215
  }
207
- await fs.appendFile(installPath, `\n\n---\n\n${content}`)
208
216
  } else {
209
217
  await fs.writeFile(installPath, content)
210
218
  }
@@ -215,24 +223,25 @@ async function installForAgent(method, agent, options, skipExtras = false) {
215
223
 
216
224
  } catch (err) {
217
225
  spinner.fail(chalk.red(`${agent.name} — ${err.message}`))
218
- return
226
+ return false
219
227
  }
220
228
 
221
- // Download extras (schema, etc.) — only once across all agents
229
+ // Extras (schema, manifests…) — downloaded once across all agents
222
230
  if (!skipExtras && method.extras) {
223
231
  for (const [key, filePath] of Object.entries(method.extras)) {
224
- const extraSpinner = ora(`Fetching ${key}...`).start()
232
+ const s = ora(`Fetching ${key}...`).start()
225
233
  try {
226
- const extraContent = await fetchFromGitHub(method.repo, filePath)
227
- const extraOut = path.join(process.cwd(), path.basename(filePath))
228
- await fs.writeFile(extraOut, extraContent)
229
- extraSpinner.succeed(chalk.dim(`${key} → ${path.basename(filePath)}`))
234
+ const content = await fetchFromGitHub(method.repo, filePath)
235
+ await fs.writeFile(path.join(process.cwd(), path.basename(filePath)), content)
236
+ s.succeed(chalk.dim(`${key} → ${path.basename(filePath)}`))
230
237
  } catch {
231
- extraSpinner.warn(chalk.dim(`${key} not available (non-critical)`))
238
+ s.warn(chalk.dim(`${key} not available (non-critical)`))
232
239
  }
233
240
  }
234
241
  console.log()
235
242
  }
243
+
244
+ return true
236
245
  }
237
246
 
238
247
  // ─────────────────────────────────────────────────────────────────
@@ -246,7 +255,7 @@ function printHints(methodId) {
246
255
  console.log(chalk.dim(' 2. Agent declares architecture — confirm it'))
247
256
  console.log(chalk.dim(' 3. Agent builds Core → modules → Admin Panel'))
248
257
  console.log()
249
- console.log(chalk.dim(` ${chalk.white('enet new module <name>')} scaffold your first module`))
258
+ console.log(chalk.dim(` ${chalk.white('enet new module <n>')} scaffold your first module`))
250
259
  console.log(chalk.dim(` ${chalk.white('enet validate')} check manifests at any time\n`))
251
260
  }
252
261
  if (methodId === 'pdca-t') {
@@ -3,13 +3,16 @@ import ora from 'ora'
3
3
  import fs from 'fs-extra'
4
4
  import path from 'path'
5
5
  import readline from 'readline'
6
- import { getAllMethods, getMethod, readInstallRecord, writeInstallRecord } from '../utils/registry.js'
6
+ import { getAllMethods, getMethod, fetchFromGitHub, readInstallRecord, writeInstallRecord } from '../utils/registry.js'
7
7
  import { detectSystemAgents, getInstallPath, AGENTS } from '../utils/agent-detector.js'
8
8
  import { installForAgent } from './install.js'
9
9
 
10
10
  export async function updateCommand(methodId, options) {
11
11
  const spinner = ora('Fetching registry...').start()
12
- const [allMethods, detectedAgents] = await Promise.all([getAllMethods(), detectSystemAgents()])
12
+ const [allMethods, detectedAgents] = await Promise.all([
13
+ getAllMethods(),
14
+ detectSystemAgents()
15
+ ])
13
16
  spinner.stop()
14
17
 
15
18
  const targets = methodId
@@ -28,6 +31,7 @@ export async function updateCommand(methodId, options) {
28
31
  for (const method of targets) {
29
32
  const record = await readInstallRecord(method.id)
30
33
 
34
+ // Skip methods not installed at all
31
35
  if (!record || record.agents.length === 0) {
32
36
  totalSkipped++
33
37
  console.log(chalk.dim(` ${method.name} — not installed, skipping`))
@@ -42,13 +46,13 @@ export async function updateCommand(methodId, options) {
42
46
  .map(key => AGENTS[key] ? { key, ...AGENTS[key] } : null)
43
47
  .filter(Boolean)
44
48
 
45
- // Agents newly detected but not yet installed for this method
49
+ // Agents newly detected on system but NOT yet installed for this method
46
50
  const newAgents = detectedAgents.filter(a =>
47
51
  !record.agents.includes(a.key) &&
48
52
  (method.adapters[a.key] || method.adapters['generic'])
49
53
  )
50
54
 
51
- // 1. Update already-installed adapters
55
+ // ── 1. Update already-installed adapters ────────────────────
52
56
  if (!options.addOnly) {
53
57
  for (const agent of installedAgents) {
54
58
  const ok = await updateOneAdapter(method, agent, options)
@@ -56,7 +60,7 @@ export async function updateCommand(methodId, options) {
56
60
  }
57
61
  }
58
62
 
59
- // 2. Offer to add newly detected agents
63
+ // ── 2. Offer to add newly detected agents ───────────────────
60
64
  if (newAgents.length > 0 && !options.updateOnly) {
61
65
  console.log(chalk.yellow(`\n New agents detected since last install:\n`))
62
66
 
@@ -70,15 +74,21 @@ export async function updateCommand(methodId, options) {
70
74
 
71
75
  for (const agent of agentsToAdd) {
72
76
  const ok = await installForAgent(method, agent, options, true)
73
- if (ok) { totalAdded++; record.agents.push(agent.key) }
77
+ if (ok) {
78
+ totalAdded++
79
+ record.agents.push(agent.key)
80
+ }
74
81
  }
75
82
 
83
+ // Save updated record with newly added agents
76
84
  await writeInstallRecord(method.id, { agents: record.agents, version: method.version })
85
+
77
86
  } else if (newAgents.length === 0 && !options.addOnly) {
78
87
  console.log(chalk.dim(` No new agents detected.\n`))
79
88
  }
80
89
  }
81
90
 
91
+ // ── Summary ──────────────────────────────────────────────────
82
92
  console.log(chalk.dim(' ─────────────────────────────'))
83
93
  if (totalUpdated > 0) console.log(chalk.green(` ✓ ${totalUpdated} adapter${totalUpdated !== 1 ? 's' : ''} updated`))
84
94
  if (totalAdded > 0) console.log(chalk.green(` ✓ ${totalAdded} new adapter${totalAdded !== 1 ? 's' : ''} installed`))
@@ -89,9 +99,14 @@ export async function updateCommand(methodId, options) {
89
99
  console.log()
90
100
  }
91
101
 
102
+ // ─────────────────────────────────────────────────────────────────
103
+ // Update a single already-installed adapter
104
+ // Re-downloads from GitHub and overwrites the local file
105
+ // ─────────────────────────────────────────────────────────────────
106
+
92
107
  async function updateOneAdapter(method, agent, options = {}) {
93
- const isGlobal = options.global || false
94
- const adapterKey = method.adapters[agent.key] ? agent.key : 'generic'
108
+ const isGlobal = options.global || false
109
+ const adapterKey = method.adapters[agent.key] ? agent.key : 'generic'
95
110
  const adapterPath = method.adapters[adapterKey]
96
111
 
97
112
  if (!adapterPath) {
@@ -105,10 +120,10 @@ async function updateOneAdapter(method, agent, options = {}) {
105
120
  const s = ora(`${action} ${agent.name}...`).start()
106
121
 
107
122
  try {
108
- const { fetchFromGitHub } = await import('../utils/registry.js')
109
123
  const content = await fetchFromGitHub(method.repo, adapterPath)
110
124
  await fs.ensureDir(path.dirname(installPath))
111
125
 
126
+ // Windsurf: replace existing section, don't append duplicates
112
127
  if (agent.key === 'windsurf' && exists) {
113
128
  const existing = await fs.readFile(installPath, 'utf8')
114
129
  if (existing.includes(method.name)) {
@@ -126,15 +141,22 @@ async function updateOneAdapter(method, agent, options = {}) {
126
141
  s.succeed(chalk.green(`${agent.name} — ${action.toLowerCase()}d`))
127
142
  console.log(chalk.dim(` → ${installPath}\n`))
128
143
  return true
144
+
129
145
  } catch (err) {
130
146
  s.fail(chalk.red(`${agent.name} — ${err.message}`))
131
147
  return false
132
148
  }
133
149
  }
134
150
 
151
+ // ─────────────────────────────────────────────────────────────────
152
+ // Checkbox for new agents only (shown during update)
153
+ // ─────────────────────────────────────────────────────────────────
154
+
135
155
  async function checkboxSelectNew(newAgents, method) {
136
156
  const items = newAgents.map(a => ({
137
- agent: a, checked: true, usesGeneric: !method.adapters[a.key]
157
+ agent: a,
158
+ checked: true,
159
+ usesGeneric: !method.adapters[a.key]
138
160
  }))
139
161
 
140
162
  return new Promise((resolve) => {
@@ -144,16 +166,18 @@ async function checkboxSelectNew(newAgents, method) {
144
166
  const render = () => {
145
167
  if (render.drawn) process.stdout.write(`\x1B[${lineCount()}A`)
146
168
  render.drawn = true
169
+
147
170
  items.forEach((item, i) => {
148
171
  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`)
172
+ const box = item.checked ? chalk.green('[✓]') : chalk.dim('[ ]')
173
+ const arrow = isCursor ? chalk.cyan(' ❯ ') : ' '
174
+ const name = isCursor ? chalk.white(item.agent.name) : chalk.dim(item.agent.name)
175
+ const generic = item.usesGeneric ? chalk.dim(' (generic adapter)') : ''
176
+ process.stdout.write(`${arrow}${box} ${name}${generic}\n`)
154
177
  })
178
+
155
179
  process.stdout.write('\n')
156
- process.stdout.write(chalk.dim(' ↑↓ navigate · Space toggle · A all · Enter confirm\n\n'))
180
+ process.stdout.write(chalk.dim(' ↑↓ navigate · Space toggle · A all · Enter confirm · Ctrl+C cancel\n\n'))
157
181
  }
158
182
 
159
183
  render()
@@ -169,55 +193,22 @@ async function checkboxSelectNew(newAgents, method) {
169
193
  else if (k === ' ') { items[cursor].checked = !items[cursor].checked; render() }
170
194
  else if (k === 'a' || k === 'A') {
171
195
  const all = items.every(i => i.checked)
172
- items.forEach(i => { i.checked = !all }); render()
196
+ items.forEach(i => { i.checked = !all })
197
+ render()
173
198
  }
174
199
  else if (k === '\r' || k === '\n') {
175
200
  if (process.stdin.isTTY) process.stdin.setRawMode(false)
176
- process.stdin.pause(); rl.close()
201
+ process.stdin.pause()
202
+ rl.close()
177
203
  resolve(items.filter(i => i.checked).map(i => i.agent))
178
204
  }
179
205
  else if (k === '\u0003') {
180
206
  if (process.stdin.isTTY) process.stdin.setRawMode(false)
181
- process.stdin.pause(); rl.close()
182
- console.log('\n'); process.exit(0)
207
+ process.stdin.pause()
208
+ rl.close()
209
+ console.log('\n')
210
+ process.exit(0)
183
211
  }
184
212
  })
185
213
  })
186
214
  }
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
@@ -9,9 +9,9 @@ export const AGENTS = {
9
9
  name: 'Cursor',
10
10
  systemSignals: [
11
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
+ path.join(HOME, 'Library', 'Application Support', 'Cursor'), // macOS
13
+ path.join(HOME, 'AppData', 'Roaming', 'Cursor'), // Windows
14
+ path.join(HOME, '.config', 'Cursor'), // Linux
15
15
  ],
16
16
  projectSignals: ['.cursor/rules', '.cursor'],
17
17
  projectInstallDir: '.cursor/rules',
@@ -24,8 +24,8 @@ export const AGENTS = {
24
24
  name: 'Windsurf',
25
25
  systemSignals: [
26
26
  path.join(HOME, '.codeium', 'windsurf'),
27
- path.join(HOME, 'Library', 'Application Support', 'Windsurf'),
28
- path.join(HOME, 'AppData', 'Roaming', 'Windsurf'),
27
+ path.join(HOME, 'Library', 'Application Support', 'Windsurf'), // macOS
28
+ path.join(HOME, 'AppData', 'Roaming', 'Windsurf'), // Windows
29
29
  ],
30
30
  projectSignals: ['.windsurfrules', '.windsurf'],
31
31
  projectInstallDir: '.',
@@ -39,11 +39,13 @@ export const AGENTS = {
39
39
  name: 'Antigravity (Google)',
40
40
  systemSignals: [
41
41
  path.join(HOME, '.gemini', 'antigravity'),
42
- path.join(HOME, 'Library', 'Application Support', 'Google', 'Antigravity'),
43
- path.join(HOME, 'AppData', 'Roaming', 'Google', 'Antigravity'),
42
+ path.join(HOME, 'Library', 'Application Support', 'Google', 'Antigravity'), // macOS
43
+ path.join(HOME, 'AppData', 'Roaming', 'Google', 'Antigravity'), // Windows
44
44
  ],
45
45
  projectSignals: ['.agent/rules', '.agent'],
46
46
  projectInstallDir: '.agent/rules',
47
+ // globalInstallDir is a base — getInstallPath() resolves the full path dynamically:
48
+ // ~/.gemini/antigravity/skills/method-{id}/SKILL.md
47
49
  globalInstallDir: path.join(HOME, '.gemini', 'antigravity', 'skills'),
48
50
  globalFilename: 'SKILL.md',
49
51
  filename: 'enet-{id}.md',
@@ -65,13 +67,16 @@ export const AGENTS = {
65
67
 
66
68
  copilot: {
67
69
  name: 'GitHub Copilot',
70
+ // Copilot is a VS Code extension — detect by checking the extensions folder
71
+ // for a github.copilot-* subfolder, not just the presence of .vscode/
68
72
  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
+ path.join(HOME, '.vscode', 'extensions'), // Linux / Windows
74
+ path.join(HOME, 'Library', 'Application Support', 'Code', 'User', 'extensions'), // macOS
75
+ path.join(HOME, 'AppData', 'Roaming', 'Code', 'User', 'extensions'), // Windows alt
76
+ path.join(HOME, '.vscode-server', 'extensions'), // remote / SSH
73
77
  ],
74
78
  systemSignalFilter: (signalPath) => {
79
+ // Only return true if a github.copilot extension folder exists inside
75
80
  try {
76
81
  const entries = fs.readdirSync(signalPath)
77
82
  return entries.some(e => e.toLowerCase().startsWith('github.copilot'))
@@ -81,7 +86,7 @@ export const AGENTS = {
81
86
  },
82
87
  projectSignals: ['.github/copilot-instructions.md'],
83
88
  projectInstallDir: '.github',
84
- globalInstallDir: null,
89
+ globalInstallDir: null, // Copilot has no global rules path
85
90
  filename: 'copilot-instructions.md',
86
91
  configNote: 'Written to .github/copilot-instructions.md'
87
92
  },
@@ -93,10 +98,19 @@ export const AGENTS = {
93
98
  projectInstallDir: '.enet',
94
99
  globalInstallDir: null,
95
100
  filename: '{id}.md',
96
- configNote: "Saved to .enet/ — paste contents into your agent's context"
101
+ configNote: 'Saved to .enet/ — paste contents into your agent\'s context'
97
102
  }
98
103
  }
99
104
 
105
+ // ─────────────────────────────────────────────────────────────────
106
+ // Detection
107
+ // ─────────────────────────────────────────────────────────────────
108
+
109
+ /**
110
+ * Detects ALL agents installed on the system by checking known global paths.
111
+ * Uses systemSignalFilter for agents that share directories with other software
112
+ * (e.g. Copilot shares VS Code's extensions folder).
113
+ */
100
114
  export async function detectSystemAgents() {
101
115
  const found = []
102
116
  for (const [key, agent] of Object.entries(AGENTS)) {
@@ -109,7 +123,7 @@ export async function detectSystemAgents() {
109
123
  found.push({ key, ...agent })
110
124
  break
111
125
  }
112
- continue
126
+ continue // signal dir exists but filter didn't match — try next signal path
113
127
  }
114
128
  found.push({ key, ...agent })
115
129
  break
@@ -118,6 +132,9 @@ export async function detectSystemAgents() {
118
132
  return found
119
133
  }
120
134
 
135
+ /**
136
+ * Detects ALL agents present in the current project folder.
137
+ */
121
138
  export async function detectProjectAgents(cwd = process.cwd()) {
122
139
  const found = []
123
140
  for (const [key, agent] of Object.entries(AGENTS)) {
@@ -132,16 +149,33 @@ export async function detectProjectAgents(cwd = process.cwd()) {
132
149
  return found
133
150
  }
134
151
 
152
+ /**
153
+ * Returns the first detected agent (legacy — used by status/doctor).
154
+ */
135
155
  export async function detectAgent(cwd = process.cwd()) {
136
156
  const agents = await detectProjectAgents(cwd)
137
157
  return agents[0] ?? { key: 'generic', ...AGENTS.generic }
138
158
  }
139
159
 
160
+ // ─────────────────────────────────────────────────────────────────
161
+ // Path resolution
162
+ // ─────────────────────────────────────────────────────────────────
163
+
164
+ /**
165
+ * Returns the resolved install path for a given agent + method.
166
+ *
167
+ * Special cases:
168
+ * - antigravity global → ~/.gemini/antigravity/skills/method-{id}/SKILL.md
169
+ * Each method gets its own subfolder (not a shared file).
170
+ * - windsurf global → ~/.codeium/windsurf/memories/global_rules.md
171
+ * - claudecode global → ~/.claude/CLAUDE.md
172
+ */
140
173
  export function getInstallPath(agent, methodId, { global = false, cwd = process.cwd() } = {}) {
141
174
  if (global) {
142
- if (!agent.globalInstallDir) return null
175
+ if (!agent.globalInstallDir) return null // agent doesn't support global install
143
176
 
144
177
  if (agent.key === 'antigravity') {
178
+ // Dynamic subfolder per method: skills/method-{id}/SKILL.md
145
179
  return path.join(agent.globalInstallDir, `method-${methodId}`, 'SKILL.md')
146
180
  }
147
181
 
@@ -149,6 +183,7 @@ export function getInstallPath(agent, methodId, { global = false, cwd = process.
149
183
  return path.join(agent.globalInstallDir, filename)
150
184
  }
151
185
 
186
+ // Project-level install
152
187
  const filename = agent.filename.replace('{id}', methodId)
153
188
  return path.join(cwd, agent.projectInstallDir, filename)
154
189
  }
@@ -4,62 +4,40 @@ import path from 'path'
4
4
  import { fileURLToPath } from 'url'
5
5
 
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
- const RAW_BASE = 'https://raw.githubusercontent.com'
7
+ const RAW_BASE = 'https://raw.githubusercontent.com'
8
8
  const REGISTRY_URL = `${RAW_BASE}/exchanet/enet/main/registry.json`
9
9
  const CACHE_FILE = path.join(__dirname, '../../.registry-cache.json')
10
10
  const CACHE_TTL_MS = 1000 * 60 * 60 // 1 hour
11
11
 
12
12
  // ── Registry ──────────────────────────────────────────────────────────────────
13
13
 
14
- /**
15
- * Loads the registry from:
16
- * 1. Remote GitHub (exchanet/enet/registry.json) — always fresh
17
- * 2. Local cache if remote fails — fallback
18
- * 3. Bundled registry.json in the package — last resort
19
- */
20
14
  export async function loadRegistry() {
21
- // Try remote first
22
15
  try {
23
16
  const res = await fetch(REGISTRY_URL, { timeout: 5000 })
24
17
  if (res.ok) {
25
18
  const data = await res.json()
26
- // Save to cache for offline fallback
27
19
  await fs.writeJson(CACHE_FILE, { ...data, _cachedAt: Date.now() }).catch(() => {})
28
20
  return data
29
21
  }
30
- } catch {
31
- // Network unavailable — fall through to cache
32
- }
22
+ } catch { /* network unavailable */ }
33
23
 
34
- // Try local cache
35
24
  try {
36
25
  if (await fs.pathExists(CACHE_FILE)) {
37
26
  const cached = await fs.readJson(CACHE_FILE)
38
27
  const age = Date.now() - (cached._cachedAt || 0)
39
- if (age < CACHE_TTL_MS * 24) { // Accept cache up to 24h when offline
40
- return cached
41
- }
28
+ if (age < CACHE_TTL_MS * 24) return cached
42
29
  }
43
- } catch {
44
- // Cache corrupted — fall through to bundled
45
- }
30
+ } catch { /* cache corrupted */ }
46
31
 
47
- // Fallback to bundled registry.json
48
32
  const bundled = path.join(__dirname, '../../registry.json')
49
33
  return fs.readJson(bundled)
50
34
  }
51
35
 
52
- /**
53
- * Returns a single method from the registry, or null if not found.
54
- */
55
36
  export async function getMethod(id) {
56
37
  const registry = await loadRegistry()
57
38
  return registry.methods?.[id] ?? null
58
39
  }
59
40
 
60
- /**
61
- * Returns all methods from the registry as an array.
62
- */
63
41
  export async function getAllMethods() {
64
42
  const registry = await loadRegistry()
65
43
  return Object.values(registry.methods ?? {})
@@ -67,47 +45,69 @@ export async function getAllMethods() {
67
45
 
68
46
  // ── GitHub file fetcher ───────────────────────────────────────────────────────
69
47
 
70
- /**
71
- * Fetches a raw file from a GitHub repo (main branch).
72
- */
73
48
  export async function fetchFromGitHub(repo, filePath) {
74
49
  const url = `${RAW_BASE}/${repo}/main/${filePath}`
75
50
  const res = await fetch(url)
76
-
77
51
  if (!res.ok) {
78
52
  throw new Error(
79
53
  `Could not fetch ${filePath} from ${repo} (HTTP ${res.status})\n` +
80
54
  ` URL: ${url}`
81
55
  )
82
56
  }
83
-
84
57
  return res.text()
85
58
  }
86
59
 
87
60
  // ── Install state ─────────────────────────────────────────────────────────────
61
+ //
62
+ // Tracks which agents have each method installed.
88
63
  // Stored in <project>/.enet/installed.json
89
- // { "pdca-t": { "agents": ["cursor", "claudecode"], "version": "3.0.0", "updatedAt": "..." } }
64
+ //
65
+ // Format:
66
+ // {
67
+ // "pdca-t": {
68
+ // "agents": ["cursor", "claudecode", "antigravity"],
69
+ // "version": "3.0.0",
70
+ // "updatedAt": "2025-03-01T10:00:00.000Z"
71
+ // }
72
+ // }
90
73
 
91
74
  function getInstallRecordFile() {
92
75
  return path.join(process.cwd(), '.enet', 'installed.json')
93
76
  }
94
77
 
78
+ /**
79
+ * Returns the install record for a single method, or null if never installed.
80
+ */
95
81
  export async function readInstallRecord(methodId) {
96
82
  try {
97
83
  const file = getInstallRecordFile()
98
84
  if (!await fs.pathExists(file)) return null
99
85
  const data = await fs.readJson(file)
100
86
  return data[methodId] ?? null
101
- } catch { return null }
87
+ } catch {
88
+ return null
89
+ }
102
90
  }
103
91
 
92
+ /**
93
+ * Writes the install record for a single method.
94
+ * Merges with existing records — other methods are never touched.
95
+ */
104
96
  export async function writeInstallRecord(methodId, record) {
105
97
  try {
106
98
  const file = getInstallRecordFile()
107
99
  await fs.ensureDir(path.dirname(file))
108
100
  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() }
101
+ if (await fs.pathExists(file)) {
102
+ data = await fs.readJson(file).catch(() => ({}))
103
+ }
104
+ data[methodId] = {
105
+ agents: record.agents,
106
+ version: record.version ?? null,
107
+ updatedAt: new Date().toISOString()
108
+ }
111
109
  await fs.writeJson(file, data, { spaces: 2 })
112
- } catch { /* non-fatal */ }
113
- }
110
+ } catch {
111
+ // Non-fatal — install works correctly even if state cannot be saved
112
+ }
113
+ }