devvami 1.4.2 → 1.5.0
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 +7 -0
- package/oclif.manifest.json +129 -89
- package/package.json +2 -1
- package/src/commands/auth/login.js +20 -16
- package/src/commands/changelog.js +12 -12
- package/src/commands/costs/get.js +14 -24
- package/src/commands/costs/trend.js +13 -24
- package/src/commands/create/repo.js +72 -54
- package/src/commands/docs/list.js +29 -25
- package/src/commands/docs/projects.js +58 -24
- package/src/commands/docs/read.js +56 -39
- package/src/commands/docs/search.js +37 -25
- package/src/commands/doctor.js +37 -35
- package/src/commands/dotfiles/add.js +51 -39
- package/src/commands/dotfiles/setup.js +62 -33
- package/src/commands/dotfiles/status.js +18 -18
- package/src/commands/dotfiles/sync.js +62 -46
- package/src/commands/init.js +143 -132
- package/src/commands/logs/index.js +10 -16
- package/src/commands/open.js +12 -12
- package/src/commands/pipeline/logs.js +8 -11
- package/src/commands/pipeline/rerun.js +21 -16
- package/src/commands/pipeline/status.js +28 -24
- package/src/commands/pr/create.js +40 -27
- package/src/commands/pr/detail.js +9 -7
- package/src/commands/pr/review.js +18 -19
- package/src/commands/pr/status.js +27 -21
- package/src/commands/prompts/browse.js +15 -15
- package/src/commands/prompts/download.js +15 -16
- package/src/commands/prompts/install-speckit.js +11 -12
- package/src/commands/prompts/list.js +12 -12
- package/src/commands/prompts/run.js +16 -19
- package/src/commands/repo/list.js +57 -41
- package/src/commands/search.js +20 -18
- package/src/commands/security/setup.js +38 -34
- package/src/commands/sync-config-ai/index.js +143 -0
- package/src/commands/tasks/assigned.js +43 -33
- package/src/commands/tasks/list.js +43 -33
- package/src/commands/tasks/today.js +32 -30
- package/src/commands/upgrade.js +18 -17
- package/src/commands/vuln/detail.js +8 -8
- package/src/commands/vuln/scan.js +39 -20
- package/src/commands/vuln/search.js +23 -18
- package/src/commands/welcome.js +2 -2
- package/src/commands/whoami.js +19 -23
- package/src/formatters/ai-config.js +127 -0
- package/src/formatters/charts.js +6 -23
- package/src/formatters/cost.js +1 -7
- package/src/formatters/dotfiles.js +48 -19
- package/src/formatters/markdown.js +11 -6
- package/src/formatters/openapi.js +7 -9
- package/src/formatters/prompts.js +69 -78
- package/src/formatters/security.js +2 -2
- package/src/formatters/status.js +1 -1
- package/src/formatters/table.js +1 -3
- package/src/formatters/vuln.js +33 -20
- package/src/help.js +162 -164
- package/src/hooks/init.js +1 -3
- package/src/hooks/postrun.js +5 -7
- package/src/index.js +1 -1
- package/src/services/ai-config-store.js +318 -0
- package/src/services/ai-env-deployer.js +444 -0
- package/src/services/ai-env-scanner.js +242 -0
- package/src/services/audit-detector.js +2 -2
- package/src/services/audit-runner.js +40 -31
- package/src/services/auth.js +9 -9
- package/src/services/awesome-copilot.js +7 -4
- package/src/services/aws-costs.js +22 -22
- package/src/services/clickup.js +26 -26
- package/src/services/cloudwatch-logs.js +5 -9
- package/src/services/config.js +13 -13
- package/src/services/docs.js +19 -20
- package/src/services/dotfiles.js +149 -51
- package/src/services/github.js +22 -24
- package/src/services/nvd.js +21 -31
- package/src/services/platform.js +2 -2
- package/src/services/prompts.js +23 -35
- package/src/services/security.js +135 -61
- package/src/services/shell.js +4 -4
- package/src/services/skills-sh.js +3 -9
- package/src/services/speckit.js +4 -7
- package/src/services/version-check.js +10 -10
- package/src/types.js +85 -0
- package/src/utils/aws-vault.js +18 -41
- package/src/utils/banner.js +5 -7
- package/src/utils/errors.js +42 -46
- package/src/utils/frontmatter.js +4 -4
- package/src/utils/gradient.js +18 -16
- package/src/utils/open-browser.js +3 -3
- package/src/utils/tui/form.js +1006 -0
- package/src/utils/tui/modal.js +15 -14
- package/src/utils/tui/navigable-table.js +16 -16
- package/src/utils/tui/tab-tui.js +800 -0
- package/src/utils/typewriter.js +3 -3
- package/src/utils/welcome.js +18 -21
- package/src/validators/repo-name.js +2 -2
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import {readFile, writeFile, mkdir, chmod} from 'node:fs/promises'
|
|
2
|
+
import {existsSync} from 'node:fs'
|
|
3
|
+
import {join, dirname} from 'node:path'
|
|
4
|
+
import {homedir} from 'node:os'
|
|
5
|
+
import {randomUUID} from 'node:crypto'
|
|
6
|
+
|
|
7
|
+
import {DvmiError} from '../utils/errors.js'
|
|
8
|
+
import {exec} from './shell.js'
|
|
9
|
+
import {loadConfig} from './config.js'
|
|
10
|
+
|
|
11
|
+
/** @import { AIConfigStore, CategoryEntry, CategoryType, EnvironmentId, MCPParams, CommandParams, SkillParams, AgentParams } from '../types.js' */
|
|
12
|
+
|
|
13
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
// Path resolution
|
|
15
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const CONFIG_DIR = process.env.XDG_CONFIG_HOME
|
|
18
|
+
? join(process.env.XDG_CONFIG_HOME, 'dvmi')
|
|
19
|
+
: join(homedir(), '.config', 'dvmi')
|
|
20
|
+
|
|
21
|
+
export const AI_CONFIG_PATH = join(CONFIG_DIR, 'ai-config.json')
|
|
22
|
+
|
|
23
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
// Compatibility matrix
|
|
25
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/** @type {Record<EnvironmentId, CategoryType[]>} */
|
|
28
|
+
const COMPATIBILITY = {
|
|
29
|
+
'vscode-copilot': ['mcp', 'command', 'skill', 'agent'],
|
|
30
|
+
'claude-code': ['mcp', 'command', 'skill', 'agent'],
|
|
31
|
+
opencode: ['mcp', 'command', 'skill', 'agent'],
|
|
32
|
+
'gemini-cli': ['mcp', 'command'],
|
|
33
|
+
'copilot-cli': ['mcp', 'command', 'skill', 'agent'],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** All known environment IDs. */
|
|
37
|
+
const KNOWN_ENVIRONMENTS = /** @type {EnvironmentId[]} */ (Object.keys(COMPATIBILITY))
|
|
38
|
+
|
|
39
|
+
/** Regex for filename-unsafe characters. */
|
|
40
|
+
const UNSAFE_CHARS = /[/\\:*?"<>|]/
|
|
41
|
+
|
|
42
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
// Default store
|
|
44
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/** @returns {AIConfigStore} */
|
|
47
|
+
function defaultStore() {
|
|
48
|
+
return {version: 1, entries: []}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
// Validation helpers
|
|
53
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Assert that a name is non-empty and contains no filename-unsafe characters.
|
|
57
|
+
* @param {string} name
|
|
58
|
+
* @returns {void}
|
|
59
|
+
*/
|
|
60
|
+
function validateName(name) {
|
|
61
|
+
if (!name || typeof name !== 'string' || name.trim() === '') {
|
|
62
|
+
throw new DvmiError(
|
|
63
|
+
'Entry name must be a non-empty string',
|
|
64
|
+
'Provide a valid name for the entry, e.g. "my-mcp-server"',
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
if (UNSAFE_CHARS.test(name)) {
|
|
68
|
+
throw new DvmiError(
|
|
69
|
+
`Entry name "${name}" contains invalid characters`,
|
|
70
|
+
'Remove characters like / \\ : * ? " < > | from the name',
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Assert that all environment IDs are compatible with the given entry type.
|
|
77
|
+
* @param {EnvironmentId[]} environments
|
|
78
|
+
* @param {CategoryType} type
|
|
79
|
+
* @returns {void}
|
|
80
|
+
*/
|
|
81
|
+
function validateEnvironments(environments, type) {
|
|
82
|
+
for (const envId of environments) {
|
|
83
|
+
const supported = COMPATIBILITY[envId]
|
|
84
|
+
if (!supported) {
|
|
85
|
+
throw new DvmiError(`Unknown environment "${envId}"`, `Valid environments are: ${KNOWN_ENVIRONMENTS.join(', ')}`)
|
|
86
|
+
}
|
|
87
|
+
if (!supported.includes(type)) {
|
|
88
|
+
throw new DvmiError(
|
|
89
|
+
`Environment "${envId}" does not support type "${type}"`,
|
|
90
|
+
`"${envId}" supports: ${supported.join(', ')}`,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
97
|
+
// Core I/O
|
|
98
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Load the AI config store from disk.
|
|
102
|
+
* Returns `{ version: 1, entries: [] }` if the file is missing or unparseable.
|
|
103
|
+
* @param {string} [configPath] - Override config path (used in tests; falls back to DVMI_AI_CONFIG_PATH or AI_CONFIG_PATH)
|
|
104
|
+
* @returns {Promise<AIConfigStore>}
|
|
105
|
+
*/
|
|
106
|
+
export async function loadAIConfig(configPath = process.env.DVMI_AI_CONFIG_PATH ?? AI_CONFIG_PATH) {
|
|
107
|
+
if (!existsSync(configPath)) return defaultStore()
|
|
108
|
+
try {
|
|
109
|
+
const raw = await readFile(configPath, 'utf8')
|
|
110
|
+
const parsed = JSON.parse(raw)
|
|
111
|
+
return {
|
|
112
|
+
version: parsed.version ?? 1,
|
|
113
|
+
entries: Array.isArray(parsed.entries) ? parsed.entries : [],
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
return defaultStore()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Persist the AI config store to disk.
|
|
122
|
+
* Creates the parent directory if it does not exist and sets file permissions to 0o600.
|
|
123
|
+
* @param {AIConfigStore} store
|
|
124
|
+
* @param {string} [configPath] - Override config path (used in tests)
|
|
125
|
+
* @returns {Promise<void>}
|
|
126
|
+
*/
|
|
127
|
+
export async function saveAIConfig(store, configPath = process.env.DVMI_AI_CONFIG_PATH ?? AI_CONFIG_PATH) {
|
|
128
|
+
const dir = dirname(configPath)
|
|
129
|
+
if (!existsSync(dir)) {
|
|
130
|
+
await mkdir(dir, {recursive: true})
|
|
131
|
+
}
|
|
132
|
+
await writeFile(configPath, JSON.stringify(store, null, 2), 'utf8')
|
|
133
|
+
await chmod(configPath, 0o600)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
137
|
+
// CRUD operations
|
|
138
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Add a new entry to the AI config store.
|
|
142
|
+
* @param {{ name: string, type: CategoryType, environments: EnvironmentId[], params: MCPParams|CommandParams|SkillParams|AgentParams }} entryData
|
|
143
|
+
* @param {string} [configPath]
|
|
144
|
+
* @returns {Promise<CategoryEntry>}
|
|
145
|
+
*/
|
|
146
|
+
export async function addEntry(entryData, configPath = process.env.DVMI_AI_CONFIG_PATH ?? AI_CONFIG_PATH) {
|
|
147
|
+
const {name, type, environments, params} = entryData
|
|
148
|
+
|
|
149
|
+
validateName(name)
|
|
150
|
+
validateEnvironments(environments, type)
|
|
151
|
+
|
|
152
|
+
const store = await loadAIConfig(configPath)
|
|
153
|
+
|
|
154
|
+
const duplicate = store.entries.find((e) => e.name === name && e.type === type)
|
|
155
|
+
if (duplicate) {
|
|
156
|
+
throw new DvmiError(
|
|
157
|
+
`An entry named "${name}" of type "${type}" already exists`,
|
|
158
|
+
'Use a unique name or update the existing entry with `dvmi sync-config-ai update`',
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const now = new Date().toISOString()
|
|
163
|
+
/** @type {CategoryEntry} */
|
|
164
|
+
const entry = {
|
|
165
|
+
id: randomUUID(),
|
|
166
|
+
name,
|
|
167
|
+
type,
|
|
168
|
+
active: true,
|
|
169
|
+
environments,
|
|
170
|
+
params,
|
|
171
|
+
createdAt: now,
|
|
172
|
+
updatedAt: now,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
store.entries.push(entry)
|
|
176
|
+
await saveAIConfig(store, configPath)
|
|
177
|
+
await syncAIConfigToChezmoi()
|
|
178
|
+
return entry
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Update an existing entry by id.
|
|
183
|
+
* @param {string} id - UUID of the entry to update
|
|
184
|
+
* @param {{ name?: string, environments?: EnvironmentId[], params?: MCPParams|CommandParams|SkillParams|AgentParams, active?: boolean }} changes
|
|
185
|
+
* @param {string} [configPath]
|
|
186
|
+
* @returns {Promise<CategoryEntry>}
|
|
187
|
+
*/
|
|
188
|
+
export async function updateEntry(id, changes, configPath = process.env.DVMI_AI_CONFIG_PATH ?? AI_CONFIG_PATH) {
|
|
189
|
+
const store = await loadAIConfig(configPath)
|
|
190
|
+
|
|
191
|
+
const index = store.entries.findIndex((e) => e.id === id)
|
|
192
|
+
if (index === -1) {
|
|
193
|
+
throw new DvmiError(
|
|
194
|
+
`Entry with id "${id}" not found`,
|
|
195
|
+
'Run `dvmi sync-config-ai list` to see available entries and their IDs',
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const existing = store.entries[index]
|
|
200
|
+
|
|
201
|
+
if (changes.name !== undefined) {
|
|
202
|
+
validateName(changes.name)
|
|
203
|
+
if (changes.name !== existing.name) {
|
|
204
|
+
const duplicate = store.entries.find((e) => e.id !== id && e.name === changes.name && e.type === existing.type)
|
|
205
|
+
if (duplicate) {
|
|
206
|
+
throw new DvmiError(
|
|
207
|
+
`An entry named "${changes.name}" of type "${existing.type}" already exists`,
|
|
208
|
+
'Choose a different name or update the conflicting entry',
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const newEnvironments = changes.environments ?? existing.environments
|
|
215
|
+
const newType = existing.type
|
|
216
|
+
if (changes.environments !== undefined) {
|
|
217
|
+
validateEnvironments(newEnvironments, newType)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** @type {CategoryEntry} */
|
|
221
|
+
const updated = {
|
|
222
|
+
...existing,
|
|
223
|
+
...(changes.name !== undefined ? {name: changes.name} : {}),
|
|
224
|
+
...(changes.environments !== undefined ? {environments: changes.environments} : {}),
|
|
225
|
+
...(changes.params !== undefined ? {params: changes.params} : {}),
|
|
226
|
+
...(changes.active !== undefined ? {active: changes.active} : {}),
|
|
227
|
+
updatedAt: new Date().toISOString(),
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
store.entries[index] = updated
|
|
231
|
+
await saveAIConfig(store, configPath)
|
|
232
|
+
await syncAIConfigToChezmoi()
|
|
233
|
+
return updated
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Set an entry's `active` flag to `false`.
|
|
238
|
+
* @param {string} id - UUID of the entry to deactivate
|
|
239
|
+
* @param {string} [configPath]
|
|
240
|
+
* @returns {Promise<CategoryEntry>}
|
|
241
|
+
*/
|
|
242
|
+
export async function deactivateEntry(id, configPath = process.env.DVMI_AI_CONFIG_PATH ?? AI_CONFIG_PATH) {
|
|
243
|
+
return updateEntry(id, {active: false}, configPath)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Set an entry's `active` flag to `true`.
|
|
248
|
+
* @param {string} id - UUID of the entry to activate
|
|
249
|
+
* @param {string} [configPath]
|
|
250
|
+
* @returns {Promise<CategoryEntry>}
|
|
251
|
+
*/
|
|
252
|
+
export async function activateEntry(id, configPath = process.env.DVMI_AI_CONFIG_PATH ?? AI_CONFIG_PATH) {
|
|
253
|
+
return updateEntry(id, {active: true}, configPath)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Permanently remove an entry from the store.
|
|
258
|
+
* @param {string} id - UUID of the entry to delete
|
|
259
|
+
* @param {string} [configPath]
|
|
260
|
+
* @returns {Promise<void>}
|
|
261
|
+
*/
|
|
262
|
+
export async function deleteEntry(id, configPath = process.env.DVMI_AI_CONFIG_PATH ?? AI_CONFIG_PATH) {
|
|
263
|
+
const store = await loadAIConfig(configPath)
|
|
264
|
+
|
|
265
|
+
const index = store.entries.findIndex((e) => e.id === id)
|
|
266
|
+
if (index === -1) {
|
|
267
|
+
throw new DvmiError(
|
|
268
|
+
`Entry with id "${id}" not found`,
|
|
269
|
+
'Run `dvmi sync-config-ai list` to see available entries and their IDs',
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
store.entries.splice(index, 1)
|
|
274
|
+
await saveAIConfig(store, configPath)
|
|
275
|
+
await syncAIConfigToChezmoi()
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Return all active entries that target a given environment.
|
|
280
|
+
* @param {EnvironmentId} envId
|
|
281
|
+
* @param {string} [configPath]
|
|
282
|
+
* @returns {Promise<CategoryEntry[]>}
|
|
283
|
+
*/
|
|
284
|
+
export async function getEntriesByEnvironment(envId, configPath = process.env.DVMI_AI_CONFIG_PATH ?? AI_CONFIG_PATH) {
|
|
285
|
+
const store = await loadAIConfig(configPath)
|
|
286
|
+
return store.entries.filter((e) => e.active && e.environments.includes(envId))
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Return all entries (active and inactive) of a given type.
|
|
291
|
+
* @param {CategoryType} type
|
|
292
|
+
* @param {string} [configPath]
|
|
293
|
+
* @returns {Promise<CategoryEntry[]>}
|
|
294
|
+
*/
|
|
295
|
+
export async function getEntriesByType(type, configPath = process.env.DVMI_AI_CONFIG_PATH ?? AI_CONFIG_PATH) {
|
|
296
|
+
const store = await loadAIConfig(configPath)
|
|
297
|
+
return store.entries.filter((e) => e.type === type)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
301
|
+
// Chezmoi sync
|
|
302
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Sync the AI config file to chezmoi if dotfiles management is enabled.
|
|
306
|
+
* Non-blocking — silently ignores errors.
|
|
307
|
+
* @returns {Promise<void>}
|
|
308
|
+
*/
|
|
309
|
+
export async function syncAIConfigToChezmoi() {
|
|
310
|
+
try {
|
|
311
|
+
const cliConfig = await loadConfig()
|
|
312
|
+
if (!cliConfig.dotfiles?.enabled) return
|
|
313
|
+
const configPath = process.env.DVMI_AI_CONFIG_PATH ?? AI_CONFIG_PATH
|
|
314
|
+
await exec('chezmoi', ['add', configPath])
|
|
315
|
+
} catch {
|
|
316
|
+
// Non-blocking — chezmoi sync failures should not disrupt the user's workflow
|
|
317
|
+
}
|
|
318
|
+
}
|