@exchanet/enet 1.0.9 → 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 +1 -1
- package/src/commands/install.js +85 -71
- package/src/commands/update.js +187 -20
- package/src/utils/{agent-detector.js → agent_detector.js} +50 -15
- package/src/utils/registry.js +59 -31
package/package.json
CHANGED
package/src/commands/install.js
CHANGED
|
@@ -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
|
|
@@ -21,11 +21,19 @@ export async function installCommand(methodId, options) {
|
|
|
21
21
|
console.log(chalk.bold(`\n ◆ ${method.name}`))
|
|
22
22
|
console.log(chalk.dim(` ${method.description}\n`))
|
|
23
23
|
|
|
24
|
-
// 2.
|
|
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
|
|
25
33
|
let targetAgents = []
|
|
26
34
|
|
|
27
35
|
if (options.agent) {
|
|
28
|
-
// --agent flag:
|
|
36
|
+
// --agent flag: bypass detection and checkbox entirely
|
|
29
37
|
if (!AGENTS[options.agent]) {
|
|
30
38
|
console.log(chalk.red(` ✗ Unknown agent: "${options.agent}"`))
|
|
31
39
|
console.log(chalk.dim(` Valid: ${Object.keys(AGENTS).filter(k => k !== 'generic').join(', ')}\n`))
|
|
@@ -34,23 +42,20 @@ export async function installCommand(methodId, options) {
|
|
|
34
42
|
targetAgents = [{ key: options.agent, ...AGENTS[options.agent] }]
|
|
35
43
|
|
|
36
44
|
} else {
|
|
37
|
-
// Detect all agents installed on the system
|
|
38
45
|
const detected = await detectSystemAgents()
|
|
39
46
|
|
|
40
47
|
if (detected.length === 0) {
|
|
41
48
|
console.log(chalk.yellow(' ⚠ No AI agents detected on this system.'))
|
|
42
|
-
console.log(chalk.dim(` Use ${chalk.white('--agent <
|
|
49
|
+
console.log(chalk.dim(` Use ${chalk.white('--agent <n>')} to force an agent.`))
|
|
43
50
|
console.log(chalk.dim(` Valid: ${Object.keys(AGENTS).filter(k => k !== 'generic').join(', ')}\n`))
|
|
44
51
|
process.exit(1)
|
|
45
52
|
}
|
|
46
53
|
|
|
47
|
-
// 3. Show checkbox selection — always, even with 1 agent detected
|
|
48
|
-
// This is the core UX fix: user always chooses, never surprised
|
|
49
54
|
if (options.all) {
|
|
50
|
-
// --all flag skips the prompt
|
|
51
55
|
targetAgents = detected
|
|
52
56
|
} else {
|
|
53
|
-
|
|
57
|
+
// Always show checkbox — even with 1 agent, even on re-run
|
|
58
|
+
targetAgents = await checkboxSelect(detected, method, alreadyInstalled)
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
if (targetAgents.length === 0) {
|
|
@@ -61,66 +66,74 @@ export async function installCommand(methodId, options) {
|
|
|
61
66
|
|
|
62
67
|
console.log()
|
|
63
68
|
|
|
64
|
-
// 4. Install
|
|
69
|
+
// 4. Install each selected agent
|
|
70
|
+
const newlyInstalled = []
|
|
65
71
|
let schemaInstalled = false
|
|
72
|
+
|
|
66
73
|
for (const agent of targetAgents) {
|
|
67
|
-
await installForAgent(method, agent, options, schemaInstalled)
|
|
74
|
+
const ok = await installForAgent(method, agent, options, schemaInstalled)
|
|
68
75
|
schemaInstalled = true
|
|
76
|
+
if (ok) newlyInstalled.push(agent.key)
|
|
69
77
|
}
|
|
70
78
|
|
|
79
|
+
// 5. Persist updated install record
|
|
80
|
+
const updatedAgents = [...new Set([...alreadyInstalled, ...newlyInstalled])]
|
|
81
|
+
await writeInstallRecord(methodId, { agents: updatedAgents, version: method.version })
|
|
82
|
+
|
|
71
83
|
printHints(methodId)
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
// ─────────────────────────────────────────────────────────────────
|
|
75
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
|
|
76
92
|
// ─────────────────────────────────────────────────────────────────
|
|
77
93
|
|
|
78
|
-
async function checkboxSelect(detected, method) {
|
|
79
|
-
|
|
80
|
-
// then show unavailable ones as disabled so user knows what exists
|
|
81
|
-
const available = detected.filter(a => method.adapters[a.key] || method.adapters['generic'])
|
|
94
|
+
async function checkboxSelect(detected, method, alreadyInstalled = new Set()) {
|
|
95
|
+
const available = detected.filter(a => method.adapters[a.key] || method.adapters['generic'])
|
|
82
96
|
const unavailable = detected.filter(a => !method.adapters[a.key] && !method.adapters['generic'])
|
|
83
97
|
|
|
84
98
|
if (available.length === 0) {
|
|
85
99
|
console.log(chalk.yellow(' ⚠ No adapters available for your detected agents.'))
|
|
86
|
-
console.log(chalk.dim(`
|
|
100
|
+
console.log(chalk.dim(` Adapters in this method: ${Object.keys(method.adapters).join(', ')}\n`))
|
|
87
101
|
process.exit(1)
|
|
88
102
|
}
|
|
89
103
|
|
|
90
|
-
// Initial state: all available agents pre-checked
|
|
91
104
|
const items = available.map(a => ({
|
|
92
|
-
agent:
|
|
93
|
-
checked:
|
|
105
|
+
agent: a,
|
|
106
|
+
checked: true,
|
|
107
|
+
installed: alreadyInstalled.has(a.key),
|
|
94
108
|
usesGeneric: !method.adapters[a.key]
|
|
95
109
|
}))
|
|
96
110
|
|
|
97
|
-
console.log(chalk.white('
|
|
111
|
+
console.log(chalk.white(' Select adapters to install:\n'))
|
|
98
112
|
|
|
99
113
|
return new Promise((resolve) => {
|
|
100
114
|
let cursor = 0
|
|
101
115
|
|
|
116
|
+
const lineCount = () => items.length + (unavailable.length > 0 ? 3 : 2) + 2
|
|
117
|
+
|
|
102
118
|
const render = () => {
|
|
103
|
-
|
|
104
|
-
const lines = items.length + 6
|
|
105
|
-
if (render.drawn) process.stdout.write(`\x1B[${lines}A`)
|
|
119
|
+
if (render.drawn) process.stdout.write(`\x1B[${lineCount()}A`)
|
|
106
120
|
render.drawn = true
|
|
107
121
|
|
|
108
122
|
items.forEach((item, i) => {
|
|
109
123
|
const isCursor = i === cursor
|
|
110
|
-
const box
|
|
111
|
-
const arrow
|
|
112
|
-
const name
|
|
113
|
-
const
|
|
114
|
-
|
|
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`)
|
|
115
130
|
})
|
|
116
131
|
|
|
117
132
|
if (unavailable.length > 0) {
|
|
118
|
-
process.stdout.write(chalk.dim(`\n
|
|
119
|
-
} else {
|
|
120
|
-
process.stdout.write('\n')
|
|
133
|
+
process.stdout.write(chalk.dim(`\n No adapter for: ${unavailable.map(a => a.name).join(', ')}\n`))
|
|
121
134
|
}
|
|
122
|
-
|
|
123
|
-
process.stdout.write(chalk.dim(' ↑↓ navigate · Space toggle · A
|
|
135
|
+
process.stdout.write('\n')
|
|
136
|
+
process.stdout.write(chalk.dim(' ↑↓ navigate · Space toggle · A all · Enter confirm · Ctrl+C cancel\n\n'))
|
|
124
137
|
}
|
|
125
138
|
|
|
126
139
|
render()
|
|
@@ -131,28 +144,25 @@ async function checkboxSelect(detected, method) {
|
|
|
131
144
|
|
|
132
145
|
process.stdin.on('data', (key) => {
|
|
133
146
|
const k = key.toString()
|
|
134
|
-
|
|
135
|
-
if (k === '\u001b[
|
|
136
|
-
|
|
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 })
|
|
137
153
|
render()
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
render()
|
|
141
|
-
} else if (k === ' ') { // space: toggle current
|
|
142
|
-
items[cursor].checked = !items[cursor].checked
|
|
143
|
-
render()
|
|
144
|
-
} else if (k === 'a' || k === 'A') { // A: toggle all
|
|
145
|
-
const allChecked = items.every(i => i.checked)
|
|
146
|
-
items.forEach(i => { i.checked = !allChecked })
|
|
147
|
-
render()
|
|
148
|
-
} else if (k === '\r' || k === '\n') { // Enter: confirm
|
|
154
|
+
}
|
|
155
|
+
else if (k === '\r' || k === '\n') {
|
|
149
156
|
if (process.stdin.isTTY) process.stdin.setRawMode(false)
|
|
150
157
|
process.stdin.pause()
|
|
151
158
|
rl.close()
|
|
152
159
|
const selected = items.filter(i => i.checked).map(i => i.agent)
|
|
153
|
-
|
|
160
|
+
if (selected.length > 0) {
|
|
161
|
+
console.log(chalk.dim(`\n Installing for: ${selected.map(a => a.name).join(', ')}\n`))
|
|
162
|
+
}
|
|
154
163
|
resolve(selected)
|
|
155
|
-
}
|
|
164
|
+
}
|
|
165
|
+
else if (k === '\u0003') {
|
|
156
166
|
if (process.stdin.isTTY) process.stdin.setRawMode(false)
|
|
157
167
|
process.stdin.pause()
|
|
158
168
|
rl.close()
|
|
@@ -165,41 +175,44 @@ async function checkboxSelect(detected, method) {
|
|
|
165
175
|
|
|
166
176
|
// ─────────────────────────────────────────────────────────────────
|
|
167
177
|
// Install for one agent
|
|
178
|
+
// Exported so update.js can reuse it without duplication
|
|
179
|
+
// Returns true on success, false on failure/skip
|
|
168
180
|
// ─────────────────────────────────────────────────────────────────
|
|
169
181
|
|
|
170
|
-
async function installForAgent(method, agent, options, skipExtras = false) {
|
|
171
|
-
const isGlobal
|
|
172
|
-
|
|
173
|
-
// Use the agent-specific adapter if available, fall back to generic
|
|
174
|
-
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'
|
|
175
185
|
const adapterPath = method.adapters[adapterKey]
|
|
176
186
|
|
|
177
187
|
if (!adapterPath) {
|
|
178
188
|
console.log(chalk.yellow(` ⚠ ${agent.name} — no adapter found, skipping`))
|
|
179
|
-
return
|
|
189
|
+
return false
|
|
180
190
|
}
|
|
181
191
|
|
|
182
192
|
if (isGlobal && !agent.globalInstallDir) {
|
|
183
193
|
console.log(chalk.yellow(` ⚠ ${agent.name} — global install not supported, skipping`))
|
|
184
|
-
return
|
|
194
|
+
return false
|
|
185
195
|
}
|
|
186
196
|
|
|
187
|
-
const spinner = ora(
|
|
197
|
+
const spinner = ora(`${agent.name}${isGlobal ? ' (global)' : ''}...`).start()
|
|
188
198
|
|
|
189
199
|
try {
|
|
190
|
-
const content
|
|
200
|
+
const content = await fetchFromGitHub(method.repo, adapterPath)
|
|
191
201
|
const installPath = getInstallPath(agent, method.id, { global: isGlobal })
|
|
192
202
|
|
|
193
203
|
await fs.ensureDir(path.dirname(installPath))
|
|
194
204
|
|
|
195
|
-
// Windsurf: append to .windsurfrules
|
|
205
|
+
// Windsurf: append to .windsurfrules — replace existing section if already present
|
|
196
206
|
if (agent.key === 'windsurf' && await fs.pathExists(installPath)) {
|
|
197
207
|
const existing = await fs.readFile(installPath, 'utf8')
|
|
198
208
|
if (existing.includes(method.name)) {
|
|
199
|
-
|
|
200
|
-
|
|
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}`)
|
|
201
215
|
}
|
|
202
|
-
await fs.appendFile(installPath, `\n\n---\n\n${content}`)
|
|
203
216
|
} else {
|
|
204
217
|
await fs.writeFile(installPath, content)
|
|
205
218
|
}
|
|
@@ -210,24 +223,25 @@ async function installForAgent(method, agent, options, skipExtras = false) {
|
|
|
210
223
|
|
|
211
224
|
} catch (err) {
|
|
212
225
|
spinner.fail(chalk.red(`${agent.name} — ${err.message}`))
|
|
213
|
-
return
|
|
226
|
+
return false
|
|
214
227
|
}
|
|
215
228
|
|
|
216
|
-
//
|
|
229
|
+
// Extras (schema, manifests…) — downloaded once across all agents
|
|
217
230
|
if (!skipExtras && method.extras) {
|
|
218
231
|
for (const [key, filePath] of Object.entries(method.extras)) {
|
|
219
|
-
const
|
|
232
|
+
const s = ora(`Fetching ${key}...`).start()
|
|
220
233
|
try {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
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)}`))
|
|
225
237
|
} catch {
|
|
226
|
-
|
|
238
|
+
s.warn(chalk.dim(`${key} not available (non-critical)`))
|
|
227
239
|
}
|
|
228
240
|
}
|
|
229
241
|
console.log()
|
|
230
242
|
}
|
|
243
|
+
|
|
244
|
+
return true
|
|
231
245
|
}
|
|
232
246
|
|
|
233
247
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -241,7 +255,7 @@ function printHints(methodId) {
|
|
|
241
255
|
console.log(chalk.dim(' 2. Agent declares architecture — confirm it'))
|
|
242
256
|
console.log(chalk.dim(' 3. Agent builds Core → modules → Admin Panel'))
|
|
243
257
|
console.log()
|
|
244
|
-
console.log(chalk.dim(` ${chalk.white('enet new module <
|
|
258
|
+
console.log(chalk.dim(` ${chalk.white('enet new module <n>')} scaffold your first module`))
|
|
245
259
|
console.log(chalk.dim(` ${chalk.white('enet validate')} check manifests at any time\n`))
|
|
246
260
|
}
|
|
247
261
|
if (methodId === 'pdca-t') {
|
package/src/commands/update.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
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, fetchFromGitHub, 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([
|
|
13
|
+
getAllMethods(),
|
|
14
|
+
detectSystemAgents()
|
|
15
|
+
])
|
|
10
16
|
spinner.stop()
|
|
11
17
|
|
|
12
18
|
const targets = methodId
|
|
@@ -18,30 +24,191 @@ export async function updateCommand(methodId, options) {
|
|
|
18
24
|
process.exit(1)
|
|
19
25
|
}
|
|
20
26
|
|
|
21
|
-
console.log(chalk.
|
|
27
|
+
console.log(chalk.bold('\n ◆ enet update\n'))
|
|
22
28
|
|
|
23
|
-
let
|
|
29
|
+
let totalUpdated = 0, totalAdded = 0, totalSkipped = 0
|
|
24
30
|
|
|
25
31
|
for (const method of targets) {
|
|
26
|
-
const
|
|
27
|
-
if (!await fs.pathExists(installPath)) { skipped++; continue }
|
|
32
|
+
const record = await readInstallRecord(method.id)
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
34
|
+
// Skip methods not installed at all
|
|
35
|
+
if (!record || record.agents.length === 0) {
|
|
36
|
+
totalSkipped++
|
|
37
|
+
console.log(chalk.dim(` ${method.name} — not installed, skipping`))
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(chalk.white(` ${method.name}`))
|
|
42
|
+
console.log(chalk.dim(` Installed for: ${record.agents.join(', ')}\n`))
|
|
43
|
+
|
|
44
|
+
// Agents already installed for this method
|
|
45
|
+
const installedAgents = record.agents
|
|
46
|
+
.map(key => AGENTS[key] ? { key, ...AGENTS[key] } : null)
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
|
|
49
|
+
// Agents newly detected on system but NOT yet installed for this method
|
|
50
|
+
const newAgents = detectedAgents.filter(a =>
|
|
51
|
+
!record.agents.includes(a.key) &&
|
|
52
|
+
(method.adapters[a.key] || method.adapters['generic'])
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
// ── 1. Update already-installed adapters ────────────────────
|
|
56
|
+
if (!options.addOnly) {
|
|
57
|
+
for (const agent of installedAgents) {
|
|
58
|
+
const ok = await updateOneAdapter(method, agent, options)
|
|
59
|
+
if (ok) totalUpdated++
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── 2. Offer to add newly detected agents ───────────────────
|
|
64
|
+
if (newAgents.length > 0 && !options.updateOnly) {
|
|
65
|
+
console.log(chalk.yellow(`\n New agents detected since last install:\n`))
|
|
66
|
+
|
|
67
|
+
let agentsToAdd = []
|
|
68
|
+
if (options.all) {
|
|
69
|
+
agentsToAdd = newAgents
|
|
70
|
+
console.log(chalk.dim(` Adding all: ${newAgents.map(a => a.name).join(', ')}\n`))
|
|
71
|
+
} else {
|
|
72
|
+
agentsToAdd = await checkboxSelectNew(newAgents, method)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const agent of agentsToAdd) {
|
|
76
|
+
const ok = await installForAgent(method, agent, options, true)
|
|
77
|
+
if (ok) {
|
|
78
|
+
totalAdded++
|
|
79
|
+
record.agents.push(agent.key)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Save updated record with newly added agents
|
|
84
|
+
await writeInstallRecord(method.id, { agents: record.agents, version: method.version })
|
|
85
|
+
|
|
86
|
+
} else if (newAgents.length === 0 && !options.addOnly) {
|
|
87
|
+
console.log(chalk.dim(` No new agents detected.\n`))
|
|
38
88
|
}
|
|
39
89
|
}
|
|
40
90
|
|
|
91
|
+
// ── Summary ──────────────────────────────────────────────────
|
|
92
|
+
console.log(chalk.dim(' ─────────────────────────────'))
|
|
93
|
+
if (totalUpdated > 0) console.log(chalk.green(` ✓ ${totalUpdated} adapter${totalUpdated !== 1 ? 's' : ''} updated`))
|
|
94
|
+
if (totalAdded > 0) console.log(chalk.green(` ✓ ${totalAdded} new adapter${totalAdded !== 1 ? 's' : ''} installed`))
|
|
95
|
+
if (totalSkipped > 0) console.log(chalk.dim(` ${totalSkipped} method${totalSkipped !== 1 ? 's' : ''} not installed, skipped`))
|
|
96
|
+
if (totalUpdated === 0 && totalAdded === 0 && totalSkipped === 0) {
|
|
97
|
+
console.log(chalk.dim(' Everything is up to date.'))
|
|
98
|
+
}
|
|
41
99
|
console.log()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────
|
|
103
|
+
// Update a single already-installed adapter
|
|
104
|
+
// Re-downloads from GitHub and overwrites the local file
|
|
105
|
+
// ─────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
async function updateOneAdapter(method, agent, options = {}) {
|
|
108
|
+
const isGlobal = options.global || false
|
|
109
|
+
const adapterKey = method.adapters[agent.key] ? agent.key : 'generic'
|
|
110
|
+
const adapterPath = method.adapters[adapterKey]
|
|
111
|
+
|
|
112
|
+
if (!adapterPath) {
|
|
113
|
+
console.log(chalk.dim(` ${agent.name} — no adapter in registry, skipping`))
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const installPath = getInstallPath(agent, method.id, { global: isGlobal })
|
|
118
|
+
const exists = await fs.pathExists(installPath)
|
|
119
|
+
const action = exists ? 'Updating' : 'Restoring'
|
|
120
|
+
const s = ora(`${action} ${agent.name}...`).start()
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const content = await fetchFromGitHub(method.repo, adapterPath)
|
|
124
|
+
await fs.ensureDir(path.dirname(installPath))
|
|
125
|
+
|
|
126
|
+
// Windsurf: replace existing section, don't append duplicates
|
|
127
|
+
if (agent.key === 'windsurf' && exists) {
|
|
128
|
+
const existing = await fs.readFile(installPath, 'utf8')
|
|
129
|
+
if (existing.includes(method.name)) {
|
|
130
|
+
const marker = '\n\n---\n\n'
|
|
131
|
+
const idx = existing.indexOf(method.name)
|
|
132
|
+
const before = existing.substring(0, existing.lastIndexOf(marker, idx) + marker.length)
|
|
133
|
+
await fs.writeFile(installPath, before + content)
|
|
134
|
+
} else {
|
|
135
|
+
await fs.appendFile(installPath, `\n\n---\n\n${content}`)
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
await fs.writeFile(installPath, content)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
s.succeed(chalk.green(`${agent.name} — ${action.toLowerCase()}d`))
|
|
142
|
+
console.log(chalk.dim(` → ${installPath}\n`))
|
|
143
|
+
return true
|
|
144
|
+
|
|
145
|
+
} catch (err) {
|
|
146
|
+
s.fail(chalk.red(`${agent.name} — ${err.message}`))
|
|
147
|
+
return false
|
|
46
148
|
}
|
|
47
149
|
}
|
|
150
|
+
|
|
151
|
+
// ─────────────────────────────────────────────────────────────────
|
|
152
|
+
// Checkbox for new agents only (shown during update)
|
|
153
|
+
// ─────────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
async function checkboxSelectNew(newAgents, method) {
|
|
156
|
+
const items = newAgents.map(a => ({
|
|
157
|
+
agent: a,
|
|
158
|
+
checked: true,
|
|
159
|
+
usesGeneric: !method.adapters[a.key]
|
|
160
|
+
}))
|
|
161
|
+
|
|
162
|
+
return new Promise((resolve) => {
|
|
163
|
+
let cursor = 0
|
|
164
|
+
const lineCount = () => items.length + 4
|
|
165
|
+
|
|
166
|
+
const render = () => {
|
|
167
|
+
if (render.drawn) process.stdout.write(`\x1B[${lineCount()}A`)
|
|
168
|
+
render.drawn = true
|
|
169
|
+
|
|
170
|
+
items.forEach((item, i) => {
|
|
171
|
+
const isCursor = i === cursor
|
|
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`)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
process.stdout.write('\n')
|
|
180
|
+
process.stdout.write(chalk.dim(' ↑↓ navigate · Space toggle · A all · Enter confirm · Ctrl+C cancel\n\n'))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
render()
|
|
184
|
+
|
|
185
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
186
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true)
|
|
187
|
+
process.stdin.resume()
|
|
188
|
+
|
|
189
|
+
process.stdin.on('data', (key) => {
|
|
190
|
+
const k = key.toString()
|
|
191
|
+
if (k === '\u001b[A') { cursor = (cursor - 1 + items.length) % items.length; render() }
|
|
192
|
+
else if (k === '\u001b[B') { cursor = (cursor + 1) % items.length; render() }
|
|
193
|
+
else if (k === ' ') { items[cursor].checked = !items[cursor].checked; render() }
|
|
194
|
+
else if (k === 'a' || k === 'A') {
|
|
195
|
+
const all = items.every(i => i.checked)
|
|
196
|
+
items.forEach(i => { i.checked = !all })
|
|
197
|
+
render()
|
|
198
|
+
}
|
|
199
|
+
else if (k === '\r' || k === '\n') {
|
|
200
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false)
|
|
201
|
+
process.stdin.pause()
|
|
202
|
+
rl.close()
|
|
203
|
+
resolve(items.filter(i => i.checked).map(i => i.agent))
|
|
204
|
+
}
|
|
205
|
+
else if (k === '\u0003') {
|
|
206
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false)
|
|
207
|
+
process.stdin.pause()
|
|
208
|
+
rl.close()
|
|
209
|
+
console.log('\n')
|
|
210
|
+
process.exit(0)
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
}
|
|
@@ -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:
|
|
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
|
}
|
package/src/utils/registry.js
CHANGED
|
@@ -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
|
|
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)
|
|
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,19 +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
|
}
|
|
59
|
+
|
|
60
|
+
// ── Install state ─────────────────────────────────────────────────────────────
|
|
61
|
+
//
|
|
62
|
+
// Tracks which agents have each method installed.
|
|
63
|
+
// Stored in <project>/.enet/installed.json
|
|
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
|
+
// }
|
|
73
|
+
|
|
74
|
+
function getInstallRecordFile() {
|
|
75
|
+
return path.join(process.cwd(), '.enet', 'installed.json')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns the install record for a single method, or null if never installed.
|
|
80
|
+
*/
|
|
81
|
+
export async function readInstallRecord(methodId) {
|
|
82
|
+
try {
|
|
83
|
+
const file = getInstallRecordFile()
|
|
84
|
+
if (!await fs.pathExists(file)) return null
|
|
85
|
+
const data = await fs.readJson(file)
|
|
86
|
+
return data[methodId] ?? null
|
|
87
|
+
} catch {
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Writes the install record for a single method.
|
|
94
|
+
* Merges with existing records — other methods are never touched.
|
|
95
|
+
*/
|
|
96
|
+
export async function writeInstallRecord(methodId, record) {
|
|
97
|
+
try {
|
|
98
|
+
const file = getInstallRecordFile()
|
|
99
|
+
await fs.ensureDir(path.dirname(file))
|
|
100
|
+
let data = {}
|
|
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
|
+
}
|
|
109
|
+
await fs.writeJson(file, data, { spaces: 2 })
|
|
110
|
+
} catch {
|
|
111
|
+
// Non-fatal — install works correctly even if state cannot be saved
|
|
112
|
+
}
|
|
113
|
+
}
|