devvami 1.4.1 → 1.4.2

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.
@@ -520,94 +520,6 @@
520
520
  "trend.js"
521
521
  ]
522
522
  },
523
- "create:repo": {
524
- "aliases": [],
525
- "args": {
526
- "template": {
527
- "description": "Nome del template",
528
- "name": "template",
529
- "required": false
530
- }
531
- },
532
- "description": "Crea nuovo progetto da template GitHub o lista i template disponibili",
533
- "examples": [
534
- "<%= config.bin %> create repo --list",
535
- "<%= config.bin %> create repo --list --search \"lambda\"",
536
- "<%= config.bin %> create repo template-lambda",
537
- "<%= config.bin %> create repo template-lambda --name my-service --dry-run"
538
- ],
539
- "flags": {
540
- "json": {
541
- "description": "Format output as json.",
542
- "helpGroup": "GLOBAL",
543
- "name": "json",
544
- "allowNo": false,
545
- "type": "boolean"
546
- },
547
- "list": {
548
- "description": "Lista template disponibili",
549
- "name": "list",
550
- "allowNo": false,
551
- "type": "boolean"
552
- },
553
- "search": {
554
- "char": "s",
555
- "description": "Cerca in nome e descrizione dei template (case-insensitive)",
556
- "name": "search",
557
- "hasDynamicHelp": false,
558
- "multiple": false,
559
- "type": "option"
560
- },
561
- "name": {
562
- "description": "Nome del nuovo repository",
563
- "name": "name",
564
- "hasDynamicHelp": false,
565
- "multiple": false,
566
- "type": "option"
567
- },
568
- "description": {
569
- "description": "Descrizione del repository",
570
- "name": "description",
571
- "default": "",
572
- "hasDynamicHelp": false,
573
- "multiple": false,
574
- "type": "option"
575
- },
576
- "private": {
577
- "description": "Repository privato (default)",
578
- "name": "private",
579
- "allowNo": false,
580
- "type": "boolean"
581
- },
582
- "public": {
583
- "description": "Repository pubblico",
584
- "name": "public",
585
- "allowNo": false,
586
- "type": "boolean"
587
- },
588
- "dry-run": {
589
- "description": "Preview senza eseguire",
590
- "name": "dry-run",
591
- "allowNo": false,
592
- "type": "boolean"
593
- }
594
- },
595
- "hasDynamicHelp": false,
596
- "hiddenAliases": [],
597
- "id": "create:repo",
598
- "pluginAlias": "devvami",
599
- "pluginName": "devvami",
600
- "pluginType": "core",
601
- "strict": true,
602
- "enableJsonFlag": true,
603
- "isESM": true,
604
- "relativePath": [
605
- "src",
606
- "commands",
607
- "create",
608
- "repo.js"
609
- ]
610
- },
611
523
  "docs:list": {
612
524
  "aliases": [],
613
525
  "args": {},
@@ -813,6 +725,94 @@
813
725
  "search.js"
814
726
  ]
815
727
  },
728
+ "create:repo": {
729
+ "aliases": [],
730
+ "args": {
731
+ "template": {
732
+ "description": "Nome del template",
733
+ "name": "template",
734
+ "required": false
735
+ }
736
+ },
737
+ "description": "Crea nuovo progetto da template GitHub o lista i template disponibili",
738
+ "examples": [
739
+ "<%= config.bin %> create repo --list",
740
+ "<%= config.bin %> create repo --list --search \"lambda\"",
741
+ "<%= config.bin %> create repo template-lambda",
742
+ "<%= config.bin %> create repo template-lambda --name my-service --dry-run"
743
+ ],
744
+ "flags": {
745
+ "json": {
746
+ "description": "Format output as json.",
747
+ "helpGroup": "GLOBAL",
748
+ "name": "json",
749
+ "allowNo": false,
750
+ "type": "boolean"
751
+ },
752
+ "list": {
753
+ "description": "Lista template disponibili",
754
+ "name": "list",
755
+ "allowNo": false,
756
+ "type": "boolean"
757
+ },
758
+ "search": {
759
+ "char": "s",
760
+ "description": "Cerca in nome e descrizione dei template (case-insensitive)",
761
+ "name": "search",
762
+ "hasDynamicHelp": false,
763
+ "multiple": false,
764
+ "type": "option"
765
+ },
766
+ "name": {
767
+ "description": "Nome del nuovo repository",
768
+ "name": "name",
769
+ "hasDynamicHelp": false,
770
+ "multiple": false,
771
+ "type": "option"
772
+ },
773
+ "description": {
774
+ "description": "Descrizione del repository",
775
+ "name": "description",
776
+ "default": "",
777
+ "hasDynamicHelp": false,
778
+ "multiple": false,
779
+ "type": "option"
780
+ },
781
+ "private": {
782
+ "description": "Repository privato (default)",
783
+ "name": "private",
784
+ "allowNo": false,
785
+ "type": "boolean"
786
+ },
787
+ "public": {
788
+ "description": "Repository pubblico",
789
+ "name": "public",
790
+ "allowNo": false,
791
+ "type": "boolean"
792
+ },
793
+ "dry-run": {
794
+ "description": "Preview senza eseguire",
795
+ "name": "dry-run",
796
+ "allowNo": false,
797
+ "type": "boolean"
798
+ }
799
+ },
800
+ "hasDynamicHelp": false,
801
+ "hiddenAliases": [],
802
+ "id": "create:repo",
803
+ "pluginAlias": "devvami",
804
+ "pluginName": "devvami",
805
+ "pluginType": "core",
806
+ "strict": true,
807
+ "enableJsonFlag": true,
808
+ "isESM": true,
809
+ "relativePath": [
810
+ "src",
811
+ "commands",
812
+ "create",
813
+ "repo.js"
814
+ ]
815
+ },
816
816
  "dotfiles:add": {
817
817
  "aliases": [],
818
818
  "args": {
@@ -2120,5 +2120,5 @@
2120
2120
  ]
2121
2121
  }
2122
2122
  },
2123
- "version": "1.4.1"
2123
+ "version": "1.4.2"
2124
2124
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "devvami",
3
3
  "description": "DevEx CLI for developers and teams — manage repos, PRs, pipelines, tasks, and costs from the terminal",
4
- "version": "1.4.1",
4
+ "version": "1.4.2",
5
5
  "author": "",
6
6
  "type": "module",
7
7
  "bin": {
@@ -4,7 +4,20 @@ import ora from 'ora'
4
4
  import chalk from 'chalk'
5
5
  import { detectEcosystems, supportedEcosystemsMessage } from '../../services/audit-detector.js'
6
6
  import { runAudit, summarizeFindings, filterBySeverity } from '../../services/audit-runner.js'
7
- import { formatFindingsTable, formatScanSummary, formatMarkdownReport } from '../../formatters/vuln.js'
7
+ import { formatFindingsTable, formatScanSummary, formatMarkdownReport, truncate, colorSeverity } from '../../formatters/vuln.js'
8
+ import { getCveDetail } from '../../services/nvd.js'
9
+ import { startInteractiveTable } from '../../utils/tui/navigable-table.js'
10
+
11
+ // Minimum terminal rows required to show the interactive TUI (same threshold as vuln search)
12
+ const MIN_TTY_ROWS = 6
13
+
14
+ // Column widths for the navigable table (match the static findings table)
15
+ const COL_WIDTHS = {
16
+ pkg: 20,
17
+ version: 12,
18
+ severity: 10,
19
+ cve: 20,
20
+ }
8
21
 
9
22
  export default class VulnScan extends Command {
10
23
  static description = 'Scan the current directory for known vulnerabilities in dependencies'
@@ -112,7 +125,7 @@ export default class VulnScan extends Command {
112
125
  errors,
113
126
  }
114
127
 
115
- // Write report if requested
128
+ // Write report if requested (always, regardless of TTY mode)
116
129
  if (report) {
117
130
  const markdown = formatMarkdownReport(result)
118
131
  await writeFile(report, markdown, 'utf8')
@@ -128,17 +141,58 @@ export default class VulnScan extends Command {
128
141
  return result
129
142
  }
130
143
 
131
- if (filteredFindings.length > 0) {
132
- this.log(chalk.bold(` Findings (${filteredFindings.length} ${filteredFindings.length === 1 ? 'vulnerability' : 'vulnerabilities'})`))
133
- this.log('')
134
- this.log(formatFindingsTable(filteredFindings))
135
- this.log('')
136
- this.log(chalk.bold(' Summary'))
137
- this.log(formatScanSummary(summary))
138
- this.log('')
139
- this.log(chalk.yellow(` ⚠ ${filteredFindings.length} ${filteredFindings.length === 1 ? 'vulnerability' : 'vulnerabilities'} found. Run \`dvmi vuln detail <CVE-ID>\` for details.`))
144
+ // ── TTY interactive table ──────────────────────────────────────────────────
145
+ // In a real TTY with enough rows and at least one finding, replace the static
146
+ // table with the navigable TUI (same experience as `dvmi vuln search`).
147
+ const ttyRows = process.stdout.rows ?? 0
148
+ const useTUI = process.stdout.isTTY && filteredFindings.length > 0 && ttyRows >= MIN_TTY_ROWS
149
+
150
+ if (useTUI) {
151
+ const count = filteredFindings.length
152
+ const label = count === 1 ? 'finding' : 'findings'
153
+ const heading = `Vulnerability Scan: ${count} ${label}`
154
+
155
+ const termCols = process.stdout.columns || 80
156
+ // Title width: whatever is left after Package + Version + Severity + CVE + separators
157
+ const fixedCols = COL_WIDTHS.pkg + COL_WIDTHS.version + COL_WIDTHS.severity + COL_WIDTHS.cve
158
+ const separators = 5 * 2 // 5 gaps between 5 columns
159
+ const titleWidth = Math.max(15, Math.min(50, termCols - fixedCols - separators))
160
+
161
+ const rows = filteredFindings.map((f) => ({
162
+ id: f.cveId ?? null,
163
+ pkg: f.package,
164
+ version: f.installedVersion ?? '—',
165
+ severity: f.severity,
166
+ cve: f.cveId ?? '—',
167
+ title: truncate(f.title ?? '—', titleWidth),
168
+ advisoryUrl: f.advisoryUrl ?? null,
169
+ }))
170
+
171
+ /** @type {import('../../utils/tui/navigable-table.js').TableColumnDef[]} */
172
+ const columns = [
173
+ { header: 'Package', key: 'pkg', width: COL_WIDTHS.pkg },
174
+ { header: 'Version', key: 'version', width: COL_WIDTHS.version },
175
+ { header: 'Severity', key: 'severity', width: COL_WIDTHS.severity, colorize: (v) => colorSeverity(v) },
176
+ { header: 'CVE', key: 'cve', width: COL_WIDTHS.cve, colorize: (v) => (v !== '—' ? chalk.cyan(v) : chalk.gray(v)) },
177
+ { header: 'Title', key: 'title', width: titleWidth },
178
+ ]
179
+
180
+ await startInteractiveTable(rows, columns, heading, filteredFindings.length, getCveDetail)
181
+ } else {
182
+ // Non-TTY fallback: static table + summary (unchanged from pre-TUI behaviour)
183
+ if (filteredFindings.length > 0) {
184
+ this.log(chalk.bold(` Findings (${filteredFindings.length} ${filteredFindings.length === 1 ? 'vulnerability' : 'vulnerabilities'})`))
185
+ this.log('')
186
+ this.log(formatFindingsTable(filteredFindings))
187
+ this.log('')
188
+ this.log(chalk.bold(' Summary'))
189
+ this.log(formatScanSummary(summary))
190
+ this.log('')
191
+ this.log(chalk.yellow(` ⚠ ${filteredFindings.length} ${filteredFindings.length === 1 ? 'vulnerability' : 'vulnerabilities'} found. Run \`dvmi vuln detail <CVE-ID>\` for details.`))
192
+ }
140
193
  }
141
194
 
195
+ // Always print audit errors (e.g. tool not installed) after findings/TUI
142
196
  if (errors.length > 0) {
143
197
  this.log('')
144
198
  for (const err of errors) {
@@ -146,6 +200,7 @@ export default class VulnScan extends Command {
146
200
  }
147
201
  }
148
202
 
203
+ // Preserve exit code semantics: exit 1 when vulns found (unless --no-fail)
149
204
  if (filteredFindings.length > 0 && !noFail) {
150
205
  this.exit(1)
151
206
  }
@@ -360,8 +360,16 @@ export async function startInteractiveTable(rows, columns, heading, totalResults
360
360
  * @returns {Promise<void>}
361
361
  */
362
362
  async function openDetail() {
363
- const cveId = state.rows[state.selectedIndex]?.id
363
+ const row = state.rows[state.selectedIndex]
364
+ const cveId = row?.id
365
+
364
366
  if (!cveId) {
367
+ // No CVE ID — check for an advisory URL (e.g. npm/pnpm advisory findings).
368
+ // Open it in the browser and stay in table view rather than showing a modal.
369
+ const advisoryUrl = row?.advisoryUrl
370
+ if (advisoryUrl) {
371
+ await openBrowser(String(advisoryUrl))
372
+ }
365
373
  state = { ...state, currentView: 'table' }
366
374
  process.stdout.write(buildTableScreen(state))
367
375
  return