@exchanet/enet 1.0.16 → 1.0.18
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 +43 -75
- package/src/commands/update.js +41 -61
- package/src/utils/agent_detector.js +189 -189
package/package.json
CHANGED
package/src/commands/install.js
CHANGED
|
@@ -2,7 +2,8 @@ import chalk from 'chalk'
|
|
|
2
2
|
import ora from 'ora'
|
|
3
3
|
import fs from 'fs-extra'
|
|
4
4
|
import path from 'path'
|
|
5
|
-
import
|
|
5
|
+
import enquirer from 'enquirer'
|
|
6
|
+
const { MultiSelect } = enquirer
|
|
6
7
|
import { detectSystemAgents, getInstallPath, AGENTS } from '../utils/agent_detector.js'
|
|
7
8
|
import { getMethod, fetchFromGitHub, readInstallRecord, writeInstallRecord } from '../utils/registry.js'
|
|
8
9
|
|
|
@@ -19,7 +20,12 @@ export async function installCommand(methodId, options) {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
console.log(chalk.bold(`\n ◆ ${method.name}`))
|
|
22
|
-
console.log(chalk.dim(` ${method.description}
|
|
23
|
+
console.log(chalk.dim(` ${method.description}`))
|
|
24
|
+
if (options.global) {
|
|
25
|
+
console.log(chalk.cyan(' Destination: global (home) — available for all agents/projects\n'))
|
|
26
|
+
} else {
|
|
27
|
+
console.log(chalk.dim(` Destination: current project (use ${chalk.white('--global')} for all agents)\n`))
|
|
28
|
+
}
|
|
23
29
|
|
|
24
30
|
// 2. Read existing install record — know what is already installed
|
|
25
31
|
const record = await readInstallRecord(methodId)
|
|
@@ -55,7 +61,7 @@ export async function installCommand(methodId, options) {
|
|
|
55
61
|
targetAgents = detected
|
|
56
62
|
} else {
|
|
57
63
|
// Always show checkbox — even with 1 agent, even on re-run
|
|
58
|
-
targetAgents = await checkboxSelect(detected, method, alreadyInstalled)
|
|
64
|
+
targetAgents = await checkboxSelect(detected, method, alreadyInstalled, options)
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
if (targetAgents.length === 0) {
|
|
@@ -84,14 +90,10 @@ export async function installCommand(methodId, options) {
|
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
// ─────────────────────────────────────────────────────────────────
|
|
87
|
-
//
|
|
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
|
|
93
|
+
// Adapter selection via Enquirer (works on Windows/PowerShell)
|
|
92
94
|
// ─────────────────────────────────────────────────────────────────
|
|
93
95
|
|
|
94
|
-
async function checkboxSelect(detected, method, alreadyInstalled = new Set()) {
|
|
96
|
+
async function checkboxSelect(detected, method, alreadyInstalled = new Set(), options = {}) {
|
|
95
97
|
const available = detected.filter(a => method.adapters[a.key] || method.adapters['generic'])
|
|
96
98
|
const unavailable = detected.filter(a => !method.adapters[a.key] && !method.adapters['generic'])
|
|
97
99
|
|
|
@@ -101,76 +103,42 @@ async function checkboxSelect(detected, method, alreadyInstalled = new Set()) {
|
|
|
101
103
|
process.exit(1)
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
installed: alreadyInstalled.has(a.key),
|
|
108
|
-
usesGeneric: !method.adapters[a.key]
|
|
109
|
-
}))
|
|
110
|
-
|
|
111
|
-
console.log(chalk.white(' Select adapters to install:\n'))
|
|
112
|
-
|
|
113
|
-
return new Promise((resolve) => {
|
|
114
|
-
let cursor = 0
|
|
115
|
-
|
|
116
|
-
const lineCount = () => items.length + (unavailable.length > 0 ? 3 : 2) + 2
|
|
106
|
+
if (unavailable.length > 0) {
|
|
107
|
+
console.log(chalk.dim(` No adapter for: ${unavailable.map(a => a.name).join(', ')}\n`))
|
|
108
|
+
}
|
|
117
109
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
110
|
+
const installScope = options.global ? 'global (home)' : 'current project'
|
|
111
|
+
const prompt = new MultiSelect({
|
|
112
|
+
name: 'agents',
|
|
113
|
+
message: `Select adapters to install (destination: ${installScope})`,
|
|
114
|
+
choices: available.map(a => ({
|
|
115
|
+
name: a.key,
|
|
116
|
+
message: `${a.name}${alreadyInstalled.has(a.key) ? ' — installed' : ' — new'}${!method.adapters[a.key] ? ' (generic)' : ''}`,
|
|
117
|
+
value: a,
|
|
118
|
+
enabled: true
|
|
119
|
+
})),
|
|
120
|
+
result (names) {
|
|
121
|
+
return this.options.choices.filter(c => names.includes(c.name)).map(c => c.value)
|
|
122
|
+
}
|
|
123
|
+
})
|
|
121
124
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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`)
|
|
130
|
-
})
|
|
125
|
+
if (!process.stdin.isTTY) {
|
|
126
|
+
return available
|
|
127
|
+
}
|
|
131
128
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
process.stdout.write(chalk.dim(' ↑↓ navigate · Space toggle · A all · Enter confirm · Ctrl+C cancel\n\n'))
|
|
129
|
+
try {
|
|
130
|
+
const selected = await prompt.run()
|
|
131
|
+
if (selected && selected.length > 0) {
|
|
132
|
+
console.log(chalk.dim(`\n Installing for: ${selected.map(a => a.name).join(', ')}\n`))
|
|
137
133
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const k = key.toString()
|
|
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 })
|
|
153
|
-
render()
|
|
154
|
-
}
|
|
155
|
-
else if (k === '\r' || k === '\n') {
|
|
156
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false)
|
|
157
|
-
process.stdin.pause()
|
|
158
|
-
rl.close()
|
|
159
|
-
const selected = items.filter(i => i.checked).map(i => i.agent)
|
|
160
|
-
if (selected.length > 0) {
|
|
161
|
-
console.log(chalk.dim(`\n Installing for: ${selected.map(a => a.name).join(', ')}\n`))
|
|
162
|
-
}
|
|
163
|
-
resolve(selected)
|
|
164
|
-
}
|
|
165
|
-
else if (k === '\u0003') {
|
|
166
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false)
|
|
167
|
-
process.stdin.pause()
|
|
168
|
-
rl.close()
|
|
169
|
-
console.log('\n')
|
|
170
|
-
process.exit(0)
|
|
171
|
-
}
|
|
172
|
-
})
|
|
173
|
-
})
|
|
134
|
+
return selected || []
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (err.name === 'ENOTTY' || err.message?.includes('cancel')) {
|
|
137
|
+
console.log(chalk.dim('\n Cancelled.\n'))
|
|
138
|
+
process.exit(0)
|
|
139
|
+
}
|
|
140
|
+
throw err
|
|
141
|
+
}
|
|
174
142
|
}
|
|
175
143
|
|
|
176
144
|
// ─────────────────────────────────────────────────────────────────
|
package/src/commands/update.js
CHANGED
|
@@ -2,9 +2,10 @@ import chalk from 'chalk'
|
|
|
2
2
|
import ora from 'ora'
|
|
3
3
|
import fs from 'fs-extra'
|
|
4
4
|
import path from 'path'
|
|
5
|
-
import
|
|
5
|
+
import enquirer from 'enquirer'
|
|
6
|
+
const { MultiSelect } = enquirer
|
|
6
7
|
import { getAllMethods, getMethod, fetchFromGitHub, readInstallRecord, writeInstallRecord } from '../utils/registry.js'
|
|
7
|
-
import { detectSystemAgents, getInstallPath, AGENTS } from '../utils/agent_detector.js'
|
|
8
|
+
import { detectSystemAgents, detectAgent, getInstallPath, AGENTS } from '../utils/agent_detector.js'
|
|
8
9
|
import { installForAgent } from './install.js'
|
|
9
10
|
|
|
10
11
|
export async function updateCommand(methodId, options) {
|
|
@@ -27,11 +28,20 @@ export async function updateCommand(methodId, options) {
|
|
|
27
28
|
console.log(chalk.bold('\n ◆ enet update\n'))
|
|
28
29
|
|
|
29
30
|
let totalUpdated = 0, totalAdded = 0, totalSkipped = 0
|
|
31
|
+
const currentAgent = await detectAgent()
|
|
30
32
|
|
|
31
33
|
for (const method of targets) {
|
|
32
|
-
|
|
34
|
+
let record = await readInstallRecord(method.id)
|
|
35
|
+
|
|
36
|
+
// If no record but adapter file exists (e.g. list shows "installed"), treat as installed for current agent
|
|
37
|
+
if ((!record || record.agents.length === 0) && currentAgent) {
|
|
38
|
+
const pathForCurrent = getInstallPath(currentAgent, method.id)
|
|
39
|
+
if (await fs.pathExists(pathForCurrent)) {
|
|
40
|
+
record = { agents: [currentAgent.key], version: record?.version, updatedAt: record?.updatedAt }
|
|
41
|
+
await writeInstallRecord(method.id, { agents: record.agents, version: method.version })
|
|
42
|
+
}
|
|
43
|
+
}
|
|
33
44
|
|
|
34
|
-
// Skip methods not installed at all
|
|
35
45
|
if (!record || record.agents.length === 0) {
|
|
36
46
|
totalSkipped++
|
|
37
47
|
console.log(chalk.dim(` ${method.name} — not installed, skipping`))
|
|
@@ -149,66 +159,36 @@ async function updateOneAdapter(method, agent, options = {}) {
|
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
// ─────────────────────────────────────────────────────────────────
|
|
152
|
-
//
|
|
162
|
+
// New agents selection during update (Enquirer, Windows-safe)
|
|
153
163
|
// ─────────────────────────────────────────────────────────────────
|
|
154
164
|
|
|
155
165
|
async function checkboxSelectNew(newAgents, method) {
|
|
156
|
-
|
|
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
|
-
}
|
|
166
|
+
if (newAgents.length === 0) return []
|
|
182
167
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
})
|
|
168
|
+
if (!process.stdin.isTTY) {
|
|
169
|
+
return newAgents
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const prompt = new MultiSelect({
|
|
173
|
+
name: 'newAgents',
|
|
174
|
+
message: 'Add these agents for this method?',
|
|
175
|
+
choices: newAgents.map(a => ({
|
|
176
|
+
name: a.key,
|
|
177
|
+
message: `${a.name}${!method.adapters[a.key] ? ' (generic adapter)' : ''}`,
|
|
178
|
+
value: a,
|
|
179
|
+
enabled: true
|
|
180
|
+
})),
|
|
181
|
+
result (names) {
|
|
182
|
+
return this.options.choices.filter(c => names.includes(c.name)).map(c => c.value)
|
|
183
|
+
}
|
|
213
184
|
})
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
return await prompt.run() || []
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (err.name === 'ENOTTY' || err.message?.includes('cancel')) {
|
|
190
|
+
return []
|
|
191
|
+
}
|
|
192
|
+
throw err
|
|
193
|
+
}
|
|
214
194
|
}
|
|
@@ -1,189 +1,189 @@
|
|
|
1
|
-
import fs from 'fs-extra'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import os from 'os'
|
|
4
|
-
|
|
5
|
-
const HOME = os.homedir()
|
|
6
|
-
|
|
7
|
-
export const AGENTS = {
|
|
8
|
-
cursor: {
|
|
9
|
-
name: 'Cursor',
|
|
10
|
-
systemSignals: [
|
|
11
|
-
path.join(HOME, '.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
|
-
],
|
|
16
|
-
projectSignals: ['.cursor/rules', '.cursor'],
|
|
17
|
-
projectInstallDir: '.cursor/rules',
|
|
18
|
-
globalInstallDir: path.join(HOME, '.cursor', 'rules'),
|
|
19
|
-
filename: 'enet-{id}.md',
|
|
20
|
-
configNote: 'Rule auto-applies to all files (alwaysApply: true)'
|
|
21
|
-
},
|
|
22
|
-
|
|
23
|
-
windsurf: {
|
|
24
|
-
name: 'Windsurf',
|
|
25
|
-
systemSignals: [
|
|
26
|
-
path.join(HOME, '.codeium', 'windsurf'),
|
|
27
|
-
path.join(HOME, 'Library', 'Application Support', 'Windsurf'), // macOS
|
|
28
|
-
path.join(HOME, 'AppData', 'Roaming', 'Windsurf'), // Windows
|
|
29
|
-
],
|
|
30
|
-
projectSignals: ['.windsurfrules', '.windsurf'],
|
|
31
|
-
projectInstallDir: '.',
|
|
32
|
-
globalInstallDir: path.join(HOME, '.codeium', 'windsurf', 'memories'),
|
|
33
|
-
globalFilename: 'global_rules.md',
|
|
34
|
-
filename: '.windsurfrules',
|
|
35
|
-
configNote: 'Appended to .windsurfrules in project root'
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
antigravity: {
|
|
39
|
-
name: 'Antigravity (Google)',
|
|
40
|
-
systemSignals: [
|
|
41
|
-
path.join(HOME, '.gemini', 'antigravity'),
|
|
42
|
-
path.join(HOME, 'Library', 'Application Support', 'Google', 'Antigravity'), // macOS
|
|
43
|
-
path.join(HOME, 'AppData', 'Roaming', 'Google', 'Antigravity'), // Windows
|
|
44
|
-
],
|
|
45
|
-
projectSignals: ['.agent/rules', '.agent'],
|
|
46
|
-
projectInstallDir: '.agent/rules',
|
|
47
|
-
// globalInstallDir is a base — getInstallPath() resolves the full path dynamically:
|
|
48
|
-
// ~/.gemini/antigravity/skills/method-{id}/SKILL.md
|
|
49
|
-
globalInstallDir: path.join(HOME, '.gemini', 'antigravity', 'skills'),
|
|
50
|
-
globalFilename: 'SKILL.md',
|
|
51
|
-
filename: 'enet-{id}.md',
|
|
52
|
-
configNote: 'Rule placed in .agent/rules/ — activates automatically in Antigravity'
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
claudecode: {
|
|
56
|
-
name: 'Claude Code',
|
|
57
|
-
systemSignals: [
|
|
58
|
-
path.join(HOME, '.claude'),
|
|
59
|
-
],
|
|
60
|
-
projectSignals: ['CLAUDE.md', '.claude'],
|
|
61
|
-
projectInstallDir: '.',
|
|
62
|
-
globalInstallDir: path.join(HOME, '.claude'),
|
|
63
|
-
globalFilename: 'CLAUDE.md',
|
|
64
|
-
filename: 'CLAUDE.md',
|
|
65
|
-
configNote: 'Written to CLAUDE.md — Claude Code reads this automatically'
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
copilot: {
|
|
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/
|
|
72
|
-
systemSignals: [
|
|
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
|
|
77
|
-
],
|
|
78
|
-
systemSignalFilter: (signalPath) => {
|
|
79
|
-
// Only return true if a github.copilot extension folder exists inside
|
|
80
|
-
try {
|
|
81
|
-
const entries = fs.readdirSync(signalPath)
|
|
82
|
-
return entries.some(e => e.toLowerCase().startsWith('github.copilot'))
|
|
83
|
-
} catch {
|
|
84
|
-
return false
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
projectSignals: ['.github/copilot-instructions.md'],
|
|
88
|
-
projectInstallDir: '.github',
|
|
89
|
-
globalInstallDir: null, // Copilot has no global rules path
|
|
90
|
-
filename: 'copilot-instructions.md',
|
|
91
|
-
configNote: 'Written to .github/copilot-instructions.md'
|
|
92
|
-
},
|
|
93
|
-
|
|
94
|
-
generic: {
|
|
95
|
-
name: 'Generic / Other agent',
|
|
96
|
-
systemSignals: [],
|
|
97
|
-
projectSignals: [],
|
|
98
|
-
projectInstallDir: '.enet',
|
|
99
|
-
globalInstallDir: null,
|
|
100
|
-
filename: '{id}.md',
|
|
101
|
-
configNote: 'Saved to .enet/ — paste contents into your agent\'s context'
|
|
102
|
-
}
|
|
103
|
-
}
|
|
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
|
-
*/
|
|
114
|
-
export async function detectSystemAgents() {
|
|
115
|
-
const found = []
|
|
116
|
-
for (const [key, agent] of Object.entries(AGENTS)) {
|
|
117
|
-
if (key === 'generic') continue
|
|
118
|
-
for (const signal of agent.systemSignals) {
|
|
119
|
-
const exists = await fs.pathExists(signal)
|
|
120
|
-
if (!exists) continue
|
|
121
|
-
if (agent.systemSignalFilter) {
|
|
122
|
-
if (agent.systemSignalFilter(signal)) {
|
|
123
|
-
found.push({ key, ...agent })
|
|
124
|
-
break
|
|
125
|
-
}
|
|
126
|
-
continue // signal dir exists but filter didn't match — try next signal path
|
|
127
|
-
}
|
|
128
|
-
found.push({ key, ...agent })
|
|
129
|
-
break
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return found
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Detects ALL agents present in the current project folder.
|
|
137
|
-
*/
|
|
138
|
-
export async function detectProjectAgents(cwd = process.cwd()) {
|
|
139
|
-
const found = []
|
|
140
|
-
for (const [key, agent] of Object.entries(AGENTS)) {
|
|
141
|
-
if (key === 'generic') continue
|
|
142
|
-
for (const signal of agent.projectSignals) {
|
|
143
|
-
if (await fs.pathExists(path.join(cwd, signal))) {
|
|
144
|
-
found.push({ key, ...agent })
|
|
145
|
-
break
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return found
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Returns the first detected agent (legacy — used by status/doctor).
|
|
154
|
-
*/
|
|
155
|
-
export async function detectAgent(cwd = process.cwd()) {
|
|
156
|
-
const agents = await detectProjectAgents(cwd)
|
|
157
|
-
return agents[0] ?? { key: 'generic', ...AGENTS.generic }
|
|
158
|
-
}
|
|
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
|
-
*/
|
|
173
|
-
export function getInstallPath(agent, methodId, { global = false, cwd = process.cwd() } = {}) {
|
|
174
|
-
if (global) {
|
|
175
|
-
if (!agent.globalInstallDir) return null // agent doesn't support global install
|
|
176
|
-
|
|
177
|
-
if (agent.key === 'antigravity') {
|
|
178
|
-
// Dynamic subfolder per method: skills/method-{id}/SKILL.md
|
|
179
|
-
return path.join(agent.globalInstallDir, `method-${methodId}`, 'SKILL.md')
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const filename = agent.globalFilename ?? agent.filename.replace('{id}', methodId)
|
|
183
|
-
return path.join(agent.globalInstallDir, filename)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Project-level install
|
|
187
|
-
const filename = agent.filename.replace('{id}', methodId)
|
|
188
|
-
return path.join(cwd, agent.projectInstallDir, filename)
|
|
189
|
-
}
|
|
1
|
+
import fs from 'fs-extra'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
|
|
5
|
+
const HOME = os.homedir()
|
|
6
|
+
|
|
7
|
+
export const AGENTS = {
|
|
8
|
+
cursor: {
|
|
9
|
+
name: 'Cursor',
|
|
10
|
+
systemSignals: [
|
|
11
|
+
path.join(HOME, '.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
|
+
],
|
|
16
|
+
projectSignals: ['.cursor/rules', '.cursor'],
|
|
17
|
+
projectInstallDir: '.cursor/rules',
|
|
18
|
+
globalInstallDir: path.join(HOME, '.cursor', 'rules'),
|
|
19
|
+
filename: 'enet-{id}.md',
|
|
20
|
+
configNote: 'Rule auto-applies to all files (alwaysApply: true)'
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
windsurf: {
|
|
24
|
+
name: 'Windsurf',
|
|
25
|
+
systemSignals: [
|
|
26
|
+
path.join(HOME, '.codeium', 'windsurf'),
|
|
27
|
+
path.join(HOME, 'Library', 'Application Support', 'Windsurf'), // macOS
|
|
28
|
+
path.join(HOME, 'AppData', 'Roaming', 'Windsurf'), // Windows
|
|
29
|
+
],
|
|
30
|
+
projectSignals: ['.windsurfrules', '.windsurf'],
|
|
31
|
+
projectInstallDir: '.',
|
|
32
|
+
globalInstallDir: path.join(HOME, '.codeium', 'windsurf', 'memories'),
|
|
33
|
+
globalFilename: 'global_rules.md',
|
|
34
|
+
filename: '.windsurfrules',
|
|
35
|
+
configNote: 'Appended to .windsurfrules in project root'
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
antigravity: {
|
|
39
|
+
name: 'Antigravity (Google)',
|
|
40
|
+
systemSignals: [
|
|
41
|
+
path.join(HOME, '.gemini', 'antigravity'),
|
|
42
|
+
path.join(HOME, 'Library', 'Application Support', 'Google', 'Antigravity'), // macOS
|
|
43
|
+
path.join(HOME, 'AppData', 'Roaming', 'Google', 'Antigravity'), // Windows
|
|
44
|
+
],
|
|
45
|
+
projectSignals: ['.agent/rules', '.agent'],
|
|
46
|
+
projectInstallDir: '.agent/rules',
|
|
47
|
+
// globalInstallDir is a base — getInstallPath() resolves the full path dynamically:
|
|
48
|
+
// ~/.gemini/antigravity/skills/method-{id}/SKILL.md
|
|
49
|
+
globalInstallDir: path.join(HOME, '.gemini', 'antigravity', 'skills'),
|
|
50
|
+
globalFilename: 'SKILL.md',
|
|
51
|
+
filename: 'enet-{id}.md',
|
|
52
|
+
configNote: 'Rule placed in .agent/rules/ — activates automatically in Antigravity'
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
claudecode: {
|
|
56
|
+
name: 'Claude Code',
|
|
57
|
+
systemSignals: [
|
|
58
|
+
path.join(HOME, '.claude'),
|
|
59
|
+
],
|
|
60
|
+
projectSignals: ['CLAUDE.md', '.claude'],
|
|
61
|
+
projectInstallDir: '.',
|
|
62
|
+
globalInstallDir: path.join(HOME, '.claude'),
|
|
63
|
+
globalFilename: 'CLAUDE.md',
|
|
64
|
+
filename: 'CLAUDE.md',
|
|
65
|
+
configNote: 'Written to CLAUDE.md — Claude Code reads this automatically'
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
copilot: {
|
|
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/
|
|
72
|
+
systemSignals: [
|
|
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
|
|
77
|
+
],
|
|
78
|
+
systemSignalFilter: (signalPath) => {
|
|
79
|
+
// Only return true if a github.copilot extension folder exists inside
|
|
80
|
+
try {
|
|
81
|
+
const entries = fs.readdirSync(signalPath)
|
|
82
|
+
return entries.some(e => e.toLowerCase().startsWith('github.copilot'))
|
|
83
|
+
} catch {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
projectSignals: ['.github/copilot-instructions.md'],
|
|
88
|
+
projectInstallDir: '.github',
|
|
89
|
+
globalInstallDir: null, // Copilot has no global rules path
|
|
90
|
+
filename: 'copilot-instructions.md',
|
|
91
|
+
configNote: 'Written to .github/copilot-instructions.md'
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
generic: {
|
|
95
|
+
name: 'Generic / Other agent',
|
|
96
|
+
systemSignals: [],
|
|
97
|
+
projectSignals: [],
|
|
98
|
+
projectInstallDir: '.enet',
|
|
99
|
+
globalInstallDir: null,
|
|
100
|
+
filename: '{id}.md',
|
|
101
|
+
configNote: 'Saved to .enet/ — paste contents into your agent\'s context'
|
|
102
|
+
}
|
|
103
|
+
}
|
|
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
|
+
*/
|
|
114
|
+
export async function detectSystemAgents() {
|
|
115
|
+
const found = []
|
|
116
|
+
for (const [key, agent] of Object.entries(AGENTS)) {
|
|
117
|
+
if (key === 'generic') continue
|
|
118
|
+
for (const signal of agent.systemSignals) {
|
|
119
|
+
const exists = await fs.pathExists(signal)
|
|
120
|
+
if (!exists) continue
|
|
121
|
+
if (agent.systemSignalFilter) {
|
|
122
|
+
if (agent.systemSignalFilter(signal)) {
|
|
123
|
+
found.push({ key, ...agent })
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
continue // signal dir exists but filter didn't match — try next signal path
|
|
127
|
+
}
|
|
128
|
+
found.push({ key, ...agent })
|
|
129
|
+
break
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return found
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Detects ALL agents present in the current project folder.
|
|
137
|
+
*/
|
|
138
|
+
export async function detectProjectAgents(cwd = process.cwd()) {
|
|
139
|
+
const found = []
|
|
140
|
+
for (const [key, agent] of Object.entries(AGENTS)) {
|
|
141
|
+
if (key === 'generic') continue
|
|
142
|
+
for (const signal of agent.projectSignals) {
|
|
143
|
+
if (await fs.pathExists(path.join(cwd, signal))) {
|
|
144
|
+
found.push({ key, ...agent })
|
|
145
|
+
break
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return found
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Returns the first detected agent (legacy — used by status/doctor).
|
|
154
|
+
*/
|
|
155
|
+
export async function detectAgent(cwd = process.cwd()) {
|
|
156
|
+
const agents = await detectProjectAgents(cwd)
|
|
157
|
+
return agents[0] ?? { key: 'generic', ...AGENTS.generic }
|
|
158
|
+
}
|
|
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
|
+
*/
|
|
173
|
+
export function getInstallPath(agent, methodId, { global = false, cwd = process.cwd() } = {}) {
|
|
174
|
+
if (global) {
|
|
175
|
+
if (!agent.globalInstallDir) return null // agent doesn't support global install
|
|
176
|
+
|
|
177
|
+
if (agent.key === 'antigravity') {
|
|
178
|
+
// Dynamic subfolder per method: skills/method-{id}/SKILL.md
|
|
179
|
+
return path.join(agent.globalInstallDir, `method-${methodId}`, 'SKILL.md')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const filename = agent.globalFilename ?? agent.filename.replace('{id}', methodId)
|
|
183
|
+
return path.join(agent.globalInstallDir, filename)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Project-level install
|
|
187
|
+
const filename = agent.filename.replace('{id}', methodId)
|
|
188
|
+
return path.join(cwd, agent.projectInstallDir, filename)
|
|
189
|
+
}
|