devvami 1.5.0 → 1.5.1
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/README.md +65 -0
- package/oclif.manifest.json +249 -249
- package/package.json +1 -1
- package/src/commands/sync-config-ai/index.js +124 -10
- package/src/formatters/ai-config.js +100 -12
- package/src/services/ai-config-store.js +43 -12
- package/src/services/ai-env-deployer.js +216 -10
- package/src/services/ai-env-scanner.js +752 -11
- package/src/types.js +35 -3
- package/src/utils/tui/form.js +195 -17
- package/src/utils/tui/tab-tui.js +353 -64
|
@@ -9,6 +9,7 @@ import {readFile, writeFile, mkdir, rm} from 'node:fs/promises'
|
|
|
9
9
|
import {existsSync} from 'node:fs'
|
|
10
10
|
import {join, dirname} from 'node:path'
|
|
11
11
|
import {homedir} from 'node:os'
|
|
12
|
+
import yaml from 'js-yaml'
|
|
12
13
|
|
|
13
14
|
/** @import { CategoryEntry, CategoryType, EnvironmentId, DetectedEnvironment } from '../types.js' */
|
|
14
15
|
|
|
@@ -20,7 +21,7 @@ import {homedir} from 'node:os'
|
|
|
20
21
|
* For each environment, the target JSON file path (relative to cwd or absolute)
|
|
21
22
|
* and the root key that holds the MCP server map.
|
|
22
23
|
*
|
|
23
|
-
* @type {Record<EnvironmentId, { resolvePath: (cwd: string) => string, mcpKey: string }>}
|
|
24
|
+
* @type {Record<EnvironmentId, { resolvePath: (cwd: string) => string, mcpKey: string, isYaml?: boolean }>}
|
|
24
25
|
*/
|
|
25
26
|
const MCP_TARGETS = {
|
|
26
27
|
'vscode-copilot': {
|
|
@@ -31,9 +32,13 @@ const MCP_TARGETS = {
|
|
|
31
32
|
resolvePath: (cwd) => join(cwd, '.mcp.json'),
|
|
32
33
|
mcpKey: 'mcpServers',
|
|
33
34
|
},
|
|
35
|
+
'claude-desktop': {
|
|
36
|
+
resolvePath: (_cwd) => join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
|
|
37
|
+
mcpKey: 'mcpServers',
|
|
38
|
+
},
|
|
34
39
|
opencode: {
|
|
35
40
|
resolvePath: (cwd) => join(cwd, 'opencode.json'),
|
|
36
|
-
mcpKey: '
|
|
41
|
+
mcpKey: 'mcp',
|
|
37
42
|
},
|
|
38
43
|
'gemini-cli': {
|
|
39
44
|
resolvePath: (_cwd) => join(homedir(), '.gemini', 'settings.json'),
|
|
@@ -43,6 +48,27 @@ const MCP_TARGETS = {
|
|
|
43
48
|
resolvePath: (_cwd) => join(homedir(), '.copilot', 'mcp-config.json'),
|
|
44
49
|
mcpKey: 'mcpServers',
|
|
45
50
|
},
|
|
51
|
+
cursor: {
|
|
52
|
+
resolvePath: (cwd) => join(cwd, '.cursor', 'mcp.json'),
|
|
53
|
+
mcpKey: 'mcpServers',
|
|
54
|
+
},
|
|
55
|
+
windsurf: {
|
|
56
|
+
resolvePath: (_cwd) => join(homedir(), '.codeium', 'windsurf', 'mcp_config.json'),
|
|
57
|
+
mcpKey: 'mcpServers',
|
|
58
|
+
},
|
|
59
|
+
'continue-dev': {
|
|
60
|
+
resolvePath: (_cwd) => join(homedir(), '.continue', 'config.yaml'),
|
|
61
|
+
mcpKey: 'mcpServers',
|
|
62
|
+
isYaml: true,
|
|
63
|
+
},
|
|
64
|
+
zed: {
|
|
65
|
+
resolvePath: (_cwd) => join(homedir(), '.config', 'zed', 'settings.json'),
|
|
66
|
+
mcpKey: 'context_servers',
|
|
67
|
+
},
|
|
68
|
+
'amazon-q': {
|
|
69
|
+
resolvePath: (cwd) => join(cwd, '.amazonq', 'mcp.json'),
|
|
70
|
+
mcpKey: 'mcpServers',
|
|
71
|
+
},
|
|
46
72
|
}
|
|
47
73
|
|
|
48
74
|
/**
|
|
@@ -62,6 +88,8 @@ function resolveFilePath(name, type, envId, cwd) {
|
|
|
62
88
|
return resolveSkillPath(name, envId, cwd)
|
|
63
89
|
case 'agent':
|
|
64
90
|
return resolveAgentPath(name, envId, cwd)
|
|
91
|
+
case 'rule':
|
|
92
|
+
return resolveRulePath(name, envId, cwd)
|
|
65
93
|
default:
|
|
66
94
|
throw new Error(`Unsupported file entry type: ${type}`)
|
|
67
95
|
}
|
|
@@ -86,6 +114,12 @@ function resolveCommandPath(name, envId, cwd) {
|
|
|
86
114
|
case 'copilot-cli':
|
|
87
115
|
// shared path with vscode-copilot for commands
|
|
88
116
|
return join(cwd, '.github', 'prompts', `${name}.prompt.md`)
|
|
117
|
+
case 'cursor':
|
|
118
|
+
return join(cwd, '.cursor', 'commands', `${name}.md`)
|
|
119
|
+
case 'windsurf':
|
|
120
|
+
return join(cwd, '.windsurf', 'workflows', `${name}.md`)
|
|
121
|
+
case 'continue-dev':
|
|
122
|
+
return join(cwd, '.continue', 'prompts', `${name}.md`)
|
|
89
123
|
default:
|
|
90
124
|
throw new Error(`Unknown environment for command: ${envId}`)
|
|
91
125
|
}
|
|
@@ -108,6 +142,8 @@ function resolveSkillPath(name, envId, cwd) {
|
|
|
108
142
|
return join(cwd, '.opencode', 'skills', `${name}.md`)
|
|
109
143
|
case 'copilot-cli':
|
|
110
144
|
return join(homedir(), '.copilot', 'skills', `${name}.md`)
|
|
145
|
+
case 'cursor':
|
|
146
|
+
return join(cwd, '.cursor', 'skills', `${name}.md`)
|
|
111
147
|
default:
|
|
112
148
|
throw new Error(`Environment "${envId}" does not support skill entries`)
|
|
113
149
|
}
|
|
@@ -129,11 +165,48 @@ function resolveAgentPath(name, envId, cwd) {
|
|
|
129
165
|
return join(cwd, '.opencode', 'agents', `${name}.md`)
|
|
130
166
|
case 'copilot-cli':
|
|
131
167
|
return join(homedir(), '.copilot', 'agents', `${name}.md`)
|
|
168
|
+
case 'continue-dev':
|
|
169
|
+
return join(cwd, '.continue', 'agents', `${name}.md`)
|
|
170
|
+
case 'amazon-q':
|
|
171
|
+
return join(homedir(), '.aws', 'amazonq', 'cli-agents', `${name}.json`)
|
|
132
172
|
default:
|
|
133
173
|
throw new Error(`Environment "${envId}" does not support agent entries`)
|
|
134
174
|
}
|
|
135
175
|
}
|
|
136
176
|
|
|
177
|
+
/**
|
|
178
|
+
* @param {string} name
|
|
179
|
+
* @param {EnvironmentId} envId
|
|
180
|
+
* @param {string} cwd
|
|
181
|
+
* @returns {string}
|
|
182
|
+
*/
|
|
183
|
+
function resolveRulePath(name, envId, cwd) {
|
|
184
|
+
switch (envId) {
|
|
185
|
+
case 'vscode-copilot':
|
|
186
|
+
return join(cwd, '.github', 'instructions', `${name}.md`)
|
|
187
|
+
case 'claude-code':
|
|
188
|
+
return join(cwd, '.claude', 'rules', `${name}.md`)
|
|
189
|
+
case 'opencode':
|
|
190
|
+
return join(cwd, 'AGENTS.md')
|
|
191
|
+
case 'gemini-cli':
|
|
192
|
+
return join(cwd, 'GEMINI.md')
|
|
193
|
+
case 'copilot-cli':
|
|
194
|
+
return join(cwd, '.github', 'copilot-instructions.md')
|
|
195
|
+
case 'cursor':
|
|
196
|
+
return join(cwd, '.cursor', 'rules', `${name}.mdc`)
|
|
197
|
+
case 'windsurf':
|
|
198
|
+
return join(cwd, '.windsurf', 'rules', `${name}.md`)
|
|
199
|
+
case 'continue-dev':
|
|
200
|
+
return join(cwd, '.continue', 'rules', `${name}.md`)
|
|
201
|
+
case 'zed':
|
|
202
|
+
return join(cwd, '.rules')
|
|
203
|
+
case 'amazon-q':
|
|
204
|
+
return join(cwd, '.amazonq', 'rules', `${name}.md`)
|
|
205
|
+
default:
|
|
206
|
+
throw new Error(`Environment "${envId}" does not support rule entries`)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
137
210
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
138
211
|
// TOML rendering
|
|
139
212
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -158,6 +231,23 @@ ${safeContent}
|
|
|
158
231
|
`
|
|
159
232
|
}
|
|
160
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Render a Cursor rule as MDC (Markdown with YAML frontmatter).
|
|
236
|
+
* @param {string} name - Rule name (used as identifier)
|
|
237
|
+
* @param {string} description - Short description
|
|
238
|
+
* @param {string} content - Rule content
|
|
239
|
+
* @returns {string}
|
|
240
|
+
*/
|
|
241
|
+
function renderCursorMDC(name, description, content) {
|
|
242
|
+
return `---
|
|
243
|
+
description: ${description || name}
|
|
244
|
+
globs:
|
|
245
|
+
alwaysApply: false
|
|
246
|
+
---
|
|
247
|
+
${content}
|
|
248
|
+
`
|
|
249
|
+
}
|
|
250
|
+
|
|
161
251
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
162
252
|
// JSON helpers
|
|
163
253
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -191,6 +281,32 @@ async function writeJson(filePath, data) {
|
|
|
191
281
|
await writeFile(filePath, JSON.stringify(data, null, 2), 'utf8')
|
|
192
282
|
}
|
|
193
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Read a YAML file from disk. Returns an empty object when the file is missing.
|
|
286
|
+
* Throws if the file exists but cannot be parsed.
|
|
287
|
+
* @param {string} filePath
|
|
288
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
289
|
+
*/
|
|
290
|
+
async function readYamlOrEmpty(filePath) {
|
|
291
|
+
if (!existsSync(filePath)) return {}
|
|
292
|
+
const raw = await readFile(filePath, 'utf8')
|
|
293
|
+
return /** @type {Record<string, unknown>} */ (yaml.load(raw) ?? {})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Write a value to disk as YAML, creating parent directories as needed.
|
|
298
|
+
* @param {string} filePath
|
|
299
|
+
* @param {unknown} data
|
|
300
|
+
* @returns {Promise<void>}
|
|
301
|
+
*/
|
|
302
|
+
async function writeYaml(filePath, data) {
|
|
303
|
+
const dir = dirname(filePath)
|
|
304
|
+
if (!existsSync(dir)) {
|
|
305
|
+
await mkdir(dir, {recursive: true})
|
|
306
|
+
}
|
|
307
|
+
await writeFile(filePath, yaml.dump(data, {lineWidth: -1}), 'utf8')
|
|
308
|
+
}
|
|
309
|
+
|
|
194
310
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
195
311
|
// Build MCP server object from entry params
|
|
196
312
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -207,10 +323,83 @@ function buildMCPServerObject(params) {
|
|
|
207
323
|
const server = {}
|
|
208
324
|
|
|
209
325
|
if (params.command !== undefined) server.command = params.command
|
|
210
|
-
|
|
211
|
-
|
|
326
|
+
// Normalize args: must be string[] in the deployed JSON.
|
|
327
|
+
// Guard against legacy data where args was stored as a newline-joined string.
|
|
328
|
+
if (params.args !== undefined) {
|
|
329
|
+
server.args = typeof params.args === 'string'
|
|
330
|
+
? params.args.split('\n').map((a) => a.trim()).filter(Boolean)
|
|
331
|
+
: params.args
|
|
332
|
+
}
|
|
333
|
+
// Normalize env: must be Record<string,string> in the deployed JSON.
|
|
334
|
+
// Guard against legacy data where env was stored as a KEY=VALUE string.
|
|
335
|
+
if (params.env !== undefined) {
|
|
336
|
+
if (typeof params.env === 'string') {
|
|
337
|
+
/** @type {Record<string, string>} */
|
|
338
|
+
const envObj = {}
|
|
339
|
+
for (const line of params.env.split('\n')) {
|
|
340
|
+
const t = line.trim()
|
|
341
|
+
if (!t) continue
|
|
342
|
+
const eq = t.indexOf('=')
|
|
343
|
+
if (eq > 0) envObj[t.slice(0, eq)] = t.slice(eq + 1)
|
|
344
|
+
}
|
|
345
|
+
if (Object.keys(envObj).length > 0) server.env = envObj
|
|
346
|
+
} else {
|
|
347
|
+
server.env = params.env
|
|
348
|
+
}
|
|
349
|
+
}
|
|
212
350
|
if (params.url !== undefined) server.url = params.url
|
|
213
|
-
|
|
351
|
+
// Omit type for stdio — most environments infer it from the presence of command.
|
|
352
|
+
// Only write type for sse/streamable-http where it's required.
|
|
353
|
+
if (params.transport && params.transport !== 'stdio') server.type = params.transport
|
|
354
|
+
|
|
355
|
+
return server
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Build an OpenCode-format MCP server object from dvmi's normalized params.
|
|
360
|
+
* OpenCode uses: command as array, `environment` instead of `env`,
|
|
361
|
+
* `type: "local"/"remote"` instead of transport strings, and `enabled` flag.
|
|
362
|
+
*
|
|
363
|
+
* @param {import('../types.js').MCPParams} params
|
|
364
|
+
* @returns {Record<string, unknown>}
|
|
365
|
+
*/
|
|
366
|
+
function buildOpenCodeMCPObject(params) {
|
|
367
|
+
/** @type {Record<string, unknown>} */
|
|
368
|
+
const server = {enabled: true}
|
|
369
|
+
|
|
370
|
+
const isRemote = params.transport === 'sse' || params.transport === 'streamable-http'
|
|
371
|
+
server.type = isRemote ? 'remote' : 'local'
|
|
372
|
+
|
|
373
|
+
if (isRemote) {
|
|
374
|
+
if (params.url !== undefined) server.url = params.url
|
|
375
|
+
} else {
|
|
376
|
+
const cmd = []
|
|
377
|
+
if (params.command !== undefined) cmd.push(params.command)
|
|
378
|
+
if (params.args !== undefined) {
|
|
379
|
+
const argsArr = typeof params.args === 'string'
|
|
380
|
+
? params.args.split('\n').map((a) => a.trim()).filter(Boolean)
|
|
381
|
+
: params.args
|
|
382
|
+
cmd.push(...argsArr)
|
|
383
|
+
}
|
|
384
|
+
if (cmd.length > 0) server.command = cmd
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Normalize env for OpenCode (uses "environment" key)
|
|
388
|
+
if (params.env !== undefined) {
|
|
389
|
+
if (typeof params.env === 'string') {
|
|
390
|
+
/** @type {Record<string, string>} */
|
|
391
|
+
const envObj = {}
|
|
392
|
+
for (const line of params.env.split('\n')) {
|
|
393
|
+
const t = line.trim()
|
|
394
|
+
if (!t) continue
|
|
395
|
+
const eq = t.indexOf('=')
|
|
396
|
+
if (eq > 0) envObj[t.slice(0, eq)] = t.slice(eq + 1)
|
|
397
|
+
}
|
|
398
|
+
if (Object.keys(envObj).length > 0) server.environment = envObj
|
|
399
|
+
} else {
|
|
400
|
+
server.environment = params.env
|
|
401
|
+
}
|
|
402
|
+
}
|
|
214
403
|
|
|
215
404
|
return server
|
|
216
405
|
}
|
|
@@ -241,7 +430,7 @@ export async function deployMCPEntry(entry, envId, cwd) {
|
|
|
241
430
|
if (!target) return
|
|
242
431
|
|
|
243
432
|
const filePath = target.resolvePath(cwd)
|
|
244
|
-
const json = await readJsonOrEmpty(filePath)
|
|
433
|
+
const json = target.isYaml ? await readYamlOrEmpty(filePath) : await readJsonOrEmpty(filePath)
|
|
245
434
|
|
|
246
435
|
if (!json[target.mcpKey] || typeof json[target.mcpKey] !== 'object') {
|
|
247
436
|
json[target.mcpKey] = {}
|
|
@@ -249,9 +438,14 @@ export async function deployMCPEntry(entry, envId, cwd) {
|
|
|
249
438
|
|
|
250
439
|
/** @type {Record<string, unknown>} */
|
|
251
440
|
const mcpKey = /** @type {any} */ (json[target.mcpKey])
|
|
252
|
-
|
|
441
|
+
const params = /** @type {import('../types.js').MCPParams} */ (entry.params)
|
|
442
|
+
mcpKey[entry.name] = envId === 'opencode' ? buildOpenCodeMCPObject(params) : buildMCPServerObject(params)
|
|
253
443
|
|
|
254
|
-
|
|
444
|
+
if (target.isYaml) {
|
|
445
|
+
await writeYaml(filePath, json)
|
|
446
|
+
} else {
|
|
447
|
+
await writeJson(filePath, json)
|
|
448
|
+
}
|
|
255
449
|
}
|
|
256
450
|
|
|
257
451
|
/**
|
|
@@ -272,13 +466,17 @@ export async function undeployMCPEntry(entryName, envId, cwd) {
|
|
|
272
466
|
const filePath = target.resolvePath(cwd)
|
|
273
467
|
if (!existsSync(filePath)) return
|
|
274
468
|
|
|
275
|
-
const json = await readJsonOrEmpty(filePath)
|
|
469
|
+
const json = target.isYaml ? await readYamlOrEmpty(filePath) : await readJsonOrEmpty(filePath)
|
|
276
470
|
|
|
277
471
|
if (json[target.mcpKey] && typeof json[target.mcpKey] === 'object') {
|
|
278
472
|
delete (/** @type {any} */ (json[target.mcpKey])[entryName])
|
|
279
473
|
}
|
|
280
474
|
|
|
281
|
-
|
|
475
|
+
if (target.isYaml) {
|
|
476
|
+
await writeYaml(filePath, json)
|
|
477
|
+
} else {
|
|
478
|
+
await writeJson(filePath, json)
|
|
479
|
+
}
|
|
282
480
|
}
|
|
283
481
|
|
|
284
482
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -317,6 +515,14 @@ export async function deployFileEntry(entry, envId, cwd) {
|
|
|
317
515
|
|
|
318
516
|
const params = /** @type {any} */ (entry.params)
|
|
319
517
|
|
|
518
|
+
// Cursor rules use MDC format (Markdown with YAML frontmatter)
|
|
519
|
+
if (envId === 'cursor' && entry.type === 'rule') {
|
|
520
|
+
const description = params.description ?? ''
|
|
521
|
+
const content = params.content ?? ''
|
|
522
|
+
await writeFile(filePath, renderCursorMDC(entry.name, description, content), 'utf8')
|
|
523
|
+
return
|
|
524
|
+
}
|
|
525
|
+
|
|
320
526
|
// Gemini CLI commands use TOML format
|
|
321
527
|
if (envId === 'gemini-cli' && entry.type === 'command') {
|
|
322
528
|
const description = params.description ?? ''
|