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,143 @@
1
+ import {Command, Flags} from '@oclif/core'
2
+ import ora from 'ora'
3
+
4
+ import {scanEnvironments, computeCategoryCounts} from '../../services/ai-env-scanner.js'
5
+ import {
6
+ loadAIConfig,
7
+ addEntry,
8
+ updateEntry,
9
+ deactivateEntry,
10
+ activateEntry,
11
+ deleteEntry,
12
+ } from '../../services/ai-config-store.js'
13
+ import {deployEntry, undeployEntry, reconcileOnScan} from '../../services/ai-env-deployer.js'
14
+ import {loadConfig} from '../../services/config.js'
15
+ import {formatEnvironmentsTable, formatCategoriesTable} from '../../formatters/ai-config.js'
16
+ import {startTabTUI} from '../../utils/tui/tab-tui.js'
17
+ import {DvmiError} from '../../utils/errors.js'
18
+
19
+ /** @import { DetectedEnvironment, CategoryEntry } from '../../types.js' */
20
+
21
+ export default class SyncConfigAi extends Command {
22
+ static description = 'Manage AI coding tool configurations across environments via TUI'
23
+
24
+ static examples = ['<%= config.bin %> sync-config-ai', '<%= config.bin %> sync-config-ai --json']
25
+
26
+ static enableJsonFlag = true
27
+
28
+ static flags = {
29
+ help: Flags.help({char: 'h'}),
30
+ }
31
+
32
+ async run() {
33
+ const {flags} = await this.parse(SyncConfigAi)
34
+ const isJson = flags.json
35
+
36
+ // ── Scan environments ────────────────────────────────────────────────────
37
+ const spinner = isJson ? null : ora('Scanning AI coding environments…').start()
38
+ let detectedEnvs
39
+
40
+ try {
41
+ detectedEnvs = scanEnvironments(process.cwd())
42
+ } catch (err) {
43
+ spinner?.fail('Scan failed')
44
+ throw new DvmiError(
45
+ 'Failed to scan AI coding environments',
46
+ err instanceof Error ? err.message : 'Check filesystem permissions',
47
+ )
48
+ }
49
+
50
+ // ── Load AI config store ─────────────────────────────────────────────────
51
+ let store
52
+ try {
53
+ store = await loadAIConfig()
54
+ } catch {
55
+ spinner?.fail('Failed to load AI config')
56
+ throw new DvmiError(
57
+ 'AI config file is corrupted',
58
+ 'Delete `~/.config/dvmi/ai-config.json` to reset, or fix the JSON manually',
59
+ )
60
+ }
61
+
62
+ // ── Reconcile: re-deploy/undeploy based on current environment detection ─
63
+ if (detectedEnvs.length > 0 && store.entries.length > 0) {
64
+ try {
65
+ await reconcileOnScan(store.entries, detectedEnvs, process.cwd())
66
+ // Reload store after reconciliation in case it mutated entries
67
+ store = await loadAIConfig()
68
+ } catch {
69
+ // Reconciliation errors are non-fatal — continue with current state
70
+ }
71
+ }
72
+
73
+ // ── Compute per-environment category counts ──────────────────────────────
74
+ for (const env of detectedEnvs) {
75
+ env.counts = computeCategoryCounts(env.id, store.entries)
76
+ }
77
+
78
+ spinner?.stop()
79
+
80
+ // ── JSON mode ────────────────────────────────────────────────────────────
81
+ if (isJson) {
82
+ const categories = {
83
+ mcp: store.entries.filter((e) => e.type === 'mcp'),
84
+ command: store.entries.filter((e) => e.type === 'command'),
85
+ skill: store.entries.filter((e) => e.type === 'skill'),
86
+ agent: store.entries.filter((e) => e.type === 'agent'),
87
+ }
88
+ return {environments: detectedEnvs, categories}
89
+ }
90
+
91
+ // ── Check chezmoi config ─────────────────────────────────────────────────
92
+ let chezmoiEnabled = false
93
+ try {
94
+ const cliConfig = await loadConfig()
95
+ chezmoiEnabled = cliConfig.dotfiles?.enabled === true
96
+ } catch {
97
+ // Non-fatal — chezmoi tip will show
98
+ }
99
+
100
+ // ── Launch TUI ───────────────────────────────────────────────────────────
101
+ await startTabTUI({
102
+ envs: detectedEnvs,
103
+ entries: store.entries,
104
+ chezmoiEnabled,
105
+ formatEnvs: formatEnvironmentsTable,
106
+ formatCats: formatCategoriesTable,
107
+ refreshEntries: async () => {
108
+ const s = await loadAIConfig()
109
+ return s.entries
110
+ },
111
+ onAction: async (action) => {
112
+ // Reload current entries for each action to avoid stale data
113
+ const currentStore = await loadAIConfig()
114
+
115
+ if (action.type === 'create') {
116
+ const created = await addEntry({
117
+ name: action.values.name,
118
+ type: action.tabKey || 'mcp',
119
+ environments: action.values.environments || [],
120
+ params: action.values,
121
+ })
122
+ await deployEntry(created, detectedEnvs, process.cwd())
123
+ } else if (action.type === 'edit') {
124
+ const updated = await updateEntry(action.id, {params: action.values})
125
+ await deployEntry(updated, detectedEnvs, process.cwd())
126
+ } else if (action.type === 'delete') {
127
+ await deleteEntry(action.id)
128
+ await undeployEntry(
129
+ currentStore.entries.find((e) => e.id === action.id),
130
+ detectedEnvs,
131
+ process.cwd(),
132
+ )
133
+ } else if (action.type === 'deactivate') {
134
+ const entry = await deactivateEntry(action.id)
135
+ await undeployEntry(entry, detectedEnvs, process.cwd())
136
+ } else if (action.type === 'activate') {
137
+ const entry = await activateEntry(action.id)
138
+ await deployEntry(entry, detectedEnvs, process.cwd())
139
+ }
140
+ },
141
+ })
142
+ }
143
+ }
@@ -1,9 +1,9 @@
1
- import { Command, Flags } from '@oclif/core'
1
+ import {Command, Flags} from '@oclif/core'
2
2
  import chalk from 'chalk'
3
3
  import ora from 'ora'
4
- import { getTasks, getTasksByList, isAuthenticated } from '../../services/clickup.js'
5
- import { loadConfig } from '../../services/config.js'
6
- import { renderTable } from '../../formatters/table.js'
4
+ import {getTasks, getTasksByList, isAuthenticated} from '../../services/clickup.js'
5
+ import {loadConfig} from '../../services/config.js'
6
+ import {renderTable} from '../../formatters/table.js'
7
7
 
8
8
  export default class TasksAssigned extends Command {
9
9
  static description = 'Task ClickUp assegnati a te (alias di tasks list)'
@@ -19,7 +19,7 @@ export default class TasksAssigned extends Command {
19
19
  static enableJsonFlag = true
20
20
 
21
21
  static flags = {
22
- status: Flags.string({ description: 'Filtra per status (open, in_progress, done)' }),
22
+ status: Flags.string({description: 'Filtra per status (open, in_progress, done)'}),
23
23
  search: Flags.string({
24
24
  char: 's',
25
25
  description: 'Cerca nel titolo del task (case-insensitive)',
@@ -30,7 +30,7 @@ export default class TasksAssigned extends Command {
30
30
  }
31
31
 
32
32
  async run() {
33
- const { flags } = await this.parse(TasksAssigned)
33
+ const {flags} = await this.parse(TasksAssigned)
34
34
  const isJson = flags.json
35
35
  const config = await loadConfig()
36
36
 
@@ -45,7 +45,9 @@ export default class TasksAssigned extends Command {
45
45
  this.error('ClickUp team ID not configured. Run `dvmi init` to configure ClickUp.')
46
46
  }
47
47
 
48
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching tasks...') }).start()
48
+ const spinner = isJson
49
+ ? null
50
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching tasks...')}).start()
49
51
 
50
52
  /** @param {number} count */
51
53
  const onProgress = (count) => {
@@ -54,40 +56,38 @@ export default class TasksAssigned extends Command {
54
56
 
55
57
  let tasks
56
58
  if (flags['list-id']) {
57
- tasks = await getTasksByList(flags['list-id'], { status: flags.status }, onProgress).catch((err) => {
59
+ tasks = await getTasksByList(flags['list-id'], {status: flags.status}, onProgress).catch((err) => {
58
60
  spinner?.stop()
59
61
  this.error(err.message)
60
62
  })
61
63
  } else {
62
- tasks = await getTasks(/** @type {string} */ (teamId), { status: flags.status }, onProgress)
64
+ tasks = await getTasks(/** @type {string} */ (teamId), {status: flags.status}, onProgress)
63
65
  }
64
66
  spinner?.stop()
65
67
 
66
68
  // Apply search filter
67
69
  const searchQuery = flags.search?.toLowerCase()
68
- const filtered = searchQuery
69
- ? tasks.filter((t) => t.name.toLowerCase().includes(searchQuery))
70
- : tasks
70
+ const filtered = searchQuery ? tasks.filter((t) => t.name.toLowerCase().includes(searchQuery)) : tasks
71
71
 
72
- if (isJson) return { tasks: filtered }
72
+ if (isJson) return {tasks: filtered}
73
73
 
74
74
  if (tasks.length === 0) {
75
75
  this.log(chalk.dim('No tasks assigned to you.'))
76
- return { tasks: [] }
76
+ return {tasks: []}
77
77
  }
78
78
 
79
79
  if (filtered.length === 0) {
80
80
  this.log(chalk.dim('No tasks matching filters.'))
81
- return { tasks: [] }
81
+ return {tasks: []}
82
82
  }
83
83
 
84
84
  // Priority label + color
85
85
  const priorityLabel = (p) => ['', 'URGENT', 'HIGH', 'NORMAL', 'LOW'][p] ?? String(p)
86
86
  const priorityColor = (label) => {
87
87
  if (label === 'URGENT') return chalk.red.bold(label)
88
- if (label === 'HIGH') return chalk.yellow(label)
88
+ if (label === 'HIGH') return chalk.yellow(label)
89
89
  if (label === 'NORMAL') return chalk.white(label)
90
- if (label === 'LOW') return chalk.dim(label)
90
+ if (label === 'LOW') return chalk.dim(label)
91
91
  return label
92
92
  }
93
93
 
@@ -96,7 +96,7 @@ export default class TasksAssigned extends Command {
96
96
  const s = status.toLowerCase()
97
97
  if (s.includes('done') || s.includes('complet') || s.includes('closed')) return chalk.green(status)
98
98
  if (s.includes('progress') || s.includes('active') || s.includes('open')) return chalk.cyan(status)
99
- if (s.includes('block') || s.includes('review') || s.includes('wait')) return chalk.yellow(status)
99
+ if (s.includes('block') || s.includes('review') || s.includes('wait')) return chalk.yellow(status)
100
100
  return chalk.dim(status)
101
101
  }
102
102
 
@@ -105,27 +105,37 @@ export default class TasksAssigned extends Command {
105
105
  flags.status && chalk.dim(`status: ${chalk.white(flags.status)}`),
106
106
  flags.search && chalk.dim(`search: ${chalk.white(`"${flags.search}"`)}`),
107
107
  flags['list-id'] && chalk.dim(`list-id: ${chalk.white(flags['list-id'])}`),
108
- ].filter(Boolean).join(chalk.dim(' · '))
108
+ ]
109
+ .filter(Boolean)
110
+ .join(chalk.dim(' · '))
109
111
 
110
112
  this.log(
111
113
  chalk.bold('\nYour assigned tasks') +
112
- (filterInfo ? chalk.dim(' — ') + filterInfo : '') +
113
- chalk.dim(` (${filtered.length}${filtered.length < tasks.length ? `/${tasks.length}` : ''})`) +
114
- '\n',
114
+ (filterInfo ? chalk.dim(' — ') + filterInfo : '') +
115
+ chalk.dim(` (${filtered.length}${filtered.length < tasks.length ? `/${tasks.length}` : ''})`) +
116
+ '\n',
115
117
  )
116
118
 
117
- this.log(renderTable(filtered, [
118
- { header: 'ID', key: 'id', width: 10 },
119
- { header: 'Link', key: 'url', width: 42, format: (v) => v ?? '—' },
120
- { header: 'Priority', key: 'priority', width: 8, format: (v) => priorityLabel(Number(v)), colorize: priorityColor },
121
- { header: 'Status', key: 'status', width: 15, colorize: statusColor },
122
- { header: 'Due', key: 'dueDate', width: 12, format: (v) => v ?? '—' },
123
- { header: 'Lista', key: 'listName', width: 20, format: (v) => v ?? '—' },
124
- { header: 'Cartella', key: 'folderName', width: 20, format: (v) => v ?? '—' },
125
- { header: 'Description', key: 'name', width: 55 },
126
- ]))
119
+ this.log(
120
+ renderTable(filtered, [
121
+ {header: 'ID', key: 'id', width: 10},
122
+ {header: 'Link', key: 'url', width: 42, format: (v) => v ?? '—'},
123
+ {
124
+ header: 'Priority',
125
+ key: 'priority',
126
+ width: 8,
127
+ format: (v) => priorityLabel(Number(v)),
128
+ colorize: priorityColor,
129
+ },
130
+ {header: 'Status', key: 'status', width: 15, colorize: statusColor},
131
+ {header: 'Due', key: 'dueDate', width: 12, format: (v) => v ?? '—'},
132
+ {header: 'Lista', key: 'listName', width: 20, format: (v) => v ?? '—'},
133
+ {header: 'Cartella', key: 'folderName', width: 20, format: (v) => v ?? '—'},
134
+ {header: 'Description', key: 'name', width: 55},
135
+ ]),
136
+ )
127
137
 
128
138
  this.log('')
129
- return { tasks: filtered }
139
+ return {tasks: filtered}
130
140
  }
131
141
  }
@@ -1,9 +1,9 @@
1
- import { Command, Flags } from '@oclif/core'
1
+ import {Command, Flags} from '@oclif/core'
2
2
  import chalk from 'chalk'
3
3
  import ora from 'ora'
4
- import { getTasks, getTasksByList, isAuthenticated } from '../../services/clickup.js'
5
- import { loadConfig } from '../../services/config.js'
6
- import { renderTable } from '../../formatters/table.js'
4
+ import {getTasks, getTasksByList, isAuthenticated} from '../../services/clickup.js'
5
+ import {loadConfig} from '../../services/config.js'
6
+ import {renderTable} from '../../formatters/table.js'
7
7
 
8
8
  export default class TasksList extends Command {
9
9
  static description = 'Task ClickUp assegnati a te'
@@ -21,7 +21,7 @@ export default class TasksList extends Command {
21
21
  static enableJsonFlag = true
22
22
 
23
23
  static flags = {
24
- status: Flags.string({ description: 'Filtra per status (open, in_progress, done)' }),
24
+ status: Flags.string({description: 'Filtra per status (open, in_progress, done)'}),
25
25
  search: Flags.string({
26
26
  char: 's',
27
27
  description: 'Cerca nel titolo del task (case-insensitive)',
@@ -32,7 +32,7 @@ export default class TasksList extends Command {
32
32
  }
33
33
 
34
34
  async run() {
35
- const { flags } = await this.parse(TasksList)
35
+ const {flags} = await this.parse(TasksList)
36
36
  const isJson = flags.json
37
37
  const config = await loadConfig()
38
38
 
@@ -47,7 +47,9 @@ export default class TasksList extends Command {
47
47
  this.error('ClickUp team ID not configured. Run `dvmi init` to configure ClickUp.')
48
48
  }
49
49
 
50
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching tasks...') }).start()
50
+ const spinner = isJson
51
+ ? null
52
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching tasks...')}).start()
51
53
 
52
54
  /** @param {number} count */
53
55
  const onProgress = (count) => {
@@ -56,40 +58,38 @@ export default class TasksList extends Command {
56
58
 
57
59
  let tasks
58
60
  if (flags['list-id']) {
59
- tasks = await getTasksByList(flags['list-id'], { status: flags.status }, onProgress).catch((err) => {
61
+ tasks = await getTasksByList(flags['list-id'], {status: flags.status}, onProgress).catch((err) => {
60
62
  spinner?.stop()
61
63
  this.error(err.message)
62
64
  })
63
65
  } else {
64
- tasks = await getTasks(/** @type {string} */ (teamId), { status: flags.status }, onProgress)
66
+ tasks = await getTasks(/** @type {string} */ (teamId), {status: flags.status}, onProgress)
65
67
  }
66
68
  spinner?.stop()
67
69
 
68
70
  // Apply search filter
69
71
  const searchQuery = flags.search?.toLowerCase()
70
- const filtered = searchQuery
71
- ? tasks.filter((t) => t.name.toLowerCase().includes(searchQuery))
72
- : tasks
72
+ const filtered = searchQuery ? tasks.filter((t) => t.name.toLowerCase().includes(searchQuery)) : tasks
73
73
 
74
- if (isJson) return { tasks: filtered }
74
+ if (isJson) return {tasks: filtered}
75
75
 
76
76
  if (tasks.length === 0) {
77
77
  this.log(chalk.dim('No tasks assigned to you.'))
78
- return { tasks: [] }
78
+ return {tasks: []}
79
79
  }
80
80
 
81
81
  if (filtered.length === 0) {
82
82
  this.log(chalk.dim(`No tasks matching filters.`))
83
- return { tasks: [] }
83
+ return {tasks: []}
84
84
  }
85
85
 
86
86
  // Priority label + color
87
87
  const priorityLabel = (p) => ['', 'URGENT', 'HIGH', 'NORMAL', 'LOW'][p] ?? String(p)
88
88
  const priorityColor = (label) => {
89
89
  if (label === 'URGENT') return chalk.red.bold(label)
90
- if (label === 'HIGH') return chalk.yellow(label)
90
+ if (label === 'HIGH') return chalk.yellow(label)
91
91
  if (label === 'NORMAL') return chalk.white(label)
92
- if (label === 'LOW') return chalk.dim(label)
92
+ if (label === 'LOW') return chalk.dim(label)
93
93
  return label
94
94
  }
95
95
 
@@ -98,7 +98,7 @@ export default class TasksList extends Command {
98
98
  const s = status.toLowerCase()
99
99
  if (s.includes('done') || s.includes('complet') || s.includes('closed')) return chalk.green(status)
100
100
  if (s.includes('progress') || s.includes('active') || s.includes('open')) return chalk.cyan(status)
101
- if (s.includes('block') || s.includes('review') || s.includes('wait')) return chalk.yellow(status)
101
+ if (s.includes('block') || s.includes('review') || s.includes('wait')) return chalk.yellow(status)
102
102
  return chalk.dim(status)
103
103
  }
104
104
 
@@ -107,27 +107,37 @@ export default class TasksList extends Command {
107
107
  flags.status && chalk.dim(`status: ${chalk.white(flags.status)}`),
108
108
  flags.search && chalk.dim(`search: ${chalk.white(`"${flags.search}"`)}`),
109
109
  flags['list-id'] && chalk.dim(`list-id: ${chalk.white(flags['list-id'])}`),
110
- ].filter(Boolean).join(chalk.dim(' · '))
110
+ ]
111
+ .filter(Boolean)
112
+ .join(chalk.dim(' · '))
111
113
 
112
114
  this.log(
113
115
  chalk.bold('\nYour tasks') +
114
- (filterInfo ? chalk.dim(' — ') + filterInfo : '') +
115
- chalk.dim(` (${filtered.length}${filtered.length < tasks.length ? `/${tasks.length}` : ''})`) +
116
- '\n',
116
+ (filterInfo ? chalk.dim(' — ') + filterInfo : '') +
117
+ chalk.dim(` (${filtered.length}${filtered.length < tasks.length ? `/${tasks.length}` : ''})`) +
118
+ '\n',
117
119
  )
118
120
 
119
- this.log(renderTable(filtered, [
120
- { header: 'ID', key: 'id', width: 10 },
121
- { header: 'Link', key: 'url', width: 42, format: (v) => v ?? '—' },
122
- { header: 'Priority', key: 'priority', width: 8, format: (v) => priorityLabel(Number(v)), colorize: priorityColor },
123
- { header: 'Status', key: 'status', width: 15, colorize: statusColor },
124
- { header: 'Due', key: 'dueDate', width: 12, format: (v) => v ?? '—' },
125
- { header: 'Lista', key: 'listName', width: 20, format: (v) => v ?? '—' },
126
- { header: 'Cartella', key: 'folderName', width: 20, format: (v) => v ?? '—' },
127
- { header: 'Description', key: 'name', width: 55 },
128
- ]))
121
+ this.log(
122
+ renderTable(filtered, [
123
+ {header: 'ID', key: 'id', width: 10},
124
+ {header: 'Link', key: 'url', width: 42, format: (v) => v ?? '—'},
125
+ {
126
+ header: 'Priority',
127
+ key: 'priority',
128
+ width: 8,
129
+ format: (v) => priorityLabel(Number(v)),
130
+ colorize: priorityColor,
131
+ },
132
+ {header: 'Status', key: 'status', width: 15, colorize: statusColor},
133
+ {header: 'Due', key: 'dueDate', width: 12, format: (v) => v ?? '—'},
134
+ {header: 'Lista', key: 'listName', width: 20, format: (v) => v ?? '—'},
135
+ {header: 'Cartella', key: 'folderName', width: 20, format: (v) => v ?? '—'},
136
+ {header: 'Description', key: 'name', width: 55},
137
+ ]),
138
+ )
129
139
 
130
140
  this.log('')
131
- return { tasks: filtered }
141
+ return {tasks: filtered}
132
142
  }
133
143
  }
@@ -1,9 +1,9 @@
1
- import { Command } from '@oclif/core'
1
+ import {Command} from '@oclif/core'
2
2
  import chalk from 'chalk'
3
3
  import ora from 'ora'
4
- import { getTasksToday, isAuthenticated } from '../../services/clickup.js'
5
- import { loadConfig } from '../../services/config.js'
6
- import { renderTable } from '../../formatters/table.js'
4
+ import {getTasksToday, isAuthenticated} from '../../services/clickup.js'
5
+ import {loadConfig} from '../../services/config.js'
6
+ import {renderTable} from '../../formatters/table.js'
7
7
 
8
8
  /**
9
9
  * Return today's date as a local YYYY-MM-DD string.
@@ -15,17 +15,15 @@ function localTodayString() {
15
15
  }
16
16
 
17
17
  export default class TasksToday extends Command {
18
- static description = 'Task in lavorazione oggi: data odierna nel range [startDate, dueDate]. Include task scaduti non conclusi.'
18
+ static description =
19
+ 'Task in lavorazione oggi: data odierna nel range [startDate, dueDate]. Include task scaduti non conclusi.'
19
20
 
20
- static examples = [
21
- '<%= config.bin %> tasks today',
22
- '<%= config.bin %> tasks today --json',
23
- ]
21
+ static examples = ['<%= config.bin %> tasks today', '<%= config.bin %> tasks today --json']
24
22
 
25
23
  static enableJsonFlag = true
26
24
 
27
25
  async run() {
28
- const { flags } = await this.parse(TasksToday)
26
+ const {flags} = await this.parse(TasksToday)
29
27
  const isJson = flags.json
30
28
  const config = await loadConfig()
31
29
 
@@ -36,38 +34,42 @@ export default class TasksToday extends Command {
36
34
  const teamId = config.clickup?.teamId
37
35
  if (!teamId) this.error('ClickUp team ID not configured. Run `dvmi init` to configure ClickUp.')
38
36
 
39
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching today\'s tasks...') }).start()
37
+ const spinner = isJson
38
+ ? null
39
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')("Fetching today's tasks...")}).start()
40
40
  const tasks = await getTasksToday(teamId)
41
41
  spinner?.stop()
42
42
 
43
- if (isJson) return { tasks }
43
+ if (isJson) return {tasks}
44
44
 
45
45
  if (tasks.length === 0) {
46
46
  this.log(chalk.dim('No tasks for today.'))
47
47
  this.log(chalk.dim('Check `dvmi tasks list` for all assigned tasks.'))
48
- return { tasks: [] }
48
+ return {tasks: []}
49
49
  }
50
50
 
51
51
  const today = localTodayString()
52
52
 
53
- this.log(chalk.bold('\nToday\'s tasks:\n'))
54
- this.log(renderTable(tasks, [
55
- { header: 'Title', key: 'name', width: 45 },
56
- { header: 'Status', key: 'status', width: 15 },
57
- {
58
- header: 'Due',
59
- key: 'dueDate',
60
- width: 12,
61
- format: (v) => v ?? '—',
62
- colorize: (v) => {
63
- if (!v) return chalk.dim('—')
64
- if (v < today) return chalk.red.bold(v)
65
- return v
53
+ this.log(chalk.bold("\nToday's tasks:\n"))
54
+ this.log(
55
+ renderTable(tasks, [
56
+ {header: 'Title', key: 'name', width: 45},
57
+ {header: 'Status', key: 'status', width: 15},
58
+ {
59
+ header: 'Due',
60
+ key: 'dueDate',
61
+ width: 12,
62
+ format: (v) => v ?? '—',
63
+ colorize: (v) => {
64
+ if (!v) return chalk.dim('—')
65
+ if (v < today) return chalk.red.bold(v)
66
+ return v
67
+ },
66
68
  },
67
- },
68
- { header: 'Link', key: 'url', format: (v) => v ?? '—' },
69
- ]))
69
+ {header: 'Link', key: 'url', format: (v) => v ?? '—'},
70
+ ]),
71
+ )
70
72
 
71
- return { tasks }
73
+ return {tasks}
72
74
  }
73
75
  }
@@ -1,25 +1,24 @@
1
- import { Command } from '@oclif/core'
1
+ import {Command} from '@oclif/core'
2
2
  import chalk from 'chalk'
3
3
  import ora from 'ora'
4
- import { checkForUpdate } from '../services/version-check.js'
5
- import { exec } from '../services/shell.js'
4
+ import {checkForUpdate} from '../services/version-check.js'
5
+ import {exec} from '../services/shell.js'
6
6
 
7
7
  export default class Upgrade extends Command {
8
- static description = 'Aggiorna la CLI all\'ultima versione disponibile'
8
+ static description = "Aggiorna la CLI all'ultima versione disponibile"
9
9
 
10
- static examples = [
11
- '<%= config.bin %> upgrade',
12
- '<%= config.bin %> upgrade --json',
13
- ]
10
+ static examples = ['<%= config.bin %> upgrade', '<%= config.bin %> upgrade --json']
14
11
 
15
12
  static enableJsonFlag = true
16
13
 
17
14
  async run() {
18
- const { flags } = await this.parse(Upgrade)
15
+ const {flags} = await this.parse(Upgrade)
19
16
  const isJson = flags.json
20
17
 
21
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking for updates...') }).start()
22
- const { hasUpdate, current, latest } = await checkForUpdate({ force: true })
18
+ const spinner = isJson
19
+ ? null
20
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking for updates...')}).start()
21
+ const {hasUpdate, current, latest} = await checkForUpdate({force: true})
23
22
  spinner?.stop()
24
23
 
25
24
  // Guard against malformed version strings from the GitHub Releases API
@@ -29,7 +28,7 @@ export default class Upgrade extends Command {
29
28
 
30
29
  if (!hasUpdate) {
31
30
  const msg = `You're already on the latest version (${current})`
32
- if (isJson) return { currentVersion: current, latestVersion: latest, updated: false }
31
+ if (isJson) return {currentVersion: current, latestVersion: latest, updated: false}
33
32
  this.log(chalk.green('✓') + ' ' + msg)
34
33
  return
35
34
  }
@@ -38,17 +37,19 @@ export default class Upgrade extends Command {
38
37
  this.log(`Updating from ${chalk.yellow(current)} to ${chalk.green(latest)}`)
39
38
  }
40
39
 
41
- const updateSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Installing update...') }).start()
42
- // Non passare --registry globale: verrebbe usato anche per le dipendenze.
43
- // ~/.npmrc ha già devvami:registry per il solo scope corretto.
44
- const result = await exec('npm', ['install', '-g', `devvami@${latest}`])
40
+ const updateSpinner = isJson
41
+ ? null
42
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Installing update...')}).start()
43
+ // Non passare --registry globale: verrebbe usato anche per le dipendenze.
44
+ // ~/.npmrc ha già devvami:registry per il solo scope corretto.
45
+ const result = await exec('npm', ['install', '-g', `devvami@${latest}`])
45
46
  if (result.exitCode !== 0) {
46
47
  updateSpinner?.fail('Update failed')
47
48
  this.error(`Update failed: ${result.stderr}`)
48
49
  }
49
50
  updateSpinner?.succeed(`Updated to ${latest}`)
50
51
 
51
- const response = { currentVersion: current, latestVersion: latest, updated: true }
52
+ const response = {currentVersion: current, latestVersion: latest, updated: true}
52
53
  if (isJson) return response
53
54
 
54
55
  this.log(chalk.green('✓') + ` Successfully updated to ${latest}`)
@@ -1,9 +1,9 @@
1
- import { Command, Args, Flags } from '@oclif/core'
1
+ import {Command, Args, Flags} from '@oclif/core'
2
2
  import ora from 'ora'
3
- import { getCveDetail } from '../../services/nvd.js'
4
- import { formatCveDetail } from '../../formatters/vuln.js'
5
- import { openBrowser } from '../../utils/open-browser.js'
6
- import { ValidationError } from '../../utils/errors.js'
3
+ import {getCveDetail} from '../../services/nvd.js'
4
+ import {formatCveDetail} from '../../formatters/vuln.js'
5
+ import {openBrowser} from '../../utils/open-browser.js'
6
+ import {ValidationError} from '../../utils/errors.js'
7
7
 
8
8
  export default class VulnDetail extends Command {
9
9
  static description = 'View full details for a specific CVE'
@@ -17,7 +17,7 @@ export default class VulnDetail extends Command {
17
17
  static enableJsonFlag = true
18
18
 
19
19
  static args = {
20
- cveId: Args.string({ description: 'CVE identifier (e.g. CVE-2021-44228)', required: true }),
20
+ cveId: Args.string({description: 'CVE identifier (e.g. CVE-2021-44228)', required: true}),
21
21
  }
22
22
 
23
23
  static flags = {
@@ -29,9 +29,9 @@ export default class VulnDetail extends Command {
29
29
  }
30
30
 
31
31
  async run() {
32
- const { args, flags } = await this.parse(VulnDetail)
32
+ const {args, flags} = await this.parse(VulnDetail)
33
33
  const isJson = flags.json
34
- const { cveId } = args
34
+ const {cveId} = args
35
35
 
36
36
  if (!cveId || !/^CVE-\d{4}-\d{4,}$/i.test(cveId)) {
37
37
  throw new ValidationError(