@exchanet/enet 1.0.10 → 1.0.12
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 -76
- package/src/commands/update.js +48 -57
- package/src/index.js +47 -4
- package/src/utils/{agent-detector.js → agent_detector.js} +50 -15
- package/src/utils/registry.js +43 -14
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
|
|
@@ -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.
|
|
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:
|
|
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 <
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(`
|
|
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:
|
|
98
|
-
checked:
|
|
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('
|
|
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
|
-
|
|
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
|
|
116
|
-
const arrow
|
|
117
|
-
const name
|
|
118
|
-
const
|
|
119
|
-
|
|
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
|
|
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
|
|
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[
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
}
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
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(
|
|
197
|
+
const spinner = ora(`${agent.name}${isGlobal ? ' (global)' : ''}...`).start()
|
|
193
198
|
|
|
194
199
|
try {
|
|
195
|
-
const content
|
|
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
|
|
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
|
-
|
|
205
|
-
|
|
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
|
-
//
|
|
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
|
|
232
|
+
const s = ora(`Fetching ${key}...`).start()
|
|
225
233
|
try {
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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 <
|
|
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') {
|
package/src/commands/update.js
CHANGED
|
@@ -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([
|
|
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
|
|
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) {
|
|
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
|
|
94
|
-
const adapterKey
|
|
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:
|
|
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
|
|
150
|
-
const arrow
|
|
151
|
-
const name
|
|
152
|
-
const
|
|
153
|
-
process.stdout.write(`${arrow}${box} ${name}${
|
|
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 })
|
|
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()
|
|
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()
|
|
182
|
-
|
|
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
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { program } from 'commander'
|
|
4
4
|
import chalk from 'chalk'
|
|
5
|
+
import { createRequire } from 'module'
|
|
5
6
|
import { installCommand } from './commands/install.js'
|
|
6
7
|
import { listCommand } from './commands/list.js'
|
|
7
8
|
import { initCommand } from './commands/init.js'
|
|
@@ -11,10 +12,48 @@ import { updateCommand } from './commands/update.js'
|
|
|
11
12
|
import { statusCommand } from './commands/status.js'
|
|
12
13
|
import { doctorCommand } from './commands/doctor.js'
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
// Read version from package.json — single source of truth
|
|
16
|
+
const require = createRequire(import.meta.url)
|
|
17
|
+
const pkg = require('../package.json')
|
|
18
|
+
const VERSION = pkg.version
|
|
19
|
+
|
|
20
|
+
// ── Version check ─────────────────────────────────────────────────────────────
|
|
21
|
+
// Runs in background — never blocks the command, never crashes if offline
|
|
22
|
+
|
|
23
|
+
async function checkForUpdate() {
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(
|
|
26
|
+
'https://registry.npmjs.org/@exchanet/enet/latest',
|
|
27
|
+
{ signal: AbortSignal.timeout(3000) }
|
|
28
|
+
)
|
|
29
|
+
if (!res.ok) return
|
|
30
|
+
const data = await res.json()
|
|
31
|
+
const latest = data.version
|
|
32
|
+
if (latest && latest !== VERSION) {
|
|
33
|
+
console.log(
|
|
34
|
+
chalk.yellow(' ⚠ Update available: ') +
|
|
35
|
+
chalk.dim(`v${VERSION}`) +
|
|
36
|
+
chalk.white(' → ') +
|
|
37
|
+
chalk.green(`v${latest}`) + '\n' +
|
|
38
|
+
chalk.dim(' Run ') +
|
|
39
|
+
chalk.white('npm install -g @exchanet/enet') +
|
|
40
|
+
chalk.dim(' to update.\n')
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// Offline or npm unreachable — silently skip
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Fire in background without await — command runs immediately
|
|
49
|
+
checkForUpdate()
|
|
50
|
+
|
|
51
|
+
// ── Header ────────────────────────────────────────────────────────────────────
|
|
15
52
|
|
|
16
53
|
console.log(chalk.cyan(`\n◆ enet v${VERSION} — exchanet methods manager\n`))
|
|
17
54
|
|
|
55
|
+
// ── Commands ──────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
18
57
|
program
|
|
19
58
|
.name('enet')
|
|
20
59
|
.description('Install, scaffold and manage exchanet AI coding methods')
|
|
@@ -23,8 +62,9 @@ program
|
|
|
23
62
|
program
|
|
24
63
|
.command('install <method>')
|
|
25
64
|
.description('Install a method into the current project')
|
|
26
|
-
.option('-a, --agent <agent>', 'Force agent: cursor | windsurf | copilot | generic')
|
|
65
|
+
.option('-a, --agent <agent>', 'Force agent: cursor | windsurf | antigravity | claudecode | copilot | generic')
|
|
27
66
|
.option('-g, --global', 'Install globally to home directory')
|
|
67
|
+
.option('--all', 'Install for all detected agents without prompting')
|
|
28
68
|
.action(installCommand)
|
|
29
69
|
|
|
30
70
|
program
|
|
@@ -58,8 +98,11 @@ program
|
|
|
58
98
|
|
|
59
99
|
program
|
|
60
100
|
.command('update [method]')
|
|
61
|
-
.description('Update installed methods
|
|
62
|
-
.option('--all', '
|
|
101
|
+
.description('Update installed methods and add adapters for new agents')
|
|
102
|
+
.option('--all', 'Add all new agents without prompting')
|
|
103
|
+
.option('--add-only', 'Only add new agents, skip re-downloading existing')
|
|
104
|
+
.option('--update-only', 'Only re-download existing, skip new agent prompt')
|
|
105
|
+
.option('-g, --global', 'Update global install')
|
|
63
106
|
.action(updateCommand)
|
|
64
107
|
|
|
65
108
|
program
|
|
@@ -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
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import fetch from 'node-fetch'
|
|
2
1
|
import fs from 'fs-extra'
|
|
3
2
|
import path from 'path'
|
|
4
3
|
import { fileURLToPath } from 'url'
|
|
@@ -18,12 +17,10 @@ const CACHE_TTL_MS = 1000 * 60 * 60 // 1 hour
|
|
|
18
17
|
* 3. Bundled registry.json in the package — last resort
|
|
19
18
|
*/
|
|
20
19
|
export async function loadRegistry() {
|
|
21
|
-
// Try remote first
|
|
22
20
|
try {
|
|
23
|
-
const res = await fetch(REGISTRY_URL, {
|
|
21
|
+
const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(5000) })
|
|
24
22
|
if (res.ok) {
|
|
25
23
|
const data = await res.json()
|
|
26
|
-
// Save to cache for offline fallback
|
|
27
24
|
await fs.writeJson(CACHE_FILE, { ...data, _cachedAt: Date.now() }).catch(() => {})
|
|
28
25
|
return data
|
|
29
26
|
}
|
|
@@ -31,12 +28,11 @@ export async function loadRegistry() {
|
|
|
31
28
|
// Network unavailable — fall through to cache
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
// Try local cache
|
|
35
31
|
try {
|
|
36
32
|
if (await fs.pathExists(CACHE_FILE)) {
|
|
37
33
|
const cached = await fs.readJson(CACHE_FILE)
|
|
38
34
|
const age = Date.now() - (cached._cachedAt || 0)
|
|
39
|
-
if (age < CACHE_TTL_MS * 24) {
|
|
35
|
+
if (age < CACHE_TTL_MS * 24) {
|
|
40
36
|
return cached
|
|
41
37
|
}
|
|
42
38
|
}
|
|
@@ -44,7 +40,6 @@ export async function loadRegistry() {
|
|
|
44
40
|
// Cache corrupted — fall through to bundled
|
|
45
41
|
}
|
|
46
42
|
|
|
47
|
-
// Fallback to bundled registry.json
|
|
48
43
|
const bundled = path.join(__dirname, '../../registry.json')
|
|
49
44
|
return fs.readJson(bundled)
|
|
50
45
|
}
|
|
@@ -85,29 +80,63 @@ export async function fetchFromGitHub(repo, filePath) {
|
|
|
85
80
|
}
|
|
86
81
|
|
|
87
82
|
// ── Install state ─────────────────────────────────────────────────────────────
|
|
88
|
-
//
|
|
89
|
-
//
|
|
83
|
+
//
|
|
84
|
+
// Tracks which agents have each method installed.
|
|
85
|
+
// Stored in .enet/installed.json in the project root.
|
|
86
|
+
//
|
|
87
|
+
// Format:
|
|
88
|
+
// {
|
|
89
|
+
// "pdca-t": {
|
|
90
|
+
// "agents": ["cursor", "claudecode", "antigravity"],
|
|
91
|
+
// "version": "3.0.0",
|
|
92
|
+
// "updatedAt": "2025-03-01T10:00:00.000Z"
|
|
93
|
+
// },
|
|
94
|
+
// "modular-design": { ... }
|
|
95
|
+
// }
|
|
90
96
|
|
|
91
97
|
function getInstallRecordFile() {
|
|
98
|
+
// Resolve relative to cwd so it works in any project
|
|
92
99
|
return path.join(process.cwd(), '.enet', 'installed.json')
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Reads the install record for a single method.
|
|
104
|
+
* Returns { agents: ['cursor', ...], version: '3.0.0', updatedAt: '...' }
|
|
105
|
+
* or null if the method has never been installed.
|
|
106
|
+
*/
|
|
95
107
|
export async function readInstallRecord(methodId) {
|
|
96
108
|
try {
|
|
97
109
|
const file = getInstallRecordFile()
|
|
98
110
|
if (!await fs.pathExists(file)) return null
|
|
99
111
|
const data = await fs.readJson(file)
|
|
100
112
|
return data[methodId] ?? null
|
|
101
|
-
} catch {
|
|
113
|
+
} catch {
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
102
116
|
}
|
|
103
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Writes (or updates) the install record for a single method.
|
|
120
|
+
* Merges with existing records — other methods are not touched.
|
|
121
|
+
*/
|
|
104
122
|
export async function writeInstallRecord(methodId, record) {
|
|
105
123
|
try {
|
|
106
124
|
const file = getInstallRecordFile()
|
|
107
125
|
await fs.ensureDir(path.dirname(file))
|
|
126
|
+
|
|
108
127
|
let data = {}
|
|
109
|
-
if (await fs.pathExists(file))
|
|
110
|
-
|
|
128
|
+
if (await fs.pathExists(file)) {
|
|
129
|
+
data = await fs.readJson(file).catch(() => ({}))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
data[methodId] = {
|
|
133
|
+
agents: record.agents,
|
|
134
|
+
version: record.version ?? null,
|
|
135
|
+
updatedAt: new Date().toISOString()
|
|
136
|
+
}
|
|
137
|
+
|
|
111
138
|
await fs.writeJson(file, data, { spaces: 2 })
|
|
112
|
-
} catch {
|
|
113
|
-
|
|
139
|
+
} catch {
|
|
140
|
+
// Non-fatal — install works correctly even if state cannot be saved
|
|
141
|
+
}
|
|
142
|
+
}
|