devvami 1.4.1 → 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 +41 -1
  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 +95 -21
  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 +25 -17
  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
@@ -1,30 +1,28 @@
1
- import { Command, Flags } from '@oclif/core'
2
- import { confirm, select } from '@inquirer/prompts'
1
+ import {Command, Flags} from '@oclif/core'
2
+ import {confirm, select} from '@inquirer/prompts'
3
3
  import ora from 'ora'
4
4
  import chalk from 'chalk'
5
- import { execa } from 'execa'
6
- import { detectPlatform } from '../../services/platform.js'
7
- import { exec } from '../../services/shell.js'
8
- import { buildSteps, checkToolStatus, listGpgKeys, deriveOverallStatus } from '../../services/security.js'
9
- import { formatEducationalIntro, formatStepHeader, formatSecuritySummary } from '../../formatters/security.js'
5
+ import {execa} from 'execa'
6
+ import {detectPlatform} from '../../services/platform.js'
7
+ import {exec} from '../../services/shell.js'
8
+ import {buildSteps, checkToolStatus, listGpgKeys, deriveOverallStatus} from '../../services/security.js'
9
+ import {formatEducationalIntro, formatStepHeader, formatSecuritySummary} from '../../formatters/security.js'
10
10
  /** @import { SetupSession, SetupStep, StepResult, PlatformInfo } from '../../types.js' */
11
11
 
12
12
  export default class SecuritySetup extends Command {
13
- static description = 'Interactive wizard to install and configure credential protection tools (aws-vault, pass, GPG, Git Credential Manager, macOS Keychain)'
13
+ static description =
14
+ 'Interactive wizard to install and configure credential protection tools (aws-vault, pass, GPG, Git Credential Manager, macOS Keychain)'
14
15
 
15
- static examples = [
16
- '<%= config.bin %> security setup',
17
- '<%= config.bin %> security setup --json',
18
- ]
16
+ static examples = ['<%= config.bin %> security setup', '<%= config.bin %> security setup --json']
19
17
 
20
18
  static enableJsonFlag = true
21
19
 
22
20
  static flags = {
23
- help: Flags.help({ char: 'h' }),
21
+ help: Flags.help({char: 'h'}),
24
22
  }
25
23
 
26
24
  async run() {
27
- const { flags } = await this.parse(SecuritySetup)
25
+ const {flags} = await this.parse(SecuritySetup)
28
26
  const isJson = flags.json
29
27
 
30
28
  // FR-018: Detect non-interactive environments
@@ -34,22 +32,19 @@ export default class SecuritySetup extends Command {
34
32
  if ((isCI || isNonInteractive) && !isJson) {
35
33
  this.error(
36
34
  'This command requires an interactive terminal (TTY). Run with --json for a non-interactive health check.',
37
- { exit: 1 },
35
+ {exit: 1},
38
36
  )
39
37
  }
40
38
 
41
39
  // Detect platform
42
40
  const platformInfo = await detectPlatform()
43
- const { platform } = platformInfo
41
+ const {platform} = platformInfo
44
42
 
45
43
  // FR-019: Sudo pre-flight on Linux/WSL2
46
44
  if (platform !== 'macos' && !isJson) {
47
45
  const sudoCheck = await exec('sudo', ['-n', 'true'])
48
46
  if (sudoCheck.exitCode !== 0) {
49
- this.error(
50
- 'sudo access is required to install packages. Run `sudo -v` to authenticate and retry.',
51
- { exit: 1 },
52
- )
47
+ this.error('sudo access is required to install packages. Run `sudo -v` to authenticate and retry.', {exit: 1})
53
48
  }
54
49
  }
55
50
 
@@ -68,7 +63,11 @@ export default class SecuritySetup extends Command {
68
63
  // ---------------------------------------------------------------------------
69
64
  // Pre-check: show current tool status
70
65
  // ---------------------------------------------------------------------------
71
- const spinner = ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking current tool status...') }).start()
66
+ const spinner = ora({
67
+ spinner: 'arc',
68
+ color: false,
69
+ text: chalk.hex('#FF6B2B')('Checking current tool status...'),
70
+ }).start()
72
71
  const currentStatus = await checkToolStatus(platform)
73
72
  spinner.stop()
74
73
 
@@ -100,7 +99,7 @@ export default class SecuritySetup extends Command {
100
99
  })
101
100
  if (!understood) {
102
101
  this.log('Setup cancelled.')
103
- return { platform, selection: null, tools: currentStatus, overallStatus: deriveOverallStatus(currentStatus) }
102
+ return {platform, selection: null, tools: currentStatus, overallStatus: deriveOverallStatus(currentStatus)}
104
103
  }
105
104
 
106
105
  // ---------------------------------------------------------------------------
@@ -109,9 +108,9 @@ export default class SecuritySetup extends Command {
109
108
  const selectionValue = await select({
110
109
  message: 'What would you like to set up?',
111
110
  choices: [
112
- { name: 'Both AWS and Git credentials (recommended)', value: 'both' },
113
- { name: 'AWS credentials only (aws-vault)', value: 'aws' },
114
- { name: 'Git credentials only (macOS Keychain / GCM)', value: 'git' },
111
+ {name: 'Both AWS and Git credentials (recommended)', value: 'both'},
112
+ {name: 'AWS credentials only (aws-vault)', value: 'aws'},
113
+ {name: 'Git credentials only (macOS Keychain / GCM)', value: 'git'},
115
114
  ],
116
115
  })
117
116
 
@@ -131,7 +130,7 @@ export default class SecuritySetup extends Command {
131
130
  name: `${k.name} <${k.email}> (${k.id})`,
132
131
  value: k.id,
133
132
  })),
134
- { name: 'Create a new GPG key', value: '__new__' },
133
+ {name: 'Create a new GPG key', value: '__new__'},
135
134
  ]
136
135
  const chosen = await select({
137
136
  message: 'Select a GPG key for pass and Git Credential Manager:',
@@ -145,7 +144,7 @@ export default class SecuritySetup extends Command {
145
144
  // ---------------------------------------------------------------------------
146
145
  // Build steps
147
146
  // ---------------------------------------------------------------------------
148
- const steps = buildSteps(platformInfo, selection, { gpgId })
147
+ const steps = buildSteps(platformInfo, selection, {gpgId})
149
148
 
150
149
  /** @type {SetupSession} */
151
150
  const session = {
@@ -166,9 +165,9 @@ export default class SecuritySetup extends Command {
166
165
 
167
166
  // FR-014: confirmation prompt before system-level changes
168
167
  if (step.requiresConfirmation) {
169
- const proceed = await confirm({ message: `Proceed with: ${step.label}?`, default: true })
168
+ const proceed = await confirm({message: `Proceed with: ${step.label}?`, default: true})
170
169
  if (!proceed) {
171
- session.results.set(step.id, { status: 'skipped', message: 'Skipped by user' })
170
+ session.results.set(step.id, {status: 'skipped', message: 'Skipped by user'})
172
171
  this.log(chalk.dim(' Skipped.'))
173
172
  continue
174
173
  }
@@ -179,17 +178,17 @@ export default class SecuritySetup extends Command {
179
178
  this.log(chalk.cyan('\n GPG will now prompt you for a passphrase in your terminal.'))
180
179
  this.log(chalk.dim(' Follow the interactive prompts to complete key generation.\n'))
181
180
  try {
182
- await execa('gpg', ['--full-generate-key'], { stdio: 'inherit', reject: true })
181
+ await execa('gpg', ['--full-generate-key'], {stdio: 'inherit', reject: true})
183
182
  // Refresh the gpgId from newly created key
184
183
  const newKeys = await listGpgKeys()
185
184
  if (newKeys.length > 0) {
186
185
  gpgId = newKeys[0].id
187
186
  // gpgId is now set — subsequent step closures capture it via the shared context object
188
187
  }
189
- session.results.set(step.id, { status: 'success', message: `GPG key created (${gpgId || 'new key'})` })
188
+ session.results.set(step.id, {status: 'success', message: `GPG key created (${gpgId || 'new key'})`})
190
189
  this.log(chalk.green(' ✔ GPG key created'))
191
190
  } catch {
192
- const result = { status: /** @type {'failed'} */ ('failed'), hint: 'Run manually: gpg --full-generate-key' }
191
+ const result = {status: /** @type {'failed'} */ ('failed'), hint: 'Run manually: gpg --full-generate-key'}
193
192
  session.results.set(step.id, result)
194
193
  this.log(chalk.red(' ✗ GPG key creation failed'))
195
194
  this.log(chalk.dim(` → ${result.hint}`))
@@ -200,7 +199,7 @@ export default class SecuritySetup extends Command {
200
199
  }
201
200
 
202
201
  // Regular step with spinner
203
- const stepSpinner = ora({ spinner: 'arc', color: false, text: chalk.dim(step.label) }).start()
202
+ const stepSpinner = ora({spinner: 'arc', color: false, text: chalk.dim(step.label)}).start()
204
203
 
205
204
  let result
206
205
  try {
@@ -243,7 +242,12 @@ export default class SecuritySetup extends Command {
243
242
  platform,
244
243
  selection,
245
244
  tools: currentStatus,
246
- overallStatus: session.overallStatus === 'completed' ? 'success' : session.overallStatus === 'failed' ? 'partial' : 'not-configured',
245
+ overallStatus:
246
+ session.overallStatus === 'completed'
247
+ ? 'success'
248
+ : session.overallStatus === 'failed'
249
+ ? 'partial'
250
+ : 'not-configured',
247
251
  }
248
252
  }
249
253
  }
@@ -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
  }