devvami 1.4.2 → 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.
Files changed (96) hide show
  1. package/README.md +72 -0
  2. package/oclif.manifest.json +275 -235
  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 +257 -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 +215 -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 +349 -0
  62. package/src/services/ai-env-deployer.js +650 -0
  63. package/src/services/ai-env-scanner.js +983 -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 +117 -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 +1184 -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 +1089 -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
@@ -1,13 +1,12 @@
1
- import { loadConfig } from './config.js'
2
- import { DvmiError } from '../utils/errors.js'
1
+ import {loadConfig} from './config.js'
2
+ import {DvmiError} from '../utils/errors.js'
3
3
 
4
4
  /** @import { CveSearchResult, CveDetail } from '../types.js' */
5
5
 
6
6
  const NVD_BASE_URL = 'https://services.nvd.nist.gov/rest/json/cves/2.0'
7
7
 
8
8
  /** NVD attribution required in all interactive output. */
9
- export const NVD_ATTRIBUTION =
10
- 'This product uses data from the NVD API but is not endorsed or certified by the NVD.'
9
+ export const NVD_ATTRIBUTION = 'This product uses data from the NVD API but is not endorsed or certified by the NVD.'
11
10
 
12
11
  /**
13
12
  * Normalize a raw NVD severity string to the 4-tier canonical form.
@@ -31,11 +30,7 @@ export function normalizeSeverity(raw) {
31
30
  * @returns {{ score: number|null, severity: string, vector: string|null }}
32
31
  */
33
32
  function extractCvss(metrics) {
34
- const sources = [
35
- (metrics?.cvssMetricV31 ?? []),
36
- (metrics?.cvssMetricV40 ?? []),
37
- (metrics?.cvssMetricV2 ?? []),
38
- ]
33
+ const sources = [metrics?.cvssMetricV31 ?? [], metrics?.cvssMetricV40 ?? [], metrics?.cvssMetricV2 ?? []]
39
34
 
40
35
  for (const list of sources) {
41
36
  if (Array.isArray(list) && list.length > 0) {
@@ -50,7 +45,7 @@ function extractCvss(metrics) {
50
45
  }
51
46
  }
52
47
 
53
- return { score: null, severity: 'Unknown', vector: null }
48
+ return {score: null, severity: 'Unknown', vector: null}
54
49
  }
55
50
 
56
51
  /**
@@ -88,15 +83,12 @@ function buildParams(params) {
88
83
  async function nvdFetch(params, apiKey) {
89
84
  const url = `${NVD_BASE_URL}?${params.toString()}`
90
85
  /** @type {Record<string, string>} */
91
- const headers = { Accept: 'application/json' }
86
+ const headers = {Accept: 'application/json'}
92
87
  if (apiKey) headers['apiKey'] = apiKey
93
88
 
94
- const res = await fetch(url, { headers })
89
+ const res = await fetch(url, {headers})
95
90
  if (!res.ok) {
96
- throw new DvmiError(
97
- `NVD API returned HTTP ${res.status}`,
98
- 'Check your network connection or try again later.',
99
- )
91
+ throw new DvmiError(`NVD API returned HTTP ${res.status}`, 'Check your network connection or try again later.')
100
92
  }
101
93
  return res.json()
102
94
  }
@@ -108,7 +100,7 @@ async function nvdFetch(params, apiKey) {
108
100
  */
109
101
  function parseCveSearchResult(raw) {
110
102
  const cve = raw.cve
111
- const { score, severity } = extractCvss(cve.metrics ?? {})
103
+ const {score, severity} = extractCvss(cve.metrics ?? {})
112
104
  return {
113
105
  id: cve.id,
114
106
  description: getEnDescription(cve.descriptions),
@@ -127,7 +119,7 @@ function parseCveSearchResult(raw) {
127
119
  */
128
120
  function parseCveDetail(raw) {
129
121
  const cve = raw.cve
130
- const { score, severity, vector } = extractCvss(cve.metrics ?? {})
122
+ const {score, severity, vector} = extractCvss(cve.metrics ?? {})
131
123
 
132
124
  // Weaknesses: flatten all CWE descriptions
133
125
  const weaknesses = (cve.weaknesses ?? []).flatMap((w) =>
@@ -149,10 +141,11 @@ function parseCveDetail(raw) {
149
141
  const product = parts[4] ?? 'unknown'
150
142
  const versionStart = m.versionStartIncluding ?? m.versionStartExcluding ?? ''
151
143
  const versionEnd = m.versionEndExcluding ?? m.versionEndIncluding ?? ''
152
- const versions = versionStart && versionEnd
153
- ? `${versionStart} to ${versionEnd}`
154
- : versionStart || versionEnd || (parts[5] ?? '*')
155
- return { vendor, product, versions }
144
+ const versions =
145
+ versionStart && versionEnd
146
+ ? `${versionStart} to ${versionEnd}`
147
+ : versionStart || versionEnd || (parts[5] ?? '*')
148
+ return {vendor, product, versions}
156
149
  }),
157
150
  ),
158
151
  )
@@ -188,7 +181,7 @@ function parseCveDetail(raw) {
188
181
  * @param {number} [options.limit=20] - Maximum results to return
189
182
  * @returns {Promise<{ results: CveSearchResult[], totalResults: number }>}
190
183
  */
191
- export async function searchCves({ keyword, days = 14, severity, limit = 20 }) {
184
+ export async function searchCves({keyword, days = 14, severity, limit = 20}) {
192
185
  const config = await loadConfig()
193
186
  const apiKey = config.nvd?.apiKey
194
187
 
@@ -202,17 +195,17 @@ export async function searchCves({ keyword, days = 14, severity, limit = 20 }) {
202
195
  const trimmedKeyword = keyword?.trim()
203
196
 
204
197
  const params = buildParams({
205
- ...(trimmedKeyword ? { keywordSearch: trimmedKeyword } : {}),
198
+ ...(trimmedKeyword ? {keywordSearch: trimmedKeyword} : {}),
206
199
  pubStartDate,
207
200
  pubEndDate,
208
201
  resultsPerPage: limit,
209
- ...(severity ? { cvssV3Severity: severity.toUpperCase() } : {}),
202
+ ...(severity ? {cvssV3Severity: severity.toUpperCase()} : {}),
210
203
  })
211
204
 
212
205
  const data = /** @type {any} */ (await nvdFetch(params, apiKey))
213
206
 
214
207
  const results = (data.vulnerabilities ?? []).map(parseCveSearchResult)
215
- return { results, totalResults: data.totalResults ?? results.length }
208
+ return {results, totalResults: data.totalResults ?? results.length}
216
209
  }
217
210
 
218
211
  /**
@@ -231,14 +224,11 @@ export async function getCveDetail(cveId) {
231
224
  const config = await loadConfig()
232
225
  const apiKey = config.nvd?.apiKey
233
226
 
234
- const params = buildParams({ cveId: cveId.toUpperCase() })
227
+ const params = buildParams({cveId: cveId.toUpperCase()})
235
228
  const data = /** @type {any} */ (await nvdFetch(params, apiKey))
236
229
 
237
230
  if (!data.vulnerabilities || data.vulnerabilities.length === 0) {
238
- throw new DvmiError(
239
- `CVE not found: ${cveId}`,
240
- 'Verify the CVE ID is correct and exists in the NVD database.',
241
- )
231
+ throw new DvmiError(`CVE not found: ${cveId}`, 'Verify the CVE ID is correct and exists in the NVD database.')
242
232
  }
243
233
 
244
234
  return parseCveDetail(data.vulnerabilities[0])
@@ -1,5 +1,5 @@
1
- import { readFile } from 'node:fs/promises'
2
- import { existsSync } from 'node:fs'
1
+ import {readFile} from 'node:fs/promises'
2
+ import {existsSync} from 'node:fs'
3
3
 
4
4
  /** @import { Platform, PlatformInfo } from '../types.js' */
5
5
 
@@ -1,10 +1,10 @@
1
- import { mkdir, writeFile, readFile, access } from 'node:fs/promises'
2
- import { join, dirname, resolve, sep } from 'node:path'
3
- import { execa } from 'execa'
4
- import { createOctokit } from './github.js'
5
- import { which } from './shell.js'
6
- import { parseFrontmatter, serializeFrontmatter } from '../utils/frontmatter.js'
7
- import { DvmiError } from '../utils/errors.js'
1
+ import {mkdir, writeFile, readFile, access} from 'node:fs/promises'
2
+ import {join, dirname, resolve, sep} from 'node:path'
3
+ import {execa} from 'execa'
4
+ import {createOctokit} from './github.js'
5
+ import {which} from './shell.js'
6
+ import {parseFrontmatter, serializeFrontmatter} from '../utils/frontmatter.js'
7
+ import {DvmiError} from '../utils/errors.js'
8
8
 
9
9
  /** @import { Prompt, AITool } from '../types.js' */
10
10
 
@@ -13,15 +13,15 @@ import { DvmiError } from '../utils/errors.js'
13
13
  * @type {Record<AITool, { bin: string[], promptFlag: string }>}
14
14
  */
15
15
  export const SUPPORTED_TOOLS = {
16
- opencode: { bin: ['opencode'], promptFlag: '--prompt' },
17
- copilot: { bin: ['gh', 'copilot'], promptFlag: '-p' },
16
+ opencode: {bin: ['opencode'], promptFlag: '--prompt'},
17
+ copilot: {bin: ['gh', 'copilot'], promptFlag: '-p'},
18
18
  }
19
19
 
20
20
  /**
21
21
  * GitHub repository containing the personal prompt collection.
22
22
  * @type {{ owner: string, repo: string }}
23
23
  */
24
- export const PROMPT_REPO = { owner: 'savez', repo: 'prompt-for-ai' }
24
+ export const PROMPT_REPO = {owner: 'savez', repo: 'prompt-for-ai'}
25
25
 
26
26
  /**
27
27
  * Default branch used when fetching the repository tree.
@@ -75,7 +75,7 @@ function categoryFromPath(filePath) {
75
75
  */
76
76
  function contentToPrompt(path, base64Content) {
77
77
  const raw = Buffer.from(base64Content, 'base64').toString('utf8')
78
- const { frontmatter, body } = parseFrontmatter(raw)
78
+ const {frontmatter, body} = parseFrontmatter(raw)
79
79
  return {
80
80
  path,
81
81
  title: typeof frontmatter.title === 'string' ? frontmatter.title : titleFromPath(path),
@@ -101,7 +101,7 @@ export async function listPrompts() {
101
101
  const octokit = await createOctokit()
102
102
  let tree
103
103
  try {
104
- const { data } = await octokit.rest.git.getTree({
104
+ const {data} = await octokit.rest.git.getTree({
105
105
  owner: PROMPT_REPO.owner,
106
106
  repo: PROMPT_REPO.repo,
107
107
  tree_sha: DEFAULT_BRANCH,
@@ -132,7 +132,7 @@ export async function listPrompts() {
132
132
 
133
133
  const prompts = await Promise.all(
134
134
  mdFiles.map(async (item) => {
135
- const { data } = await octokit.rest.repos.getContent({
135
+ const {data} = await octokit.rest.repos.getContent({
136
136
  owner: PROMPT_REPO.owner,
137
137
  repo: PROMPT_REPO.repo,
138
138
  path: item.path ?? '',
@@ -169,10 +169,7 @@ export async function fetchPromptByPath(relativePath) {
169
169
  } catch (err) {
170
170
  const status = /** @type {{ status?: number }} */ (err).status
171
171
  if (status === 404) {
172
- throw new DvmiError(
173
- `Prompt not found: ${relativePath}`,
174
- `Run \`dvmi prompts list\` to see available prompts`,
175
- )
172
+ throw new DvmiError(`Prompt not found: ${relativePath}`, `Run \`dvmi prompts list\` to see available prompts`)
176
173
  }
177
174
  throw err
178
175
  }
@@ -207,17 +204,14 @@ export async function downloadPrompt(relativePath, localDir, opts = {}) {
207
204
  // Prevent path traversal: destPath must remain within localDir
208
205
  const safeBase = resolve(localDir) + sep
209
206
  if (!resolve(destPath).startsWith(safeBase)) {
210
- throw new DvmiError(
211
- `Invalid prompt path: "${relativePath}"`,
212
- 'Path must stay within the prompts directory',
213
- )
207
+ throw new DvmiError(`Invalid prompt path: "${relativePath}"`, 'Path must stay within the prompts directory')
214
208
  }
215
209
 
216
210
  // Fast-path: skip without a network round-trip if file exists and no overwrite
217
211
  if (!opts.overwrite) {
218
212
  try {
219
213
  await access(destPath)
220
- return { path: destPath, skipped: true }
214
+ return {path: destPath, skipped: true}
221
215
  } catch {
222
216
  // File does not exist — fall through to download
223
217
  }
@@ -237,10 +231,10 @@ export async function downloadPrompt(relativePath, localDir, opts = {}) {
237
231
 
238
232
  const content = serializeFrontmatter(fm, prompt.body)
239
233
 
240
- await mkdir(dirname(destPath), { recursive: true, mode: 0o700 })
241
- await writeFile(destPath, content, { encoding: 'utf8', mode: 0o600 })
234
+ await mkdir(dirname(destPath), {recursive: true, mode: 0o700})
235
+ await writeFile(destPath, content, {encoding: 'utf8', mode: 0o600})
242
236
 
243
- return { path: destPath, skipped: false }
237
+ return {path: destPath, skipped: false}
244
238
  }
245
239
 
246
240
  /**
@@ -258,10 +252,7 @@ export async function resolveLocalPrompt(relativePath, localDir) {
258
252
  // Prevent path traversal: fullPath must remain within localDir
259
253
  const safeBase = resolve(localDir) + sep
260
254
  if (!resolve(fullPath).startsWith(safeBase)) {
261
- throw new DvmiError(
262
- `Invalid prompt path: "${relativePath}"`,
263
- 'Path must stay within the prompts directory',
264
- )
255
+ throw new DvmiError(`Invalid prompt path: "${relativePath}"`, 'Path must stay within the prompts directory')
265
256
  }
266
257
 
267
258
  let raw
@@ -274,7 +265,7 @@ export async function resolveLocalPrompt(relativePath, localDir) {
274
265
  )
275
266
  }
276
267
 
277
- const { frontmatter, body } = parseFrontmatter(raw)
268
+ const {frontmatter, body} = parseFrontmatter(raw)
278
269
  return {
279
270
  path: relativePath,
280
271
  title: typeof frontmatter.title === 'string' ? frontmatter.title : titleFromPath(relativePath),
@@ -301,10 +292,7 @@ export async function resolveLocalPrompt(relativePath, localDir) {
301
292
  export async function invokeTool(toolName, promptContent) {
302
293
  const tool = SUPPORTED_TOOLS[toolName]
303
294
  if (!tool) {
304
- throw new DvmiError(
305
- `Unknown AI tool: "${toolName}"`,
306
- `Supported tools: ${Object.keys(SUPPORTED_TOOLS).join(', ')}`,
307
- )
295
+ throw new DvmiError(`Unknown AI tool: "${toolName}"`, `Supported tools: ${Object.keys(SUPPORTED_TOOLS).join(', ')}`)
308
296
  }
309
297
 
310
298
  // Verify binary availability
@@ -322,5 +310,5 @@ export async function invokeTool(toolName, promptContent) {
322
310
  }
323
311
 
324
312
  // Spawn tool with prompt content — inherits stdio so TUI/interactive tools work
325
- await execa(bin, [...subArgs, tool.promptFlag, promptContent], { stdio: 'inherit' })
313
+ await execa(bin, [...subArgs, tool.promptFlag, promptContent], {stdio: 'inherit'})
326
314
  }