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.
Files changed (96) hide show
  1. package/README.md +7 -0
  2. package/oclif.manifest.json +129 -89
  3. package/package.json +2 -1
  4. package/src/commands/auth/login.js +20 -16
  5. package/src/commands/changelog.js +12 -12
  6. package/src/commands/costs/get.js +14 -24
  7. package/src/commands/costs/trend.js +13 -24
  8. package/src/commands/create/repo.js +72 -54
  9. package/src/commands/docs/list.js +29 -25
  10. package/src/commands/docs/projects.js +58 -24
  11. package/src/commands/docs/read.js +56 -39
  12. package/src/commands/docs/search.js +37 -25
  13. package/src/commands/doctor.js +37 -35
  14. package/src/commands/dotfiles/add.js +51 -39
  15. package/src/commands/dotfiles/setup.js +62 -33
  16. package/src/commands/dotfiles/status.js +18 -18
  17. package/src/commands/dotfiles/sync.js +62 -46
  18. package/src/commands/init.js +143 -132
  19. package/src/commands/logs/index.js +10 -16
  20. package/src/commands/open.js +12 -12
  21. package/src/commands/pipeline/logs.js +8 -11
  22. package/src/commands/pipeline/rerun.js +21 -16
  23. package/src/commands/pipeline/status.js +28 -24
  24. package/src/commands/pr/create.js +40 -27
  25. package/src/commands/pr/detail.js +9 -7
  26. package/src/commands/pr/review.js +18 -19
  27. package/src/commands/pr/status.js +27 -21
  28. package/src/commands/prompts/browse.js +15 -15
  29. package/src/commands/prompts/download.js +15 -16
  30. package/src/commands/prompts/install-speckit.js +11 -12
  31. package/src/commands/prompts/list.js +12 -12
  32. package/src/commands/prompts/run.js +16 -19
  33. package/src/commands/repo/list.js +57 -41
  34. package/src/commands/search.js +20 -18
  35. package/src/commands/security/setup.js +38 -34
  36. package/src/commands/sync-config-ai/index.js +143 -0
  37. package/src/commands/tasks/assigned.js +43 -33
  38. package/src/commands/tasks/list.js +43 -33
  39. package/src/commands/tasks/today.js +32 -30
  40. package/src/commands/upgrade.js +18 -17
  41. package/src/commands/vuln/detail.js +8 -8
  42. package/src/commands/vuln/scan.js +39 -20
  43. package/src/commands/vuln/search.js +23 -18
  44. package/src/commands/welcome.js +2 -2
  45. package/src/commands/whoami.js +19 -23
  46. package/src/formatters/ai-config.js +127 -0
  47. package/src/formatters/charts.js +6 -23
  48. package/src/formatters/cost.js +1 -7
  49. package/src/formatters/dotfiles.js +48 -19
  50. package/src/formatters/markdown.js +11 -6
  51. package/src/formatters/openapi.js +7 -9
  52. package/src/formatters/prompts.js +69 -78
  53. package/src/formatters/security.js +2 -2
  54. package/src/formatters/status.js +1 -1
  55. package/src/formatters/table.js +1 -3
  56. package/src/formatters/vuln.js +33 -20
  57. package/src/help.js +162 -164
  58. package/src/hooks/init.js +1 -3
  59. package/src/hooks/postrun.js +5 -7
  60. package/src/index.js +1 -1
  61. package/src/services/ai-config-store.js +318 -0
  62. package/src/services/ai-env-deployer.js +444 -0
  63. package/src/services/ai-env-scanner.js +242 -0
  64. package/src/services/audit-detector.js +2 -2
  65. package/src/services/audit-runner.js +40 -31
  66. package/src/services/auth.js +9 -9
  67. package/src/services/awesome-copilot.js +7 -4
  68. package/src/services/aws-costs.js +22 -22
  69. package/src/services/clickup.js +26 -26
  70. package/src/services/cloudwatch-logs.js +5 -9
  71. package/src/services/config.js +13 -13
  72. package/src/services/docs.js +19 -20
  73. package/src/services/dotfiles.js +149 -51
  74. package/src/services/github.js +22 -24
  75. package/src/services/nvd.js +21 -31
  76. package/src/services/platform.js +2 -2
  77. package/src/services/prompts.js +23 -35
  78. package/src/services/security.js +135 -61
  79. package/src/services/shell.js +4 -4
  80. package/src/services/skills-sh.js +3 -9
  81. package/src/services/speckit.js +4 -7
  82. package/src/services/version-check.js +10 -10
  83. package/src/types.js +85 -0
  84. package/src/utils/aws-vault.js +18 -41
  85. package/src/utils/banner.js +5 -7
  86. package/src/utils/errors.js +42 -46
  87. package/src/utils/frontmatter.js +4 -4
  88. package/src/utils/gradient.js +18 -16
  89. package/src/utils/open-browser.js +3 -3
  90. package/src/utils/tui/form.js +1006 -0
  91. package/src/utils/tui/modal.js +15 -14
  92. package/src/utils/tui/navigable-table.js +16 -16
  93. package/src/utils/tui/tab-tui.js +800 -0
  94. package/src/utils/typewriter.js +3 -3
  95. package/src/utils/welcome.js +18 -21
  96. 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
+ }