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
@@ -1,34 +1,30 @@
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 { confirm, input, select } from '@inquirer/prompts'
5
- import { printWelcomeScreen } from '../utils/welcome.js'
6
- import { typewriterLine } from '../utils/typewriter.js'
7
- import { detectPlatform } from '../services/platform.js'
8
- import { exec, which } from '../services/shell.js'
9
- import { configExists, loadConfig, saveConfig, CONFIG_PATH } from '../services/config.js'
10
- import { oauthFlow, storeToken, validateToken, getTeams } from '../services/clickup.js'
11
- import { SUPPORTED_TOOLS } from '../services/prompts.js'
12
- import { isChezmoiInstalled, setupChezmoiInline } from '../services/dotfiles.js'
4
+ import {confirm, input, select} from '@inquirer/prompts'
5
+ import {printWelcomeScreen} from '../utils/welcome.js'
6
+ import {typewriterLine} from '../utils/typewriter.js'
7
+ import {detectPlatform} from '../services/platform.js'
8
+ import {exec, which} from '../services/shell.js'
9
+ import {configExists, loadConfig, saveConfig, CONFIG_PATH} from '../services/config.js'
10
+ import {oauthFlow, storeToken, validateToken, getTeams} from '../services/clickup.js'
11
+ import {SUPPORTED_TOOLS} from '../services/prompts.js'
12
+ import {isChezmoiInstalled, setupChezmoiInline} from '../services/dotfiles.js'
13
13
 
14
14
  export default class Init extends Command {
15
15
  static description = 'Setup completo ambiente di sviluppo locale'
16
16
 
17
- static examples = [
18
- '<%= config.bin %> init',
19
- '<%= config.bin %> init --dry-run',
20
- '<%= config.bin %> init --verbose',
21
- ]
17
+ static examples = ['<%= config.bin %> init', '<%= config.bin %> init --dry-run', '<%= config.bin %> init --verbose']
22
18
 
23
19
  static enableJsonFlag = true
24
20
 
25
21
  static flags = {
26
- verbose: Flags.boolean({ description: 'Mostra output dettagliato', default: false }),
27
- 'dry-run': Flags.boolean({ description: 'Mostra cosa farebbe senza eseguire', default: false }),
22
+ verbose: Flags.boolean({description: 'Mostra output dettagliato', default: false}),
23
+ 'dry-run': Flags.boolean({description: 'Mostra cosa farebbe senza eseguire', default: false}),
28
24
  }
29
25
 
30
26
  async run() {
31
- const { flags } = await this.parse(Init)
27
+ const {flags} = await this.parse(Init)
32
28
  const isDryRun = flags['dry-run']
33
29
  const isJson = flags.json
34
30
 
@@ -38,75 +34,82 @@ export default class Init extends Command {
38
34
  const steps = []
39
35
 
40
36
  // 1. Check prerequisites
41
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking prerequisites...') }).start()
37
+ const spinner = isJson
38
+ ? null
39
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking prerequisites...')}).start()
42
40
  const prerequisites = [
43
- { name: 'Node.js', cmd: 'node', args: ['--version'], required: true },
44
- { name: 'nvm', cmd: 'nvm', args: ['--version'], required: false },
45
- { name: 'npm', cmd: 'npm', args: ['--version'], required: true },
46
- { name: 'Git', cmd: 'git', args: ['--version'], required: true },
47
- { name: 'gh CLI', cmd: 'gh', args: ['--version'], required: true },
48
- { name: 'Docker', cmd: 'docker', args: ['--version'], required: false },
49
- { name: 'AWS CLI', cmd: 'aws', args: ['--version'], required: false },
50
- { name: 'aws-vault', cmd: 'aws-vault', args: ['--version'], required: false },
41
+ {name: 'Node.js', cmd: 'node', args: ['--version'], required: true},
42
+ {name: 'nvm', cmd: 'nvm', args: ['--version'], required: false},
43
+ {name: 'npm', cmd: 'npm', args: ['--version'], required: true},
44
+ {name: 'Git', cmd: 'git', args: ['--version'], required: true},
45
+ {name: 'gh CLI', cmd: 'gh', args: ['--version'], required: true},
46
+ {name: 'Docker', cmd: 'docker', args: ['--version'], required: false},
47
+ {name: 'AWS CLI', cmd: 'aws', args: ['--version'], required: false},
48
+ {name: 'aws-vault', cmd: 'aws-vault', args: ['--version'], required: false},
51
49
  ]
52
50
 
53
51
  for (const prereq of prerequisites) {
54
52
  const path = await which(prereq.cmd)
55
53
  const status = path ? 'ok' : prereq.required ? 'fail' : 'warn'
56
- steps.push({ name: prereq.name, status, action: path ? 'found' : 'missing' })
54
+ steps.push({name: prereq.name, status, action: path ? 'found' : 'missing'})
57
55
  if (flags.verbose && !isJson) this.log(` ${prereq.name}: ${path ?? 'not found'}`)
58
56
  }
59
57
  spinner?.succeed('Prerequisites checked')
60
58
 
61
59
  // 2. Configure Git credential helper
62
- const gitCredSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Configuring Git credential helper...') }).start()
60
+ const gitCredSpinner = isJson
61
+ ? null
62
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Configuring Git credential helper...')}).start()
63
63
  if (!isDryRun) {
64
64
  await exec('git', ['config', '--global', 'credential.helper', platform.credentialHelper])
65
65
  }
66
- steps.push({ name: 'git-credential', status: 'ok', action: isDryRun ? 'would configure' : 'configured' })
66
+ steps.push({name: 'git-credential', status: 'ok', action: isDryRun ? 'would configure' : 'configured'})
67
67
  gitCredSpinner?.succeed(`Git credential helper: ${platform.credentialHelper}`)
68
68
 
69
69
  // 3. Configure aws-vault (interactive if not configured)
70
70
  const awsVaultInstalled = await which('aws-vault')
71
71
  if (awsVaultInstalled) {
72
- steps.push({ name: 'aws-vault', status: 'ok', action: 'found' })
72
+ steps.push({name: 'aws-vault', status: 'ok', action: 'found'})
73
73
  } else {
74
- steps.push({ name: 'aws-vault', status: 'warn', action: 'not installed' })
74
+ steps.push({name: 'aws-vault', status: 'warn', action: 'not installed'})
75
75
  if (!isJson) {
76
- const installHint = platform.platform === 'macos'
77
- ? 'brew install aws-vault'
78
- : 'run `dvmi security setup` (Debian/Ubuntu/WSL2) or install aws-vault manually'
76
+ const installHint =
77
+ platform.platform === 'macos'
78
+ ? 'brew install aws-vault'
79
+ : 'run `dvmi security setup` (Debian/Ubuntu/WSL2) or install aws-vault manually'
79
80
  this.log(chalk.yellow(` aws-vault not found. Install: ${installHint}`))
80
81
  }
81
82
  }
82
83
 
83
84
  // 4. Create/update config
84
- const configSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Creating config...') }).start()
85
+ const configSpinner = isJson
86
+ ? null
87
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Creating config...')}).start()
85
88
  let config = await loadConfig()
86
89
 
87
- if (!configExists() && !isDryRun && !isJson) {
88
- // Stop the spinner before interactive prompts to avoid TTY contention on macOS
89
- configSpinner?.stop()
90
- const useOrg = await confirm({ message: 'Do you use a GitHub organization? (y/n)', default: true })
91
- let org = ''
92
- if (useOrg) {
93
- org = await input({ message: 'GitHub org name:', default: config.org || '' })
94
- }
95
- const awsProfile = await input({ message: 'AWS profile name:', default: config.awsProfile || 'default' })
96
- const awsRegion = await input({ message: 'AWS region:', default: config.awsRegion || 'eu-west-1' })
97
- config = { ...config, org, awsProfile, awsRegion, shell: platform.credentialHelper }
98
- }
90
+ if (!configExists() && !isDryRun && !isJson) {
91
+ // Stop the spinner before interactive prompts to avoid TTY contention on macOS
92
+ configSpinner?.stop()
93
+ const useOrg = await confirm({message: 'Do you use a GitHub organization? (y/n)', default: true})
94
+ let org = ''
95
+ if (useOrg) {
96
+ org = await input({message: 'GitHub org name:', default: config.org || ''})
97
+ }
98
+ const awsProfile = await input({message: 'AWS profile name:', default: config.awsProfile || 'default'})
99
+ const awsRegion = await input({message: 'AWS region:', default: config.awsRegion || 'eu-west-1'})
100
+ config = {...config, org, awsProfile, awsRegion, shell: platform.credentialHelper}
101
+ }
99
102
 
100
103
  if (!isDryRun) {
101
104
  await saveConfig(config)
102
105
  }
103
- steps.push({ name: 'config', status: 'ok', action: isDryRun ? 'would create' : 'created' })
106
+ steps.push({name: 'config', status: 'ok', action: isDryRun ? 'would create' : 'created'})
104
107
  configSpinner?.succeed(`Config: ${CONFIG_PATH}`)
105
108
 
106
109
  // 5. ClickUp wizard (T008: interactive, T009: dry-run, T010: json)
107
110
  if (isDryRun) {
108
111
  // T009: In dry-run mode report what would happen without any network calls
109
- steps.push({ name: 'clickup', status: 'would configure' })
112
+ steps.push({name: 'clickup', status: 'would configure'})
110
113
  } else if (isJson) {
111
114
  // T010: In JSON mode skip wizard, report current ClickUp config status
112
115
  config = await loadConfig()
@@ -119,11 +122,11 @@ export default class Init extends Command {
119
122
  })
120
123
  } else {
121
124
  // T008: Full interactive wizard
122
- const configureClickUp = await confirm({ message: 'Configure ClickUp integration?', default: true })
123
- if (!configureClickUp) {
124
- steps.push({ name: 'clickup', status: 'skipped' })
125
- this.log(chalk.dim(' Skipped. Run `dvmi init` again to configure ClickUp later.'))
126
- } else {
125
+ const configureClickUp = await confirm({message: 'Configure ClickUp integration?', default: true})
126
+ if (!configureClickUp) {
127
+ steps.push({name: 'clickup', status: 'skipped'})
128
+ this.log(chalk.dim(' Skipped. Run `dvmi init` again to configure ClickUp later.'))
129
+ } else {
127
130
  // Determine auth method
128
131
  const clientId = process.env.CLICKUP_CLIENT_ID
129
132
  const clientSecret = process.env.CLICKUP_CLIENT_SECRET
@@ -133,8 +136,8 @@ export default class Init extends Command {
133
136
  const choice = await select({
134
137
  message: 'Select ClickUp authentication method:',
135
138
  choices: [
136
- { name: 'Personal API Token (paste from ClickUp Settings > Apps)', value: 'personal_token' },
137
- { name: 'OAuth (opens browser)', value: 'oauth' },
139
+ {name: 'Personal API Token (paste from ClickUp Settings > Apps)', value: 'personal_token'},
140
+ {name: 'OAuth (opens browser)', value: 'oauth'},
138
141
  ],
139
142
  })
140
143
  authMethod = /** @type {'oauth'|'personal_token'} */ (choice)
@@ -152,12 +155,16 @@ export default class Init extends Command {
152
155
  }
153
156
 
154
157
  if (authMethod === 'personal_token') {
155
- const token = await input({ message: 'Paste your ClickUp Personal API Token:' })
158
+ const token = await input({message: 'Paste your ClickUp Personal API Token:'})
156
159
  await storeToken(token)
157
160
  }
158
161
 
159
162
  // Validate token
160
- const validateSpinner = ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Validating ClickUp credentials...') }).start()
163
+ const validateSpinner = ora({
164
+ spinner: 'arc',
165
+ color: false,
166
+ text: chalk.hex('#FF6B2B')('Validating ClickUp credentials...'),
167
+ }).start()
161
168
  let tokenValid = false
162
169
  try {
163
170
  const result = await validateToken()
@@ -169,11 +176,11 @@ export default class Init extends Command {
169
176
 
170
177
  if (!tokenValid) {
171
178
  this.log(chalk.yellow(' Invalid token. Check your ClickUp Personal API Token and try again.'))
172
- const retry = await confirm({ message: 'Retry ClickUp configuration?', default: false })
179
+ const retry = await confirm({message: 'Retry ClickUp configuration?', default: false})
173
180
  if (!retry) {
174
- steps.push({ name: 'clickup', status: 'skipped' })
181
+ steps.push({name: 'clickup', status: 'skipped'})
175
182
  } else {
176
- const token = await input({ message: 'Paste your ClickUp Personal API Token:' })
183
+ const token = await input({message: 'Paste your ClickUp Personal API Token:'})
177
184
  await storeToken(token)
178
185
  tokenValid = (await validateToken()).valid
179
186
  }
@@ -183,7 +190,11 @@ export default class Init extends Command {
183
190
  // Fetch teams
184
191
  let teamId = ''
185
192
  let teamName = ''
186
- const teamsSpinner = ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching available teams...') }).start()
193
+ const teamsSpinner = ora({
194
+ spinner: 'arc',
195
+ color: false,
196
+ text: chalk.hex('#FF6B2B')('Fetching available teams...'),
197
+ }).start()
187
198
  try {
188
199
  const teams = await getTeams()
189
200
  teamsSpinner.stop()
@@ -194,97 +205,97 @@ export default class Init extends Command {
194
205
  } else if (teams.length > 1) {
195
206
  const selected = await select({
196
207
  message: 'Select your ClickUp team:',
197
- choices: teams.map((t) => ({ name: `${t.name} (${t.id})`, value: t.id })),
208
+ choices: teams.map((t) => ({name: `${t.name} (${t.id})`, value: t.id})),
198
209
  })
199
210
  teamId = selected
200
211
  teamName = teams.find((t) => t.id === selected)?.name ?? ''
201
212
  } else {
202
- teamId = await input({ message: 'Enter ClickUp team ID:' })
213
+ teamId = await input({message: 'Enter ClickUp team ID:'})
203
214
  }
204
215
  } catch {
205
216
  teamsSpinner.fail('Could not fetch teams')
206
- teamId = await input({ message: 'Enter ClickUp team ID (find in ClickUp Settings > Spaces):' })
217
+ teamId = await input({message: 'Enter ClickUp team ID (find in ClickUp Settings > Spaces):'})
207
218
  }
208
219
 
209
220
  // Save ClickUp config
210
221
  config = await loadConfig()
211
- config = { ...config, clickup: { ...config.clickup, teamId, teamName, authMethod } }
222
+ config = {...config, clickup: {...config.clickup, teamId, teamName, authMethod}}
212
223
  await saveConfig(config)
213
224
  this.log(chalk.green('✓') + ' ClickUp configured successfully!')
214
- steps.push({ name: 'clickup', status: 'configured', teamId, teamName, authMethod })
225
+ steps.push({name: 'clickup', status: 'configured', teamId, teamName, authMethod})
215
226
  }
216
227
  }
217
228
  }
218
229
 
219
- // 6. AI tool selection
220
- if (isDryRun) {
221
- steps.push({ name: 'ai-tool', status: 'would configure' })
222
- } else if (isJson) {
223
- config = await loadConfig()
224
- steps.push({
225
- name: 'ai-tool',
226
- status: config.aiTool ? 'configured' : 'not_configured',
227
- aiTool: config.aiTool,
228
- })
229
- } else {
230
- const aiToolChoices = Object.keys(SUPPORTED_TOOLS).map((t) => ({ name: t, value: t }))
231
- aiToolChoices.push({ name: 'none / skip', value: '' })
232
- const aiTool = await select({
233
- message: 'Select your preferred AI tool for `dvmi prompts run`:',
234
- choices: aiToolChoices,
235
- })
236
- if (aiTool) {
237
- config = { ...config, aiTool }
238
- await saveConfig(config)
239
- this.log(chalk.green(`✓ AI tool set to: ${aiTool}`))
240
- steps.push({ name: 'ai-tool', status: 'configured', aiTool })
241
- } else {
242
- steps.push({ name: 'ai-tool', status: 'skipped' })
243
- }
244
- }
230
+ // 6. AI tool selection
231
+ if (isDryRun) {
232
+ steps.push({name: 'ai-tool', status: 'would configure'})
233
+ } else if (isJson) {
234
+ config = await loadConfig()
235
+ steps.push({
236
+ name: 'ai-tool',
237
+ status: config.aiTool ? 'configured' : 'not_configured',
238
+ aiTool: config.aiTool,
239
+ })
240
+ } else {
241
+ const aiToolChoices = Object.keys(SUPPORTED_TOOLS).map((t) => ({name: t, value: t}))
242
+ aiToolChoices.push({name: 'none / skip', value: ''})
243
+ const aiTool = await select({
244
+ message: 'Select your preferred AI tool for `dvmi prompts run`:',
245
+ choices: aiToolChoices,
246
+ })
247
+ if (aiTool) {
248
+ config = {...config, aiTool}
249
+ await saveConfig(config)
250
+ this.log(chalk.green(`✓ AI tool set to: ${aiTool}`))
251
+ steps.push({name: 'ai-tool', status: 'configured', aiTool})
252
+ } else {
253
+ steps.push({name: 'ai-tool', status: 'skipped'})
254
+ }
255
+ }
245
256
 
246
- // 7. Chezmoi dotfiles setup
247
- if (isDryRun) {
248
- steps.push({ name: 'dotfiles', status: 'would configure' })
249
- } else if (isJson) {
250
- config = await loadConfig()
251
- steps.push({
252
- name: 'dotfiles',
253
- status: config.dotfiles?.enabled ? 'configured' : 'not_configured',
254
- enabled: config.dotfiles?.enabled ?? false,
255
- })
256
- } else {
257
- const chezmoiInstalled = await isChezmoiInstalled()
258
- if (!chezmoiInstalled) {
259
- steps.push({ name: 'dotfiles', status: 'skipped', reason: 'chezmoi not installed' })
260
- } else {
261
- const setupDotfiles = await confirm({
262
- message: 'Set up chezmoi dotfiles management with age encryption?',
263
- default: false,
264
- })
265
- if (setupDotfiles) {
266
- try {
267
- const dotfilesResult = await setupChezmoiInline(platform.platform)
268
- config = await loadConfig()
269
- steps.push({ name: 'dotfiles', status: dotfilesResult.status, sourceDir: dotfilesResult.sourceDir })
270
- } catch (err) {
271
- steps.push({ name: 'dotfiles', status: 'failed', reason: err instanceof Error ? err.message : String(err) })
272
- }
273
- } else {
274
- steps.push({ name: 'dotfiles', status: 'skipped', hint: 'Run `dvmi dotfiles setup` anytime to enable' })
275
- }
276
- }
277
- }
257
+ // 7. Chezmoi dotfiles setup
258
+ if (isDryRun) {
259
+ steps.push({name: 'dotfiles', status: 'would configure'})
260
+ } else if (isJson) {
261
+ config = await loadConfig()
262
+ steps.push({
263
+ name: 'dotfiles',
264
+ status: config.dotfiles?.enabled ? 'configured' : 'not_configured',
265
+ enabled: config.dotfiles?.enabled ?? false,
266
+ })
267
+ } else {
268
+ const chezmoiInstalled = await isChezmoiInstalled()
269
+ if (!chezmoiInstalled) {
270
+ steps.push({name: 'dotfiles', status: 'skipped', reason: 'chezmoi not installed'})
271
+ } else {
272
+ const setupDotfiles = await confirm({
273
+ message: 'Set up chezmoi dotfiles management with age encryption?',
274
+ default: false,
275
+ })
276
+ if (setupDotfiles) {
277
+ try {
278
+ const dotfilesResult = await setupChezmoiInline(platform.platform)
279
+ config = await loadConfig()
280
+ steps.push({name: 'dotfiles', status: dotfilesResult.status, sourceDir: dotfilesResult.sourceDir})
281
+ } catch (err) {
282
+ steps.push({name: 'dotfiles', status: 'failed', reason: err instanceof Error ? err.message : String(err)})
283
+ }
284
+ } else {
285
+ steps.push({name: 'dotfiles', status: 'skipped', hint: 'Run `dvmi dotfiles setup` anytime to enable'})
286
+ }
287
+ }
288
+ }
278
289
 
279
- // 8. Shell completions
280
- steps.push({ name: 'shell-completions', status: 'ok', action: 'install via: dvmi autocomplete' })
290
+ // 8. Shell completions
291
+ steps.push({name: 'shell-completions', status: 'ok', action: 'install via: dvmi autocomplete'})
281
292
 
282
- const result = { steps, configPath: CONFIG_PATH }
293
+ const result = {steps, configPath: CONFIG_PATH}
283
294
 
284
- if (isJson) return result
295
+ if (isJson) return result
285
296
 
286
- await typewriterLine('✓ Setup complete!')
287
- this.log(chalk.dim(' Run `dvmi doctor` to verify your environment'))
297
+ await typewriterLine('✓ Setup complete!')
298
+ this.log(chalk.dim(' Run `dvmi doctor` to verify your environment'))
288
299
 
289
300
  return result
290
301
  }
@@ -1,9 +1,9 @@
1
- import { Command, Flags } from '@oclif/core'
1
+ import {Command, Flags} from '@oclif/core'
2
2
  import ora from 'ora'
3
- import { search, input } from '@inquirer/prompts'
4
- import { listLogGroups, filterLogEvents, sinceToEpochMs } from '../../services/cloudwatch-logs.js'
5
- import { loadConfig } from '../../services/config.js'
6
- import { DvmiError } from '../../utils/errors.js'
3
+ import {search, input} from '@inquirer/prompts'
4
+ import {listLogGroups, filterLogEvents, sinceToEpochMs} from '../../services/cloudwatch-logs.js'
5
+ import {loadConfig} from '../../services/config.js'
6
+ import {DvmiError} from '../../utils/errors.js'
7
7
 
8
8
  const SINCE_OPTIONS = ['1h', '24h', '7d']
9
9
 
@@ -44,7 +44,7 @@ export default class Logs extends Command {
44
44
  }
45
45
 
46
46
  async run() {
47
- const { flags } = await this.parse(Logs)
47
+ const {flags} = await this.parse(Logs)
48
48
  const isJson = flags.json
49
49
 
50
50
  // Validate --limit
@@ -86,9 +86,7 @@ export default class Logs extends Command {
86
86
  message: 'Select a log group',
87
87
  source: async (input) => {
88
88
  const term = (input ?? '').toLowerCase()
89
- return groups
90
- .filter((g) => g.name.toLowerCase().includes(term))
91
- .map((g) => ({ name: g.name, value: g.name }))
89
+ return groups.filter((g) => g.name.toLowerCase().includes(term)).map((g) => ({name: g.name, value: g.name}))
92
90
  },
93
91
  })
94
92
 
@@ -102,7 +100,7 @@ export default class Logs extends Command {
102
100
  }
103
101
  }
104
102
 
105
- const { startTime, endTime } = sinceToEpochMs(/** @type {'1h'|'24h'|'7d'} */ (flags.since))
103
+ const {startTime, endTime} = sinceToEpochMs(/** @type {'1h'|'24h'|'7d'} */ (flags.since))
106
104
 
107
105
  const fetchSpinner = isJson ? null : ora('Fetching log events...').start()
108
106
 
@@ -171,14 +169,10 @@ export default class Logs extends Command {
171
169
  _handleAwsError(err, _region, _logGroupName) {
172
170
  const msg = String(err)
173
171
  if (msg.includes('AccessDenied') || msg.includes('UnauthorizedAccess')) {
174
- this.error(
175
- 'Access denied. Ensure your role has logs:DescribeLogGroups and logs:FilterLogEvents permissions.',
176
- )
172
+ this.error('Access denied. Ensure your role has logs:DescribeLogGroups and logs:FilterLogEvents permissions.')
177
173
  }
178
174
  if (msg.includes('ResourceNotFoundException')) {
179
- this.error(
180
- `Log group not found. Check the name and confirm you are using the correct region (--region).`,
181
- )
175
+ this.error(`Log group not found. Check the name and confirm you are using the correct region (--region).`)
182
176
  }
183
177
  if (msg.includes('InvalidParameterException')) {
184
178
  this.error('Invalid filter pattern or parameter. Check the pattern syntax and time range.')
@@ -1,8 +1,8 @@
1
- import { Command, Args } from '@oclif/core'
1
+ import {Command, Args} from '@oclif/core'
2
2
  import chalk from 'chalk'
3
- import { exec } from '../services/shell.js'
4
- import { openBrowser } from '../utils/open-browser.js'
5
- import { loadConfig } from '../services/config.js'
3
+ import {exec} from '../services/shell.js'
4
+ import {openBrowser} from '../utils/open-browser.js'
5
+ import {loadConfig} from '../services/config.js'
6
6
 
7
7
  const VALID_TARGETS = ['repo', 'pr', 'actions', 'aws']
8
8
 
@@ -20,11 +20,11 @@ export default class Open extends Command {
20
20
  static enableJsonFlag = true
21
21
 
22
22
  static args = {
23
- target: Args.string({ description: 'Target: repo, pr, actions, aws', required: true }),
23
+ target: Args.string({description: 'Target: repo, pr, actions, aws', required: true}),
24
24
  }
25
25
 
26
26
  async run() {
27
- const { args, flags } = await this.parse(Open)
27
+ const {args, flags} = await this.parse(Open)
28
28
  const isJson = flags.json
29
29
 
30
30
  if (!VALID_TARGETS.includes(args.target)) {
@@ -55,15 +55,15 @@ export default class Open extends Command {
55
55
  const branch = branchResult.stdout
56
56
  // Try to find open PR for current branch
57
57
  const prResult = await exec('gh', ['pr', 'view', '--json', 'url', '-H', branch])
58
- if (prResult.exitCode === 0) {
59
- url = JSON.parse(prResult.stdout).url
60
- } else {
61
- this.error(`No PR found for branch "${branch}". Create one with \`dvmi pr create\``)
62
- }
58
+ if (prResult.exitCode === 0) {
59
+ url = JSON.parse(prResult.stdout).url
60
+ } else {
61
+ this.error(`No PR found for branch "${branch}". Create one with \`dvmi pr create\``)
62
+ }
63
63
  }
64
64
  }
65
65
 
66
- const result = { target: args.target, url, opened: !isJson }
66
+ const result = {target: args.target, url, opened: !isJson}
67
67
 
68
68
  if (isJson) return result
69
69
 
@@ -1,26 +1,23 @@
1
- import { Command, Args, Flags } from '@oclif/core'
2
- import { exec } from '../../services/shell.js'
1
+ import {Command, Args, Flags} from '@oclif/core'
2
+ import {exec} from '../../services/shell.js'
3
3
 
4
4
  export default class PipelineLogs extends Command {
5
5
  static description = 'Log di un workflow run specifico'
6
6
 
7
- static examples = [
8
- '<%= config.bin %> pipeline logs 12345',
9
- '<%= config.bin %> pipeline logs 12345 --job test',
10
- ]
7
+ static examples = ['<%= config.bin %> pipeline logs 12345', '<%= config.bin %> pipeline logs 12345 --job test']
11
8
 
12
9
  static enableJsonFlag = true
13
10
 
14
11
  static args = {
15
- 'run-id': Args.integer({ description: 'ID del workflow run', required: true }),
12
+ 'run-id': Args.integer({description: 'ID del workflow run', required: true}),
16
13
  }
17
14
 
18
15
  static flags = {
19
- job: Flags.string({ description: 'Filtra per job name' }),
16
+ job: Flags.string({description: 'Filtra per job name'}),
20
17
  }
21
18
 
22
19
  async run() {
23
- const { args, flags } = await this.parse(PipelineLogs)
20
+ const {args, flags} = await this.parse(PipelineLogs)
24
21
  const isJson = flags.json
25
22
 
26
23
  const ghArgs = ['run', 'view', String(args['run-id']), '--log']
@@ -32,10 +29,10 @@ export default class PipelineLogs extends Command {
32
29
  }
33
30
 
34
31
  if (isJson) {
35
- return { runId: args['run-id'], log: result.stdout }
32
+ return {runId: args['run-id'], log: result.stdout}
36
33
  }
37
34
 
38
35
  this.log(result.stdout)
39
- return { runId: args['run-id'], log: result.stdout }
36
+ return {runId: args['run-id'], log: result.stdout}
40
37
  }
41
38
  }
@@ -1,12 +1,12 @@
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 { confirm } from '@inquirer/prompts'
5
- import { listWorkflowRuns, rerunWorkflow } from '../../services/github.js'
6
- import { exec } from '../../services/shell.js'
4
+ import {confirm} from '@inquirer/prompts'
5
+ import {listWorkflowRuns, rerunWorkflow} from '../../services/github.js'
6
+ import {exec} from '../../services/shell.js'
7
7
 
8
8
  export default class PipelineRerun extends Command {
9
- static description = 'Rilancia l\'ultimo workflow fallito'
9
+ static description = "Rilancia l'ultimo workflow fallito"
10
10
 
11
11
  static examples = [
12
12
  '<%= config.bin %> pipeline rerun',
@@ -17,12 +17,12 @@ export default class PipelineRerun extends Command {
17
17
  static enableJsonFlag = true
18
18
 
19
19
  static flags = {
20
- 'run-id': Flags.integer({ description: 'ID specifico del run' }),
21
- 'failed-only': Flags.boolean({ description: 'Rilancia solo i job falliti', default: false }),
20
+ 'run-id': Flags.integer({description: 'ID specifico del run'}),
21
+ 'failed-only': Flags.boolean({description: 'Rilancia solo i job falliti', default: false}),
22
22
  }
23
23
 
24
24
  async run() {
25
- const { flags } = await this.parse(PipelineRerun)
25
+ const {flags} = await this.parse(PipelineRerun)
26
26
  const isJson = flags.json
27
27
 
28
28
  const remoteResult = await exec('git', ['remote', 'get-url', 'origin'])
@@ -34,7 +34,7 @@ export default class PipelineRerun extends Command {
34
34
  let runId = flags['run-id']
35
35
 
36
36
  if (!runId) {
37
- const runs = await listWorkflowRuns(owner, repo, { limit: 10 })
37
+ const runs = await listWorkflowRuns(owner, repo, {limit: 10})
38
38
  const failed = runs.find((r) => r.conclusion === 'failure')
39
39
  if (!failed) {
40
40
  this.log(chalk.green('No failed runs found.'))
@@ -47,19 +47,24 @@ export default class PipelineRerun extends Command {
47
47
  }
48
48
 
49
49
  if (!isJson) {
50
- const ok = await confirm({ message: `Rerun workflow #${runId}?` })
51
- if (!ok) { this.log('Aborted.'); return }
50
+ const ok = await confirm({message: `Rerun workflow #${runId}?`})
51
+ if (!ok) {
52
+ this.log('Aborted.')
53
+ return
54
+ }
52
55
  }
53
56
 
54
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Relaunching workflow...') }).start()
57
+ const spinner = isJson
58
+ ? null
59
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Relaunching workflow...')}).start()
55
60
  await rerunWorkflow(owner, repo, runId, flags['failed-only'])
56
61
  spinner?.succeed(`Workflow #${runId} rerun started`)
57
62
 
58
- const result = { rerun: { id: runId, failedOnly: flags['failed-only'], status: 'queued' } }
63
+ const result = {rerun: {id: runId, failedOnly: flags['failed-only'], status: 'queued'}}
59
64
 
60
- if (!isJson) {
61
- this.log(chalk.dim('Track with `dvmi pipeline status`'))
62
- }
65
+ if (!isJson) {
66
+ this.log(chalk.dim('Track with `dvmi pipeline status`'))
67
+ }
63
68
 
64
69
  return result
65
70
  }