devvami 1.3.0 → 1.4.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/oclif.manifest.json +397 -1
- package/package.json +1 -1
- package/src/commands/dotfiles/add.js +249 -0
- package/src/commands/dotfiles/setup.js +190 -0
- package/src/commands/dotfiles/status.js +103 -0
- package/src/commands/dotfiles/sync.js +375 -0
- package/src/commands/init.js +35 -2
- package/src/commands/vuln/detail.js +65 -0
- package/src/commands/vuln/scan.js +155 -0
- package/src/commands/vuln/search.js +128 -0
- package/src/formatters/dotfiles.js +259 -0
- package/src/formatters/vuln.js +317 -0
- package/src/help.js +62 -2
- package/src/services/audit-detector.js +120 -0
- package/src/services/audit-runner.js +365 -0
- package/src/services/dotfiles.js +573 -0
- package/src/services/nvd.js +245 -0
- package/src/types.js +73 -5
- package/src/utils/errors.js +2 -0
- package/src/utils/tui/modal.js +224 -0
- package/src/utils/tui/navigable-table.js +496 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { renderTable } from './table.js'
|
|
3
|
+
import { NVD_ATTRIBUTION } from '../services/nvd.js'
|
|
4
|
+
|
|
5
|
+
/** @import { CveSearchResult, CveDetail, VulnerabilityFinding, ScanResult } from '../types.js' */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Colorize a severity string for terminal output.
|
|
9
|
+
* @param {string} severity
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
export function colorSeverity(severity) {
|
|
13
|
+
switch (severity) {
|
|
14
|
+
case 'Critical': return chalk.red.bold(severity)
|
|
15
|
+
case 'High': return chalk.red(severity)
|
|
16
|
+
case 'Medium': return chalk.yellow(severity)
|
|
17
|
+
case 'Low': return chalk.blue(severity)
|
|
18
|
+
default: return chalk.gray(severity)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Format a CVE score as a fixed-precision string or "N/A".
|
|
24
|
+
* @param {number|null} score
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function formatScore(score) {
|
|
28
|
+
if (score === null || score === undefined) return 'N/A'
|
|
29
|
+
return score.toFixed(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Format an ISO-8601 date string as YYYY-MM-DD.
|
|
34
|
+
* @param {string} iso
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function formatDate(iso) {
|
|
38
|
+
if (!iso) return ''
|
|
39
|
+
return iso.slice(0, 10)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Truncate a string to max length, appending … if needed.
|
|
44
|
+
* @param {string} str
|
|
45
|
+
* @param {number} max
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
export function truncate(str, max) {
|
|
49
|
+
if (!str) return ''
|
|
50
|
+
if (str.length <= max) return str
|
|
51
|
+
return str.slice(0, max - 1) + '…'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Format a list of CVE search results as a terminal table string.
|
|
56
|
+
* @param {CveSearchResult[]} results
|
|
57
|
+
* @param {string|null|undefined} keyword
|
|
58
|
+
* @param {number} days
|
|
59
|
+
* @param {number} totalResults
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
export function formatCveSearchTable(results, keyword, days, totalResults) {
|
|
63
|
+
const lines = []
|
|
64
|
+
const heading = keyword
|
|
65
|
+
? `CVE Search: "${keyword}" (last ${days} days)`
|
|
66
|
+
: `CVE Search: all recent (last ${days} days)`
|
|
67
|
+
lines.push(chalk.bold(`${heading}\n`))
|
|
68
|
+
|
|
69
|
+
if (results.length === 0) {
|
|
70
|
+
lines.push(keyword ? ' No CVEs found for this search.' : ' No CVEs published in this time window.')
|
|
71
|
+
lines.push('')
|
|
72
|
+
lines.push(chalk.dim(NVD_ATTRIBUTION))
|
|
73
|
+
return lines.join('\n')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const rows = results.map((r) => ({
|
|
77
|
+
id: r.id,
|
|
78
|
+
severity: r.severity,
|
|
79
|
+
score: formatScore(r.score),
|
|
80
|
+
published: formatDate(r.publishedDate),
|
|
81
|
+
description: truncate(r.description, 90),
|
|
82
|
+
reference: r.firstReference ? truncate(r.firstReference, 45) : '—',
|
|
83
|
+
}))
|
|
84
|
+
|
|
85
|
+
const table = renderTable(rows, [
|
|
86
|
+
{ header: 'CVE ID', key: 'id', colorize: (v) => chalk.cyan(v) },
|
|
87
|
+
{ header: 'Severity', key: 'severity', colorize: (v) => colorSeverity(v) },
|
|
88
|
+
{ header: 'Score', key: 'score', width: 5 },
|
|
89
|
+
{ header: 'Published', key: 'published', width: 10 },
|
|
90
|
+
{ header: 'Description', key: 'description', width: 90 },
|
|
91
|
+
{ header: 'Reference', key: 'reference', width: 45 },
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
// Indent table by 2 spaces
|
|
95
|
+
lines.push(table.split('\n').map((l) => ` ${l}`).join('\n'))
|
|
96
|
+
lines.push('')
|
|
97
|
+
lines.push(`Showing ${results.length} of ${totalResults} results.`)
|
|
98
|
+
lines.push(chalk.dim(NVD_ATTRIBUTION))
|
|
99
|
+
return lines.join('\n')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Format a full CVE detail record for terminal output.
|
|
104
|
+
* @param {CveDetail} cve
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
export function formatCveDetail(cve) {
|
|
108
|
+
const lines = []
|
|
109
|
+
|
|
110
|
+
const scoreStr = cve.score !== null ? ` (${formatScore(cve.score)})` : ''
|
|
111
|
+
lines.push(chalk.bold.cyan(`${cve.id}`) + chalk.bold(` — `) + colorSeverity(cve.severity) + chalk.bold(scoreStr))
|
|
112
|
+
lines.push('')
|
|
113
|
+
|
|
114
|
+
// Description
|
|
115
|
+
lines.push(chalk.bold(' Description'))
|
|
116
|
+
// Word-wrap at ~80 chars, indented 2 spaces
|
|
117
|
+
lines.push(wordWrap(cve.description, 78, ' '))
|
|
118
|
+
lines.push('')
|
|
119
|
+
|
|
120
|
+
// Details
|
|
121
|
+
lines.push(chalk.bold(' Details'))
|
|
122
|
+
lines.push(` Status: ${cve.status}`)
|
|
123
|
+
lines.push(` Published: ${formatDate(cve.publishedDate)}`)
|
|
124
|
+
lines.push(` Last Modified: ${formatDate(cve.lastModified)}`)
|
|
125
|
+
if (cve.cvssVector) lines.push(` CVSS Vector: ${cve.cvssVector}`)
|
|
126
|
+
if (cve.weaknesses.length > 0) {
|
|
127
|
+
const cweIds = [...new Set(cve.weaknesses.map((w) => w.id))].join(', ')
|
|
128
|
+
lines.push(` Weaknesses: ${cweIds}`)
|
|
129
|
+
}
|
|
130
|
+
lines.push('')
|
|
131
|
+
|
|
132
|
+
// Affected products
|
|
133
|
+
if (cve.affectedProducts.length > 0) {
|
|
134
|
+
lines.push(chalk.bold(' Affected Products'))
|
|
135
|
+
lines.push(' ' + chalk.dim('─'.repeat(40)))
|
|
136
|
+
for (const p of cve.affectedProducts) {
|
|
137
|
+
lines.push(` ${p.vendor} / ${p.product} / ${p.versions}`)
|
|
138
|
+
}
|
|
139
|
+
lines.push('')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// References
|
|
143
|
+
if (cve.references.length > 0) {
|
|
144
|
+
lines.push(chalk.bold(' References'))
|
|
145
|
+
cve.references.slice(0, 5).forEach((ref, i) => {
|
|
146
|
+
const tagStr = ref.tags.length > 0 ? ` (${ref.tags.join(', ')})` : ''
|
|
147
|
+
lines.push(` [${i + 1}] ${ref.url}${tagStr}`)
|
|
148
|
+
})
|
|
149
|
+
lines.push('')
|
|
150
|
+
lines.push(chalk.dim(' Tip: Use --open to open the first reference in your browser.'))
|
|
151
|
+
lines.push('')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
lines.push(chalk.dim(NVD_ATTRIBUTION))
|
|
155
|
+
return lines.join('\n')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Format a full CVE detail record as an array of plain-text lines (no chalk/ANSI).
|
|
160
|
+
* Used by the modal overlay in the navigable table TUI.
|
|
161
|
+
* @param {CveDetail} cve
|
|
162
|
+
* @returns {string[]}
|
|
163
|
+
*/
|
|
164
|
+
export function formatCveDetailPlain(cve) {
|
|
165
|
+
const lines = []
|
|
166
|
+
|
|
167
|
+
const scoreStr = cve.score !== null ? ` (${formatScore(cve.score)})` : ''
|
|
168
|
+
lines.push(`${cve.id} — ${cve.severity}${scoreStr}`)
|
|
169
|
+
lines.push('')
|
|
170
|
+
|
|
171
|
+
lines.push(' Description')
|
|
172
|
+
const wrappedDesc = wordWrap(cve.description, 78, ' ')
|
|
173
|
+
for (const l of wrappedDesc.split('\n')) lines.push(l)
|
|
174
|
+
lines.push('')
|
|
175
|
+
|
|
176
|
+
lines.push(' Details')
|
|
177
|
+
lines.push(` Status: ${cve.status}`)
|
|
178
|
+
lines.push(` Published: ${formatDate(cve.publishedDate)}`)
|
|
179
|
+
lines.push(` Last Modified: ${formatDate(cve.lastModified)}`)
|
|
180
|
+
if (cve.cvssVector) lines.push(` CVSS Vector: ${cve.cvssVector}`)
|
|
181
|
+
if (cve.weaknesses.length > 0) {
|
|
182
|
+
const cweIds = [...new Set(cve.weaknesses.map((w) => w.id))].join(', ')
|
|
183
|
+
lines.push(` Weaknesses: ${cweIds}`)
|
|
184
|
+
}
|
|
185
|
+
lines.push('')
|
|
186
|
+
|
|
187
|
+
if (cve.affectedProducts.length > 0) {
|
|
188
|
+
lines.push(' Affected Products')
|
|
189
|
+
lines.push(' ' + '─'.repeat(40))
|
|
190
|
+
for (const p of cve.affectedProducts) {
|
|
191
|
+
lines.push(` ${p.vendor} / ${p.product} / ${p.versions}`)
|
|
192
|
+
}
|
|
193
|
+
lines.push('')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (cve.references.length > 0) {
|
|
197
|
+
lines.push(' References')
|
|
198
|
+
cve.references.slice(0, 5).forEach((ref, i) => {
|
|
199
|
+
const tagStr = ref.tags.length > 0 ? ` (${ref.tags.join(', ')})` : ''
|
|
200
|
+
lines.push(` [${i + 1}] ${ref.url}${tagStr}`)
|
|
201
|
+
})
|
|
202
|
+
lines.push('')
|
|
203
|
+
lines.push(' Tip: Press o to open the first reference in your browser.')
|
|
204
|
+
lines.push('')
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
lines.push(NVD_ATTRIBUTION)
|
|
208
|
+
return lines
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Word-wrap a string to a max line length with an indent prefix.
|
|
213
|
+
* @param {string} text
|
|
214
|
+
* @param {number} maxLen
|
|
215
|
+
* @param {string} indent
|
|
216
|
+
* @returns {string}
|
|
217
|
+
*/
|
|
218
|
+
function wordWrap(text, maxLen, indent) {
|
|
219
|
+
if (!text) return ''
|
|
220
|
+
const words = text.split(' ')
|
|
221
|
+
const wrappedLines = []
|
|
222
|
+
let current = indent
|
|
223
|
+
|
|
224
|
+
for (const word of words) {
|
|
225
|
+
if (current.length + word.length + 1 > maxLen && current.trim().length > 0) {
|
|
226
|
+
wrappedLines.push(current.trimEnd())
|
|
227
|
+
current = indent + word + ' '
|
|
228
|
+
} else {
|
|
229
|
+
current += word + ' '
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (current.trim()) wrappedLines.push(current.trimEnd())
|
|
233
|
+
return wrappedLines.join('\n')
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Format a list of VulnerabilityFindings as a terminal table string.
|
|
238
|
+
* @param {VulnerabilityFinding[]} findings
|
|
239
|
+
* @returns {string}
|
|
240
|
+
*/
|
|
241
|
+
export function formatFindingsTable(findings) {
|
|
242
|
+
if (findings.length === 0) return ''
|
|
243
|
+
|
|
244
|
+
const rows = findings.map((f) => ({
|
|
245
|
+
pkg: f.package,
|
|
246
|
+
version: f.installedVersion,
|
|
247
|
+
severity: f.severity,
|
|
248
|
+
cve: f.cveId ?? '—',
|
|
249
|
+
title: truncate(f.title ?? '—', 40),
|
|
250
|
+
}))
|
|
251
|
+
|
|
252
|
+
const table = renderTable(rows, [
|
|
253
|
+
{ header: 'Package', key: 'pkg', width: 20 },
|
|
254
|
+
{ header: 'Version', key: 'version', width: 12 },
|
|
255
|
+
{ header: 'Severity', key: 'severity', colorize: (v) => colorSeverity(v) },
|
|
256
|
+
{ header: 'CVE', key: 'cve', colorize: (v) => (v !== '—' ? chalk.cyan(v) : chalk.gray(v)) },
|
|
257
|
+
{ header: 'Title', key: 'title', width: 40 },
|
|
258
|
+
])
|
|
259
|
+
|
|
260
|
+
return table.split('\n').map((l) => ` ${l}`).join('\n')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Format a ScanResult summary block for terminal output.
|
|
265
|
+
* @param {import('../types.js').ScanSummary} summary
|
|
266
|
+
* @returns {string}
|
|
267
|
+
*/
|
|
268
|
+
export function formatScanSummary(summary) {
|
|
269
|
+
const lines = []
|
|
270
|
+
lines.push(' ' + chalk.dim('─'.repeat(21)))
|
|
271
|
+
lines.push(` Critical: ${summary.critical}`)
|
|
272
|
+
lines.push(` High: ${summary.high}`)
|
|
273
|
+
lines.push(` Medium: ${summary.medium}`)
|
|
274
|
+
lines.push(` Low: ${summary.low}`)
|
|
275
|
+
lines.push(` Total: ${summary.total}`)
|
|
276
|
+
return lines.join('\n')
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Generate a Markdown vulnerability report.
|
|
281
|
+
* @param {ScanResult} result
|
|
282
|
+
* @returns {string}
|
|
283
|
+
*/
|
|
284
|
+
export function formatMarkdownReport(result) {
|
|
285
|
+
const lines = []
|
|
286
|
+
lines.push('# Vulnerability Report')
|
|
287
|
+
lines.push('')
|
|
288
|
+
lines.push(`**Project**: ${result.projectPath} `)
|
|
289
|
+
lines.push(`**Date**: ${result.scanDate} `)
|
|
290
|
+
lines.push(`**Ecosystems scanned**: ${result.ecosystems.map((e) => e.name).join(', ')}`)
|
|
291
|
+
lines.push('')
|
|
292
|
+
lines.push('## Summary')
|
|
293
|
+
lines.push('')
|
|
294
|
+
lines.push('| Severity | Count |')
|
|
295
|
+
lines.push('|----------|-------|')
|
|
296
|
+
lines.push(`| Critical | ${result.summary.critical} |`)
|
|
297
|
+
lines.push(`| High | ${result.summary.high} |`)
|
|
298
|
+
lines.push(`| Medium | ${result.summary.medium} |`)
|
|
299
|
+
lines.push(`| Low | ${result.summary.low} |`)
|
|
300
|
+
lines.push(`| **Total** | **${result.summary.total}** |`)
|
|
301
|
+
lines.push('')
|
|
302
|
+
|
|
303
|
+
if (result.findings.length > 0) {
|
|
304
|
+
lines.push('## Findings')
|
|
305
|
+
lines.push('')
|
|
306
|
+
lines.push('| Package | Version | Severity | CVE | Title |')
|
|
307
|
+
lines.push('|---------|---------|----------|-----|-------|')
|
|
308
|
+
for (const f of result.findings) {
|
|
309
|
+
lines.push(`| ${f.package} | ${f.installedVersion} | ${f.severity} | ${f.cveId ?? '—'} | ${f.title ?? '—'} |`)
|
|
310
|
+
}
|
|
311
|
+
lines.push('')
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
lines.push('---')
|
|
315
|
+
lines.push(`*${NVD_ATTRIBUTION}*`)
|
|
316
|
+
return lines.join('\n')
|
|
317
|
+
}
|
package/src/help.js
CHANGED
|
@@ -87,6 +87,23 @@ const CATEGORIES = [
|
|
|
87
87
|
{ id: 'security:setup', hint: '[--json]' },
|
|
88
88
|
],
|
|
89
89
|
},
|
|
90
|
+
{
|
|
91
|
+
title: 'CVE & Vulnerabilità',
|
|
92
|
+
cmds: [
|
|
93
|
+
{ id: 'vuln:search', hint: '[KEYWORD] [--days] [--severity] [--limit]' },
|
|
94
|
+
{ id: 'vuln:detail', hint: '<CVE-ID> [--open]' },
|
|
95
|
+
{ id: 'vuln:scan', hint: '[--severity] [--no-fail] [--report]' },
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
title: 'Dotfiles & Cifratura',
|
|
100
|
+
cmds: [
|
|
101
|
+
{ id: 'dotfiles:setup', hint: '[--json]' },
|
|
102
|
+
{ id: 'dotfiles:add', hint: '[FILES...] [--encrypt]' },
|
|
103
|
+
{ id: 'dotfiles:status', hint: '[--json]' },
|
|
104
|
+
{ id: 'dotfiles:sync', hint: '[--push] [--pull] [--dry-run]' },
|
|
105
|
+
],
|
|
106
|
+
},
|
|
90
107
|
{
|
|
91
108
|
title: 'Setup & Ambiente',
|
|
92
109
|
cmds: [
|
|
@@ -119,7 +136,20 @@ export default class CustomHelp extends Help {
|
|
|
119
136
|
async showRootHelp() {
|
|
120
137
|
// Animated logo — identical to `dvmi init` (no-ops in CI/non-TTY)
|
|
121
138
|
await printBanner()
|
|
122
|
-
|
|
139
|
+
|
|
140
|
+
// Version check: uses cached result (populated by init hook) — 800 ms timeout
|
|
141
|
+
let versionInfo = null
|
|
142
|
+
try {
|
|
143
|
+
const { checkForUpdate } = await import('./services/version-check.js')
|
|
144
|
+
versionInfo = await Promise.race([
|
|
145
|
+
checkForUpdate(),
|
|
146
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 800)),
|
|
147
|
+
])
|
|
148
|
+
} catch {
|
|
149
|
+
// never block help output
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.log(this.#buildRootLayout(versionInfo))
|
|
123
153
|
}
|
|
124
154
|
|
|
125
155
|
/**
|
|
@@ -151,9 +181,10 @@ export default class CustomHelp extends Help {
|
|
|
151
181
|
|
|
152
182
|
/**
|
|
153
183
|
* Build the full categorized root help layout.
|
|
184
|
+
* @param {{ hasUpdate: boolean, current: string, latest: string|null }|null} [versionInfo]
|
|
154
185
|
* @returns {string}
|
|
155
186
|
*/
|
|
156
|
-
#buildRootLayout() {
|
|
187
|
+
#buildRootLayout(versionInfo = null) {
|
|
157
188
|
/** @type {Map<string, import('@oclif/core').Command.Cached>} */
|
|
158
189
|
const cmdMap = new Map(this.config.commands.map((c) => [c.id, c]))
|
|
159
190
|
|
|
@@ -181,7 +212,18 @@ export default class CustomHelp extends Help {
|
|
|
181
212
|
{ cmd: 'dvmi logs --group /aws/lambda/my-fn --filter "ERROR"', note: 'Filtra eventi ERROR su un log group' },
|
|
182
213
|
{ cmd: 'dvmi security setup --json', note: 'Controlla lo stato degli strumenti di sicurezza' },
|
|
183
214
|
{ cmd: 'dvmi security setup', note: 'Wizard interattivo: installa aws-vault e GCM' },
|
|
215
|
+
{ cmd: 'dvmi dotfiles setup', note: 'Configura chezmoi con cifratura age' },
|
|
216
|
+
{ cmd: 'dvmi dotfiles add ~/.zshrc ~/.gitconfig', note: 'Aggiungi dotfile a chezmoi' },
|
|
217
|
+
{ cmd: 'dvmi dotfiles status --json', note: 'Stato dotfile gestiti (JSON)' },
|
|
218
|
+
{ cmd: 'dvmi dotfiles sync --push', note: 'Push dotfile al repository remoto' },
|
|
184
219
|
{ cmd: 'dvmi welcome', note: 'Dashboard missione dvmi con intro animata' },
|
|
220
|
+
{ cmd: 'dvmi vuln search openssl', note: 'Cerca CVE recenti per keyword' },
|
|
221
|
+
{ cmd: 'dvmi vuln search log4j --days 30 --severity critical', note: 'CVE critiche Log4j negli ultimi 30 giorni' },
|
|
222
|
+
{ cmd: 'dvmi vuln detail CVE-2021-44228', note: 'Dettaglio completo di una CVE' },
|
|
223
|
+
{ cmd: 'dvmi vuln detail CVE-2021-44228 --open', note: 'Apri la prima referenza nel browser' },
|
|
224
|
+
{ cmd: 'dvmi vuln scan', note: 'Scansiona dipendenze del progetto corrente' },
|
|
225
|
+
{ cmd: 'dvmi vuln scan --severity high --no-fail', note: 'Scansione senza bloccare CI (solo high+)' },
|
|
226
|
+
{ cmd: 'dvmi vuln scan --report ./vuln-report.md', note: 'Esporta report Markdown delle vulnerabilità' },
|
|
185
227
|
]
|
|
186
228
|
|
|
187
229
|
const lines = []
|
|
@@ -248,6 +290,24 @@ export default class CustomHelp extends Help {
|
|
|
248
290
|
}
|
|
249
291
|
|
|
250
292
|
lines.push('')
|
|
293
|
+
|
|
294
|
+
// ── Versione + update notice ───────────────────────────────────────────
|
|
295
|
+
const current = versionInfo?.current ?? this.config.version
|
|
296
|
+
const versionStr = isColorEnabled
|
|
297
|
+
? chalk.dim('version ') + chalk.hex(DIM_BLUE)(current)
|
|
298
|
+
: `version ${current}`
|
|
299
|
+
|
|
300
|
+
if (versionInfo?.hasUpdate && versionInfo.latest) {
|
|
301
|
+
const updateStr = isColorEnabled
|
|
302
|
+
? chalk.yellow('update disponibile: ') +
|
|
303
|
+
chalk.dim(current) + chalk.yellow(' → ') + chalk.green(versionInfo.latest) +
|
|
304
|
+
chalk.dim(' (esegui ') + chalk.hex(LIGHT_ORANGE)('dvmi upgrade') + chalk.dim(')')
|
|
305
|
+
: `update disponibile: ${current} → ${versionInfo.latest} (esegui dvmi upgrade)`
|
|
306
|
+
lines.push(' ' + versionStr + chalk.dim(' · ') + updateStr)
|
|
307
|
+
} else {
|
|
308
|
+
lines.push(' ' + versionStr)
|
|
309
|
+
}
|
|
310
|
+
|
|
251
311
|
lines.push(
|
|
252
312
|
' ' + chalk.dim('Approfondisci:') + ' ' +
|
|
253
313
|
chalk.hex(DIM_BLUE)('dvmi <COMANDO> --help') +
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { resolve, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/** @import { PackageEcosystem } from '../types.js' */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Lock file detection entries in priority order.
|
|
8
|
+
* For Node.js: pnpm > npm > yarn (only highest priority used).
|
|
9
|
+
*/
|
|
10
|
+
const LOCK_FILE_MAP = [
|
|
11
|
+
{
|
|
12
|
+
lockFile: 'pnpm-lock.yaml',
|
|
13
|
+
name: 'pnpm',
|
|
14
|
+
auditCommand: 'pnpm audit --json',
|
|
15
|
+
builtIn: true,
|
|
16
|
+
nodeGroup: true,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
lockFile: 'package-lock.json',
|
|
20
|
+
name: 'npm',
|
|
21
|
+
auditCommand: 'npm audit --json',
|
|
22
|
+
builtIn: true,
|
|
23
|
+
nodeGroup: true,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
lockFile: 'yarn.lock',
|
|
27
|
+
name: 'yarn',
|
|
28
|
+
auditCommand: 'yarn audit --json',
|
|
29
|
+
builtIn: true,
|
|
30
|
+
nodeGroup: true,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
lockFile: 'Pipfile.lock',
|
|
34
|
+
name: 'pip',
|
|
35
|
+
auditCommand: 'pip-audit -f json',
|
|
36
|
+
builtIn: false,
|
|
37
|
+
nodeGroup: false,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
lockFile: 'poetry.lock',
|
|
41
|
+
name: 'pip',
|
|
42
|
+
auditCommand: 'pip-audit -f json',
|
|
43
|
+
builtIn: false,
|
|
44
|
+
nodeGroup: false,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
lockFile: 'requirements.txt',
|
|
48
|
+
name: 'pip',
|
|
49
|
+
auditCommand: 'pip-audit -r requirements.txt -f json',
|
|
50
|
+
builtIn: false,
|
|
51
|
+
nodeGroup: false,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
lockFile: 'Cargo.lock',
|
|
55
|
+
name: 'cargo',
|
|
56
|
+
auditCommand: 'cargo audit --json',
|
|
57
|
+
builtIn: false,
|
|
58
|
+
nodeGroup: false,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
lockFile: 'Gemfile.lock',
|
|
62
|
+
name: 'bundler',
|
|
63
|
+
auditCommand: 'bundle-audit check --format json',
|
|
64
|
+
builtIn: false,
|
|
65
|
+
nodeGroup: false,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
lockFile: 'composer.lock',
|
|
69
|
+
name: 'composer',
|
|
70
|
+
auditCommand: 'composer audit --format json',
|
|
71
|
+
builtIn: true,
|
|
72
|
+
nodeGroup: false,
|
|
73
|
+
},
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Detect package manager ecosystems present in a directory by scanning for lock files.
|
|
78
|
+
* Node.js lock files are de-duplicated: only the highest-priority one is returned.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} [dir] - Directory to scan (defaults to process.cwd())
|
|
81
|
+
* @returns {PackageEcosystem[]}
|
|
82
|
+
*/
|
|
83
|
+
export function detectEcosystems(dir = process.cwd()) {
|
|
84
|
+
const ecosystems = []
|
|
85
|
+
let nodeDetected = false
|
|
86
|
+
|
|
87
|
+
for (const entry of LOCK_FILE_MAP) {
|
|
88
|
+
// Skip lower-priority Node.js lock files if one already detected
|
|
89
|
+
if (entry.nodeGroup && nodeDetected) continue
|
|
90
|
+
|
|
91
|
+
const lockFilePath = resolve(join(dir, entry.lockFile))
|
|
92
|
+
if (existsSync(lockFilePath)) {
|
|
93
|
+
ecosystems.push({
|
|
94
|
+
name: entry.name,
|
|
95
|
+
lockFile: entry.lockFile,
|
|
96
|
+
lockFilePath,
|
|
97
|
+
auditCommand: entry.auditCommand,
|
|
98
|
+
builtIn: entry.builtIn,
|
|
99
|
+
})
|
|
100
|
+
if (entry.nodeGroup) nodeDetected = true
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return ecosystems
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Return the human-readable list of supported ecosystems for display in the "no package manager"
|
|
109
|
+
* error message.
|
|
110
|
+
* @returns {string}
|
|
111
|
+
*/
|
|
112
|
+
export function supportedEcosystemsMessage() {
|
|
113
|
+
return [
|
|
114
|
+
' • Node.js (npm, pnpm, yarn) — requires lock file',
|
|
115
|
+
' • Python (pip-audit) — requires Pipfile.lock, poetry.lock, or requirements.txt',
|
|
116
|
+
' • Rust (cargo-audit) — requires Cargo.lock',
|
|
117
|
+
' • Ruby (bundler-audit) — requires Gemfile.lock',
|
|
118
|
+
' • PHP (composer) — requires composer.lock',
|
|
119
|
+
].join('\n')
|
|
120
|
+
}
|