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,13 +1,13 @@
1
- import { Command, Flags, Args } from '@oclif/core'
1
+ import {Command, Flags, Args} from '@oclif/core'
2
2
  import ora from 'ora'
3
3
  import chalk from 'chalk'
4
- import { confirm, input, select } from '@inquirer/prompts'
5
- import { detectPlatform } from '../../services/platform.js'
6
- import { isChezmoiInstalled, getChezmoiRemote, hasLocalChanges } from '../../services/dotfiles.js'
7
- import { loadConfig, saveConfig } from '../../services/config.js'
8
- import { exec, execOrThrow } from '../../services/shell.js'
9
- import { formatDotfilesSync } from '../../formatters/dotfiles.js'
10
- import { DvmiError } from '../../utils/errors.js'
4
+ import {confirm, input, select} from '@inquirer/prompts'
5
+ import {detectPlatform} from '../../services/platform.js'
6
+ import {isChezmoiInstalled, getChezmoiRemote, hasLocalChanges} from '../../services/dotfiles.js'
7
+ import {loadConfig, saveConfig} from '../../services/config.js'
8
+ import {exec, execOrThrow} from '../../services/shell.js'
9
+ import {formatDotfilesSync} from '../../formatters/dotfiles.js'
10
+ import {DvmiError} from '../../utils/errors.js'
11
11
 
12
12
  /** @import { DotfilesSyncResult } from '../../types.js' */
13
13
 
@@ -26,18 +26,18 @@ export default class DotfilesSync extends Command {
26
26
  static enableJsonFlag = true
27
27
 
28
28
  static flags = {
29
- help: Flags.help({ char: 'h' }),
30
- push: Flags.boolean({ description: 'Push local changes to remote', default: false }),
31
- pull: Flags.boolean({ description: 'Pull remote changes and apply', default: false }),
32
- 'dry-run': Flags.boolean({ description: 'Show what would change without applying', default: false }),
29
+ help: Flags.help({char: 'h'}),
30
+ push: Flags.boolean({description: 'Push local changes to remote', default: false}),
31
+ pull: Flags.boolean({description: 'Pull remote changes and apply', default: false}),
32
+ 'dry-run': Flags.boolean({description: 'Show what would change without applying', default: false}),
33
33
  }
34
34
 
35
35
  static args = {
36
- repo: Args.string({ description: 'Remote repository URL (for initial remote setup)', required: false }),
36
+ repo: Args.string({description: 'Remote repository URL (for initial remote setup)', required: false}),
37
37
  }
38
38
 
39
39
  async run() {
40
- const { flags, args } = await this.parse(DotfilesSync)
40
+ const {flags, args} = await this.parse(DotfilesSync)
41
41
  const isJson = flags.json
42
42
  const isPush = flags.push
43
43
  const isPull = flags.pull
@@ -56,30 +56,27 @@ export default class DotfilesSync extends Command {
56
56
  const isCI = process.env.CI === 'true'
57
57
  const isNonInteractive = !process.stdout.isTTY
58
58
  if ((isCI || isNonInteractive) && !isJson) {
59
- this.error(
60
- 'This command requires an interactive terminal (TTY). Run with --json for a non-interactive sync.',
61
- { exit: 1 },
62
- )
59
+ this.error('This command requires an interactive terminal (TTY). Run with --json for a non-interactive sync.', {
60
+ exit: 1,
61
+ })
63
62
  }
64
63
 
65
64
  const config = await loadConfig()
66
65
  if (!config.dotfiles?.enabled) {
67
- throw new DvmiError(
68
- 'Chezmoi dotfiles management is not configured',
69
- 'Run `dvmi dotfiles setup` first',
70
- )
66
+ throw new DvmiError('Chezmoi dotfiles management is not configured', 'Run `dvmi dotfiles setup` first')
71
67
  }
72
68
 
73
69
  const chezmoiInstalled = await isChezmoiInstalled()
74
70
  if (!chezmoiInstalled) {
75
71
  const platformInfo = await detectPlatform()
76
- const hint = platformInfo.platform === 'macos'
77
- ? 'Run `brew install chezmoi` or visit https://chezmoi.io/install'
78
- : 'Run `sh -c "$(curl -fsLS get.chezmoi.io)"` or visit https://chezmoi.io/install'
72
+ const hint =
73
+ platformInfo.platform === 'macos'
74
+ ? 'Run `brew install chezmoi` or visit https://chezmoi.io/install'
75
+ : 'Run `sh -c "$(curl -fsLS get.chezmoi.io)"` or visit https://chezmoi.io/install'
79
76
  throw new DvmiError('chezmoi is not installed', hint)
80
77
  }
81
78
 
82
- const remote = config.dotfiles?.repo ?? await getChezmoiRemote()
79
+ const remote = config.dotfiles?.repo ?? (await getChezmoiRemote())
83
80
 
84
81
  // --json mode: attempt push/pull or report status
85
82
  if (isJson) {
@@ -105,7 +102,13 @@ export default class DotfilesSync extends Command {
105
102
  }
106
103
 
107
104
  /** @type {DotfilesSyncResult} */
108
- return { action: 'skipped', repo: effectiveRemote ?? null, status: 'skipped', message: 'No action specified', conflicts: [] }
105
+ return {
106
+ action: 'skipped',
107
+ repo: effectiveRemote ?? null,
108
+ status: 'skipped',
109
+ message: 'No action specified',
110
+ conflicts: [],
111
+ }
109
112
  }
110
113
 
111
114
  // ---------------------------------------------------------------------------
@@ -140,22 +143,29 @@ export default class DotfilesSync extends Command {
140
143
  const action = await select({
141
144
  message: 'What would you like to do?',
142
145
  choices: [
143
- { name: 'Push local changes to remote', value: 'push' },
144
- { name: 'Pull remote changes and apply', value: 'pull' },
145
- { name: 'Cancel', value: 'cancel' },
146
+ {name: 'Push local changes to remote', value: 'push'},
147
+ {name: 'Pull remote changes and apply', value: 'pull'},
148
+ {name: 'Cancel', value: 'cancel'},
146
149
  ],
147
150
  })
148
151
 
149
152
  if (action === 'cancel') {
150
153
  /** @type {DotfilesSyncResult} */
151
- const cancelResult = { action: 'skipped', repo: effectiveRemote ?? null, status: 'skipped', message: 'Cancelled by user', conflicts: [] }
154
+ const cancelResult = {
155
+ action: 'skipped',
156
+ repo: effectiveRemote ?? null,
157
+ status: 'skipped',
158
+ message: 'Cancelled by user',
159
+ conflicts: [],
160
+ }
152
161
  this.log(formatDotfilesSync(cancelResult))
153
162
  return cancelResult
154
163
  }
155
164
 
156
- const result = action === 'push'
157
- ? await this._push(effectiveRemote, isDryRun, false)
158
- : await this._pull(effectiveRemote, isDryRun, false)
165
+ const result =
166
+ action === 'push'
167
+ ? await this._push(effectiveRemote, isDryRun, false)
168
+ : await this._pull(effectiveRemote, isDryRun, false)
159
169
 
160
170
  this.log(formatDotfilesSync(result))
161
171
  return result
@@ -174,25 +184,25 @@ export default class DotfilesSync extends Command {
174
184
  const choice = await select({
175
185
  message: 'Connect to an existing dotfiles repository or create a new one?',
176
186
  choices: [
177
- { name: 'Connect to existing repository', value: 'existing' },
178
- { name: 'Create new repository on GitHub', value: 'new' },
187
+ {name: 'Connect to existing repository', value: 'existing'},
188
+ {name: 'Create new repository on GitHub', value: 'new'},
179
189
  ],
180
190
  })
181
191
 
182
192
  let repoUrl = ''
183
193
 
184
194
  if (choice === 'existing') {
185
- repoUrl = await input({ message: 'Repository URL (SSH or HTTPS):' })
195
+ repoUrl = await input({message: 'Repository URL (SSH or HTTPS):'})
186
196
  } else {
187
- const repoName = await input({ message: 'Repository name:', default: 'dotfiles' })
188
- const isPrivate = await confirm({ message: 'Make repository private?', default: true })
197
+ const repoName = await input({message: 'Repository name:', default: 'dotfiles'})
198
+ const isPrivate = await confirm({message: 'Make repository private?', default: true})
189
199
 
190
200
  if (!isDryRun) {
191
201
  try {
192
202
  const visFlag = isPrivate ? '--private' : '--public'
193
203
  await execOrThrow('gh', ['repo', 'create', repoName, visFlag, '--confirm'])
194
204
  // Get the SSH URL from the created repo
195
- const { exec } = await import('../../services/shell.js')
205
+ const {exec} = await import('../../services/shell.js')
196
206
  const result = await exec('gh', ['repo', 'view', repoName, '--json', 'sshUrl', '--jq', '.sshUrl'])
197
207
  repoUrl = result.stdout.trim() || `git@github.com:${repoName}.git`
198
208
  } catch {
@@ -211,7 +221,7 @@ export default class DotfilesSync extends Command {
211
221
  await execOrThrow('chezmoi', ['git', '--', 'remote', 'add', 'origin', repoUrl])
212
222
  await execOrThrow('chezmoi', ['git', '--', 'push', '-u', 'origin', 'main'])
213
223
  // Save repo to dvmi config
214
- config.dotfiles = { ...(config.dotfiles ?? { enabled: true }), repo: repoUrl }
224
+ config.dotfiles = {...(config.dotfiles ?? {enabled: true}), repo: repoUrl}
215
225
  await saveConfig(config)
216
226
  } catch (err) {
217
227
  /** @type {DotfilesSyncResult} */
@@ -232,7 +242,9 @@ export default class DotfilesSync extends Command {
232
242
  action: 'init-remote',
233
243
  repo: repoUrl,
234
244
  status: isDryRun ? 'skipped' : 'success',
235
- message: isDryRun ? `Would configure remote: ${repoUrl}` : 'Remote repository configured and initial push completed',
245
+ message: isDryRun
246
+ ? `Would configure remote: ${repoUrl}`
247
+ : 'Remote repository configured and initial push completed',
236
248
  conflicts: [],
237
249
  }
238
250
  this.log(formatDotfilesSync(result))
@@ -271,7 +283,9 @@ export default class DotfilesSync extends Command {
271
283
  }
272
284
  }
273
285
 
274
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Pushing to remote...') }).start()
286
+ const spinner = isJson
287
+ ? null
288
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Pushing to remote...')}).start()
275
289
 
276
290
  try {
277
291
  // Stage all changes
@@ -282,7 +296,7 @@ export default class DotfilesSync extends Command {
282
296
  await execOrThrow('chezmoi', ['git', '--', 'push', 'origin', 'HEAD'])
283
297
  spinner?.succeed(chalk.green('Pushed to remote'))
284
298
 
285
- return { action: 'push', repo: remote, status: 'success', message: 'Changes pushed to remote', conflicts: [] }
299
+ return {action: 'push', repo: remote, status: 'success', message: 'Changes pushed to remote', conflicts: []}
286
300
  } catch (err) {
287
301
  spinner?.fail(chalk.red('Push failed'))
288
302
  return {
@@ -327,7 +341,9 @@ export default class DotfilesSync extends Command {
327
341
  }
328
342
  }
329
343
 
330
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Pulling from remote...') }).start()
344
+ const spinner = isJson
345
+ ? null
346
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Pulling from remote...')}).start()
331
347
 
332
348
  try {
333
349
  // Check if chezmoi init was done with this remote (first-time pull)
@@ -360,7 +376,7 @@ export default class DotfilesSync extends Command {
360
376
  }
361
377
  }
362
378
 
363
- return { action: 'pull', repo: remote, status: 'success', message: 'Remote changes applied', conflicts: [] }
379
+ return {action: 'pull', repo: remote, status: 'success', message: 'Remote changes applied', conflicts: []}
364
380
  } catch (err) {
365
381
  spinner?.fail(chalk.red('Pull failed'))
366
382
  return {
@@ -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
  }