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,9 +1,9 @@
1
- import { Command, Args, Flags } from '@oclif/core'
1
+ import {Command, Args, Flags} from '@oclif/core'
2
2
  import chalk from 'chalk'
3
3
  import ora from 'ora'
4
- import { loadConfig } from '../../services/config.js'
5
- import { searchDocs, detectCurrentRepo } from '../../services/docs.js'
6
- import { renderTable } from '../../formatters/table.js'
4
+ import {loadConfig} from '../../services/config.js'
5
+ import {searchDocs, detectCurrentRepo} from '../../services/docs.js'
6
+ import {renderTable} from '../../formatters/table.js'
7
7
 
8
8
  const MAX_MATCHES_PER_FILE = 3
9
9
 
@@ -19,15 +19,15 @@ export default class DocsSearch extends Command {
19
19
  static enableJsonFlag = true
20
20
 
21
21
  static args = {
22
- term: Args.string({ description: 'Termine di ricerca (case-insensitive)', required: true }),
22
+ term: Args.string({description: 'Termine di ricerca (case-insensitive)', required: true}),
23
23
  }
24
24
 
25
25
  static flags = {
26
- repo: Flags.string({ char: 'r', description: 'Nome del repository (default: repo nella directory corrente)' }),
26
+ repo: Flags.string({char: 'r', description: 'Nome del repository (default: repo nella directory corrente)'}),
27
27
  }
28
28
 
29
29
  async run() {
30
- const { args, flags } = await this.parse(DocsSearch)
30
+ const {args, flags} = await this.parse(DocsSearch)
31
31
  const isJson = flags.json
32
32
  const config = await loadConfig()
33
33
 
@@ -39,13 +39,15 @@ export default class DocsSearch extends Command {
39
39
  repo = flags.repo
40
40
  } else {
41
41
  try {
42
- ;({ owner, repo } = await detectCurrentRepo())
42
+ ;({owner, repo} = await detectCurrentRepo())
43
43
  } catch (err) {
44
44
  this.error(/** @type {Error} */ (err).message)
45
45
  }
46
46
  }
47
47
 
48
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')(`Searching "${args.term}" in docs...`) }).start()
48
+ const spinner = isJson
49
+ ? null
50
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')(`Searching "${args.term}" in docs...`)}).start()
49
51
  let matches
50
52
  try {
51
53
  matches = await searchDocs(owner, repo, args.term)
@@ -55,14 +57,17 @@ export default class DocsSearch extends Command {
55
57
  }
56
58
  spinner?.stop()
57
59
 
58
- if (isJson) return { repo, owner, term: args.term, matches, total: matches.length }
60
+ if (isJson) return {repo, owner, term: args.term, matches, total: matches.length}
59
61
 
60
62
  if (matches.length === 0) {
61
63
  this.log(chalk.dim(`No matches found for "${args.term}" in ${owner}/${repo} documentation.`))
62
- return { repo, owner, term: args.term, matches: [], total: 0 }
64
+ return {repo, owner, term: args.term, matches: [], total: 0}
63
65
  }
64
66
 
65
- this.log(chalk.bold(`\nSearch results for "${args.term}" in ${owner}/${repo}`) + chalk.dim(` (${matches.length} match${matches.length === 1 ? '' : 'es'})\n`))
67
+ this.log(
68
+ chalk.bold(`\nSearch results for "${args.term}" in ${owner}/${repo}`) +
69
+ chalk.dim(` (${matches.length} match${matches.length === 1 ? '' : 'es'})\n`),
70
+ )
66
71
 
67
72
  // Group by file and limit rows
68
73
  /** @type {Map<string, import('../../types.js').SearchMatch[]>} */
@@ -80,24 +85,31 @@ export default class DocsSearch extends Command {
80
85
  rows.push(...shown)
81
86
  const extra = fileMatches.length - shown.length
82
87
  if (extra > 0) {
83
- rows.push({ file: '', line: 0, context: chalk.dim(`(+${extra} more in this file)`), occurrences: 0 })
88
+ rows.push({file: '', line: 0, context: chalk.dim(`(+${extra} more in this file)`), occurrences: 0})
84
89
  }
85
90
  }
86
91
 
87
92
  const q = args.term.toLowerCase()
88
- this.log(renderTable(rows, [
89
- { header: 'File', key: 'file', width: 35 },
90
- { header: 'Line', key: 'line', width: 5, format: (v) => Number(v) === 0 ? '' : String(v) },
91
- { header: 'Context', key: 'context', width: 65, format: (v) => {
92
- const s = String(v)
93
- // highlight term
94
- const re = new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')
95
- return s.replace(re, (m) => chalk.yellow.bold(m))
96
- }},
97
- { header: 'Matches', key: 'occurrences', width: 8, format: (v) => Number(v) === 0 ? '' : `${v}` },
98
- ]))
93
+ this.log(
94
+ renderTable(rows, [
95
+ {header: 'File', key: 'file', width: 35},
96
+ {header: 'Line', key: 'line', width: 5, format: (v) => (Number(v) === 0 ? '' : String(v))},
97
+ {
98
+ header: 'Context',
99
+ key: 'context',
100
+ width: 65,
101
+ format: (v) => {
102
+ const s = String(v)
103
+ // highlight term
104
+ const re = new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')
105
+ return s.replace(re, (m) => chalk.yellow.bold(m))
106
+ },
107
+ },
108
+ {header: 'Matches', key: 'occurrences', width: 8, format: (v) => (Number(v) === 0 ? '' : `${v}`)},
109
+ ]),
110
+ )
99
111
  this.log('')
100
112
 
101
- return { repo, owner, term: args.term, matches, total: matches.length }
113
+ return {repo, owner, term: args.term, matches, total: matches.length}
102
114
  }
103
115
  }
@@ -1,10 +1,10 @@
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 { typewriterLine } from '../utils/typewriter.js'
5
- import { which, exec } from '../services/shell.js'
6
- import { checkGitHubAuth, checkAWSAuth } from '../services/auth.js'
7
- import { formatDoctorCheck, formatDoctorSummary } from '../formatters/status.js'
4
+ import {typewriterLine} from '../utils/typewriter.js'
5
+ import {which, exec} from '../services/shell.js'
6
+ import {checkGitHubAuth, checkAWSAuth} from '../services/auth.js'
7
+ import {formatDoctorCheck, formatDoctorSummary} from '../formatters/status.js'
8
8
 
9
9
  /** @import { DoctorCheck } from '../types.js' */
10
10
 
@@ -20,28 +20,30 @@ export default class Doctor extends Command {
20
20
  static enableJsonFlag = true
21
21
 
22
22
  static flags = {
23
- verbose: Flags.boolean({ description: 'Mostra dettagli aggiuntivi', default: false }),
23
+ verbose: Flags.boolean({description: 'Mostra dettagli aggiuntivi', default: false}),
24
24
  }
25
25
 
26
26
  async run() {
27
- const { flags } = await this.parse(Doctor)
27
+ const {flags} = await this.parse(Doctor)
28
28
  const isJson = flags.json
29
29
 
30
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Running diagnostics...') }).start()
30
+ const spinner = isJson
31
+ ? null
32
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Running diagnostics...')}).start()
31
33
 
32
34
  /** @type {DoctorCheck[]} */
33
35
  const checks = []
34
36
 
35
37
  // Software checks
36
38
  const softwareChecks = [
37
- { name: 'Node.js', cmd: 'node', args: ['--version'], required: '>=24' },
38
- { name: 'nvm', cmd: 'nvm', args: ['--version'], required: null },
39
- { name: 'npm', cmd: 'npm', args: ['--version'], required: null },
40
- { name: 'Git', cmd: 'git', args: ['--version'], required: null },
41
- { name: 'gh CLI', cmd: 'gh', args: ['--version'], required: null },
42
- { name: 'Docker', cmd: 'docker', args: ['--version'], required: null },
43
- { name: 'AWS CLI', cmd: 'aws', args: ['--version'], required: null },
44
- { name: 'aws-vault', cmd: 'aws-vault', args: ['--version'], required: null },
39
+ {name: 'Node.js', cmd: 'node', args: ['--version'], required: '>=24'},
40
+ {name: 'nvm', cmd: 'nvm', args: ['--version'], required: null},
41
+ {name: 'npm', cmd: 'npm', args: ['--version'], required: null},
42
+ {name: 'Git', cmd: 'git', args: ['--version'], required: null},
43
+ {name: 'gh CLI', cmd: 'gh', args: ['--version'], required: null},
44
+ {name: 'Docker', cmd: 'docker', args: ['--version'], required: null},
45
+ {name: 'AWS CLI', cmd: 'aws', args: ['--version'], required: null},
46
+ {name: 'aws-vault', cmd: 'aws-vault', args: ['--version'], required: null},
45
47
  ]
46
48
 
47
49
  for (const check of softwareChecks) {
@@ -68,23 +70,23 @@ export default class Doctor extends Command {
68
70
  }
69
71
 
70
72
  // Auth checks
71
- const ghAuth = await checkGitHubAuth()
72
- checks.push({
73
- name: 'GitHub auth',
74
- status: ghAuth.authenticated ? 'ok' : 'fail',
75
- version: ghAuth.authenticated ? ghAuth.username ?? null : null,
76
- required: null,
77
- hint: ghAuth.authenticated ? null : 'Run `dvmi auth login`',
78
- })
79
-
80
- const awsAuth = await checkAWSAuth()
81
- checks.push({
82
- name: 'AWS auth',
83
- status: awsAuth.authenticated ? 'ok' : 'warn',
84
- version: awsAuth.authenticated ? awsAuth.account ?? null : null,
85
- required: null,
86
- hint: awsAuth.authenticated ? null : 'Run `dvmi auth login --aws`',
87
- })
73
+ const ghAuth = await checkGitHubAuth()
74
+ checks.push({
75
+ name: 'GitHub auth',
76
+ status: ghAuth.authenticated ? 'ok' : 'fail',
77
+ version: ghAuth.authenticated ? (ghAuth.username ?? null) : null,
78
+ required: null,
79
+ hint: ghAuth.authenticated ? null : 'Run `dvmi auth login`',
80
+ })
81
+
82
+ const awsAuth = await checkAWSAuth()
83
+ checks.push({
84
+ name: 'AWS auth',
85
+ status: awsAuth.authenticated ? 'ok' : 'warn',
86
+ version: awsAuth.authenticated ? (awsAuth.account ?? null) : null,
87
+ required: null,
88
+ hint: awsAuth.authenticated ? null : 'Run `dvmi auth login --aws`',
89
+ })
88
90
 
89
91
  spinner?.stop()
90
92
 
@@ -94,7 +96,7 @@ export default class Doctor extends Command {
94
96
  fail: checks.filter((c) => c.status === 'fail').length,
95
97
  }
96
98
 
97
- if (isJson) return { checks, summary }
99
+ if (isJson) return {checks, summary}
98
100
 
99
101
  await typewriterLine('Environment Diagnostics')
100
102
  for (const check of checks) {
@@ -110,6 +112,6 @@ export default class Doctor extends Command {
110
112
  }
111
113
  }
112
114
 
113
- return { checks, summary }
115
+ return {checks, summary}
114
116
  }
115
117
  }
@@ -1,8 +1,8 @@
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 { checkbox, confirm, input } from '@inquirer/prompts'
5
- import { detectPlatform } from '../../services/platform.js'
4
+ import {checkbox, confirm, input} from '@inquirer/prompts'
5
+ import {detectPlatform} from '../../services/platform.js'
6
6
  import {
7
7
  isChezmoiInstalled,
8
8
  getManagedFiles,
@@ -11,13 +11,13 @@ import {
11
11
  isPathSensitive,
12
12
  isWSLWindowsPath,
13
13
  } from '../../services/dotfiles.js'
14
- import { loadConfig } from '../../services/config.js'
15
- import { execOrThrow } from '../../services/shell.js'
16
- import { formatDotfilesAdd } from '../../formatters/dotfiles.js'
17
- import { DvmiError } from '../../utils/errors.js'
18
- import { homedir } from 'node:os'
19
- import { join } from 'node:path'
20
- import { existsSync } from 'node:fs'
14
+ import {loadConfig} from '../../services/config.js'
15
+ import {execOrThrow} from '../../services/shell.js'
16
+ import {formatDotfilesAdd} from '../../formatters/dotfiles.js'
17
+ import {DvmiError} from '../../utils/errors.js'
18
+ import {homedir} from 'node:os'
19
+ import {join} from 'node:path'
20
+ import {existsSync} from 'node:fs'
21
21
 
22
22
  /** @import { DotfilesAddResult } from '../../types.js' */
23
23
 
@@ -47,13 +47,13 @@ export default class DotfilesAdd extends Command {
47
47
  static enableJsonFlag = true
48
48
 
49
49
  static flags = {
50
- help: Flags.help({ char: 'h' }),
51
- encrypt: Flags.boolean({ char: 'e', description: 'Force encryption for all files being added', default: false }),
52
- 'no-encrypt': Flags.boolean({ description: 'Disable auto-encryption (add all as plaintext)', default: false }),
50
+ help: Flags.help({char: 'h'}),
51
+ encrypt: Flags.boolean({char: 'e', description: 'Force encryption for all files being added', default: false}),
52
+ 'no-encrypt': Flags.boolean({description: 'Disable auto-encryption (add all as plaintext)', default: false}),
53
53
  }
54
54
 
55
55
  static args = {
56
- files: Args.string({ description: 'File paths to add', required: false }),
56
+ files: Args.string({description: 'File paths to add', required: false}),
57
57
  }
58
58
 
59
59
  // oclif does not support variadic args natively via Args.string for multiple values;
@@ -61,7 +61,7 @@ export default class DotfilesAdd extends Command {
61
61
  static strict = false
62
62
 
63
63
  async run() {
64
- const { flags } = await this.parse(DotfilesAdd)
64
+ const {flags} = await this.parse(DotfilesAdd)
65
65
  const isJson = flags.json
66
66
  const forceEncrypt = flags.encrypt
67
67
  const forceNoEncrypt = flags['no-encrypt']
@@ -73,23 +73,21 @@ export default class DotfilesAdd extends Command {
73
73
  // Pre-checks
74
74
  const config = await loadConfig()
75
75
  if (!config.dotfiles?.enabled) {
76
- throw new DvmiError(
77
- 'Chezmoi dotfiles management is not configured',
78
- 'Run `dvmi dotfiles setup` first',
79
- )
76
+ throw new DvmiError('Chezmoi dotfiles management is not configured', 'Run `dvmi dotfiles setup` first')
80
77
  }
81
78
 
82
79
  const chezmoiInstalled = await isChezmoiInstalled()
83
80
  if (!chezmoiInstalled) {
84
81
  const platformInfo = await detectPlatform()
85
- const hint = platformInfo.platform === 'macos'
86
- ? 'Run `brew install chezmoi` or visit https://chezmoi.io/install'
87
- : 'Run `sh -c "$(curl -fsLS get.chezmoi.io)"` or visit https://chezmoi.io/install'
82
+ const hint =
83
+ platformInfo.platform === 'macos'
84
+ ? 'Run `brew install chezmoi` or visit https://chezmoi.io/install'
85
+ : 'Run `sh -c "$(curl -fsLS get.chezmoi.io)"` or visit https://chezmoi.io/install'
88
86
  throw new DvmiError('chezmoi is not installed', hint)
89
87
  }
90
88
 
91
89
  const platformInfo = await detectPlatform()
92
- const { platform } = platformInfo
90
+ const {platform} = platformInfo
93
91
  const sensitivePatterns = getSensitivePatterns(config)
94
92
 
95
93
  // Get already-managed files for V-007 check
@@ -97,7 +95,7 @@ export default class DotfilesAdd extends Command {
97
95
  const managedPaths = new Set(managedFiles.map((f) => f.path))
98
96
 
99
97
  /** @type {DotfilesAddResult} */
100
- const result = { added: [], skipped: [], rejected: [] }
98
+ const result = {added: [], skipped: [], rejected: []}
101
99
 
102
100
  if (fileArgs.length > 0) {
103
101
  // Direct mode — files provided as arguments
@@ -107,19 +105,22 @@ export default class DotfilesAdd extends Command {
107
105
 
108
106
  // V-002: WSL2 Windows path rejection
109
107
  if (platform === 'wsl2' && isWSLWindowsPath(absPath)) {
110
- result.rejected.push({ path: displayPath, reason: 'Windows filesystem paths not supported on WSL2. Use Linux-native paths (~/) instead.' })
108
+ result.rejected.push({
109
+ path: displayPath,
110
+ reason: 'Windows filesystem paths not supported on WSL2. Use Linux-native paths (~/) instead.',
111
+ })
111
112
  continue
112
113
  }
113
114
 
114
115
  // V-001: file must exist
115
116
  if (!existsSync(absPath)) {
116
- result.skipped.push({ path: displayPath, reason: 'File not found' })
117
+ result.skipped.push({path: displayPath, reason: 'File not found'})
117
118
  continue
118
119
  }
119
120
 
120
121
  // V-007: not already managed
121
122
  if (managedPaths.has(absPath)) {
122
- result.skipped.push({ path: displayPath, reason: 'Already managed by chezmoi' })
123
+ result.skipped.push({path: displayPath, reason: 'Already managed by chezmoi'})
123
124
  continue
124
125
  }
125
126
 
@@ -138,9 +139,12 @@ export default class DotfilesAdd extends Command {
138
139
  if (encrypt) args.push('--encrypt')
139
140
  args.push(absPath)
140
141
  await execOrThrow('chezmoi', args)
141
- result.added.push({ path: displayPath, encrypted: encrypt })
142
+ result.added.push({path: displayPath, encrypted: encrypt})
142
143
  } catch {
143
- result.skipped.push({ path: displayPath, reason: `Failed to add to chezmoi. Run \`chezmoi doctor\` to verify your setup.` })
144
+ result.skipped.push({
145
+ path: displayPath,
146
+ reason: `Failed to add to chezmoi. Run \`chezmoi doctor\` to verify your setup.`,
147
+ })
144
148
  }
145
149
  }
146
150
 
@@ -161,11 +165,15 @@ export default class DotfilesAdd extends Command {
161
165
  if (isCI || isNonInteractive) {
162
166
  this.error(
163
167
  'This command requires an interactive terminal (TTY) when no files are specified. Provide file paths as arguments or run with --json.',
164
- { exit: 1 },
168
+ {exit: 1},
165
169
  )
166
170
  }
167
171
 
168
- const spinner = ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Loading recommended files...') }).start()
172
+ const spinner = ora({
173
+ spinner: 'arc',
174
+ color: false,
175
+ text: chalk.hex('#FF6B2B')('Loading recommended files...'),
176
+ }).start()
169
177
  const recommended = getDefaultFileList(platform)
170
178
  spinner.stop()
171
179
 
@@ -191,9 +199,9 @@ export default class DotfilesAdd extends Command {
191
199
  })
192
200
 
193
201
  // Offer custom file
194
- const addCustom = await confirm({ message: 'Add a custom file path?', default: false })
202
+ const addCustom = await confirm({message: 'Add a custom file path?', default: false})
195
203
  if (addCustom) {
196
- const customPath = await input({ message: 'Enter file path:' })
204
+ const customPath = await input({message: 'Enter file path:'})
197
205
  if (customPath.trim()) selected.push(customPath.trim())
198
206
  }
199
207
 
@@ -202,24 +210,28 @@ export default class DotfilesAdd extends Command {
202
210
  return result
203
211
  }
204
212
 
205
- const addSpinner = ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Adding files to chezmoi...') }).start()
213
+ const addSpinner = ora({
214
+ spinner: 'arc',
215
+ color: false,
216
+ text: chalk.hex('#FF6B2B')('Adding files to chezmoi...'),
217
+ }).start()
206
218
  addSpinner.stop()
207
219
 
208
220
  for (const rawPath of selected) {
209
221
  const absPath = expandTilde(rawPath)
210
222
 
211
223
  if (platform === 'wsl2' && isWSLWindowsPath(absPath)) {
212
- result.rejected.push({ path: rawPath, reason: 'Windows filesystem paths not supported on WSL2' })
224
+ result.rejected.push({path: rawPath, reason: 'Windows filesystem paths not supported on WSL2'})
213
225
  continue
214
226
  }
215
227
 
216
228
  if (!existsSync(absPath)) {
217
- result.skipped.push({ path: rawPath, reason: 'File not found' })
229
+ result.skipped.push({path: rawPath, reason: 'File not found'})
218
230
  continue
219
231
  }
220
232
 
221
233
  if (managedPaths.has(absPath)) {
222
- result.skipped.push({ path: rawPath, reason: 'Already managed by chezmoi' })
234
+ result.skipped.push({path: rawPath, reason: 'Already managed by chezmoi'})
223
235
  continue
224
236
  }
225
237
 
@@ -237,9 +249,9 @@ export default class DotfilesAdd extends Command {
237
249
  if (encrypt) args.push('--encrypt')
238
250
  args.push(absPath)
239
251
  await execOrThrow('chezmoi', args)
240
- result.added.push({ path: rawPath, encrypted: encrypt })
252
+ result.added.push({path: rawPath, encrypted: encrypt})
241
253
  } catch {
242
- result.skipped.push({ path: rawPath, reason: `Failed to add. Run \`chezmoi doctor\` to verify your setup.` })
254
+ result.skipped.push({path: rawPath, reason: `Failed to add. Run \`chezmoi doctor\` to verify your setup.`})
243
255
  }
244
256
  }
245
257
 
@@ -1,30 +1,27 @@
1
- import { Command, Flags } from '@oclif/core'
1
+ import {Command, Flags} from '@oclif/core'
2
2
  import ora from 'ora'
3
3
  import chalk from 'chalk'
4
- import { confirm } from '@inquirer/prompts'
5
- import { detectPlatform } from '../../services/platform.js'
6
- import { isChezmoiInstalled, getChezmoiConfig, buildSetupSteps } from '../../services/dotfiles.js'
7
- import { formatDotfilesSetup } from '../../formatters/dotfiles.js'
8
- import { DvmiError } from '../../utils/errors.js'
4
+ import {confirm} from '@inquirer/prompts'
5
+ import {detectPlatform} from '../../services/platform.js'
6
+ import {isChezmoiInstalled, getChezmoiConfig, buildSetupSteps} from '../../services/dotfiles.js'
7
+ import {formatDotfilesSetup} from '../../formatters/dotfiles.js'
8
+ import {DvmiError} from '../../utils/errors.js'
9
9
 
10
10
  /** @import { DotfilesSetupResult, SetupStep, StepResult } from '../../types.js' */
11
11
 
12
12
  export default class DotfilesSetup extends Command {
13
13
  static description = 'Interactive wizard to configure chezmoi with age encryption for dotfile management'
14
14
 
15
- static examples = [
16
- '<%= config.bin %> dotfiles setup',
17
- '<%= config.bin %> dotfiles setup --json',
18
- ]
15
+ static examples = ['<%= config.bin %> dotfiles setup', '<%= config.bin %> dotfiles setup --json']
19
16
 
20
17
  static enableJsonFlag = true
21
18
 
22
19
  static flags = {
23
- help: Flags.help({ char: 'h' }),
20
+ help: Flags.help({char: 'h'}),
24
21
  }
25
22
 
26
23
  async run() {
27
- const { flags } = await this.parse(DotfilesSetup)
24
+ const {flags} = await this.parse(DotfilesSetup)
28
25
  const isJson = flags.json
29
26
 
30
27
  // Non-interactive guard
@@ -33,12 +30,12 @@ export default class DotfilesSetup extends Command {
33
30
  if ((isCI || isNonInteractive) && !isJson) {
34
31
  this.error(
35
32
  'This command requires an interactive terminal (TTY). Run with --json for a non-interactive status check.',
36
- { exit: 1 },
33
+ {exit: 1},
37
34
  )
38
35
  }
39
36
 
40
37
  const platformInfo = await detectPlatform()
41
- const { platform } = platformInfo
38
+ const {platform} = platformInfo
42
39
 
43
40
  // --json branch: non-interactive setup attempt
44
41
  if (isJson) {
@@ -52,9 +49,10 @@ export default class DotfilesSetup extends Command {
52
49
  sourceDir: null,
53
50
  publicKey: null,
54
51
  status: 'failed',
55
- message: platform === 'macos'
56
- ? 'chezmoi is not installed. Run `brew install chezmoi` or visit https://chezmoi.io/install'
57
- : 'chezmoi is not installed. Run `sh -c "$(curl -fsLS get.chezmoi.io)"` or visit https://chezmoi.io/install',
52
+ message:
53
+ platform === 'macos'
54
+ ? 'chezmoi is not installed. Run `brew install chezmoi` or visit https://chezmoi.io/install'
55
+ : 'chezmoi is not installed. Run `sh -c "$(curl -fsLS get.chezmoi.io)"` or visit https://chezmoi.io/install',
58
56
  }
59
57
  }
60
58
 
@@ -76,15 +74,20 @@ export default class DotfilesSetup extends Command {
76
74
  // ---------------------------------------------------------------------------
77
75
  // Interactive mode
78
76
  // ---------------------------------------------------------------------------
79
- const preSpinner = ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking chezmoi status...') }).start()
77
+ const preSpinner = ora({
78
+ spinner: 'arc',
79
+ color: false,
80
+ text: chalk.hex('#FF6B2B')('Checking chezmoi status...'),
81
+ }).start()
80
82
  const chezmoiInstalled = await isChezmoiInstalled()
81
83
  const existingConfig = await getChezmoiConfig()
82
84
  preSpinner.stop()
83
85
 
84
86
  if (!chezmoiInstalled) {
85
- const hint = platform === 'macos'
86
- ? 'Run `brew install chezmoi` or visit https://chezmoi.io/install'
87
- : 'Run `sh -c "$(curl -fsLS get.chezmoi.io)"` or visit https://chezmoi.io/install'
87
+ const hint =
88
+ platform === 'macos'
89
+ ? 'Run `brew install chezmoi` or visit https://chezmoi.io/install'
90
+ : 'Run `sh -c "$(curl -fsLS get.chezmoi.io)"` or visit https://chezmoi.io/install'
88
91
  throw new DvmiError('chezmoi is not installed', hint)
89
92
  }
90
93
 
@@ -92,18 +95,26 @@ export default class DotfilesSetup extends Command {
92
95
  const hasEncryption = existingConfig?.encryption?.tool === 'age' || !!existingConfig?.age?.identity
93
96
  if (existingConfig && hasEncryption) {
94
97
  this.log(chalk.green(' ✔ chezmoi is already configured with age encryption'))
95
- const reconfigure = await confirm({ message: 'Reconfigure encryption (regenerate age key)?', default: false })
98
+ const reconfigure = await confirm({message: 'Reconfigure encryption (regenerate age key)?', default: false})
96
99
  if (!reconfigure) {
97
100
  const sourceDir = existingConfig?.sourceDir ?? existingConfig?.sourcePath ?? null
98
101
  this.log(chalk.dim(' Skipped. Existing encryption configuration kept.'))
99
- return { platform, chezmoiInstalled: true, encryptionConfigured: true, sourceDir, publicKey: null, status: 'skipped', message: 'Existing encryption configuration kept' }
102
+ return {
103
+ platform,
104
+ chezmoiInstalled: true,
105
+ encryptionConfigured: true,
106
+ sourceDir,
107
+ publicKey: null,
108
+ status: 'skipped',
109
+ message: 'Existing encryption configuration kept',
110
+ }
100
111
  }
101
112
  } else if (existingConfig) {
102
113
  this.log(chalk.yellow(' chezmoi is initialised but encryption is not configured — adding age encryption'))
103
114
  }
104
115
 
105
116
  // Build and run steps
106
- const steps = buildSetupSteps(platform, { existingConfig })
117
+ const steps = buildSetupSteps(platform, {existingConfig})
107
118
 
108
119
  this.log('')
109
120
 
@@ -111,24 +122,24 @@ export default class DotfilesSetup extends Command {
111
122
  let sourceDir = null
112
123
 
113
124
  for (const step of steps) {
114
- const typeColor = { check: chalk.blue, install: chalk.yellow, configure: chalk.cyan, verify: chalk.green }
125
+ const typeColor = {check: chalk.blue, install: chalk.yellow, configure: chalk.cyan, verify: chalk.green}
115
126
  const colorFn = typeColor[step.type] ?? chalk.white
116
127
  this.log(` ${colorFn(`[${step.type}]`)} ${step.label}`)
117
128
 
118
129
  if (step.requiresConfirmation) {
119
- const proceed = await confirm({ message: `Proceed with: ${step.label}?`, default: true })
130
+ const proceed = await confirm({message: `Proceed with: ${step.label}?`, default: true})
120
131
  if (!proceed) {
121
132
  this.log(chalk.dim(' Skipped.'))
122
133
  continue
123
134
  }
124
135
  }
125
136
 
126
- const stepSpinner = ora({ spinner: 'arc', color: false, text: chalk.dim(step.label) }).start()
137
+ const stepSpinner = ora({spinner: 'arc', color: false, text: chalk.dim(step.label)}).start()
127
138
  let result
128
139
  try {
129
140
  result = await step.run()
130
141
  } catch (err) {
131
- result = { status: /** @type {'failed'} */ ('failed'), hint: err instanceof Error ? err.message : String(err) }
142
+ result = {status: /** @type {'failed'} */ ('failed'), hint: err instanceof Error ? err.message : String(err)}
132
143
  }
133
144
 
134
145
  if (result.status === 'success') {
@@ -147,8 +158,26 @@ export default class DotfilesSetup extends Command {
147
158
  } else {
148
159
  stepSpinner.fail(chalk.red(`${step.label} — failed`))
149
160
  if (result.hint) this.log(chalk.dim(` → ${result.hint}`))
150
- this.log(formatDotfilesSetup({ platform, chezmoiInstalled: true, encryptionConfigured: false, sourceDir: null, publicKey: null, status: 'failed', message: result.hint }))
151
- return { platform, chezmoiInstalled: true, encryptionConfigured: false, sourceDir: null, publicKey: null, status: 'failed', message: result.hint }
161
+ this.log(
162
+ formatDotfilesSetup({
163
+ platform,
164
+ chezmoiInstalled: true,
165
+ encryptionConfigured: false,
166
+ sourceDir: null,
167
+ publicKey: null,
168
+ status: 'failed',
169
+ message: result.hint,
170
+ }),
171
+ )
172
+ return {
173
+ platform,
174
+ chezmoiInstalled: true,
175
+ encryptionConfigured: false,
176
+ sourceDir: null,
177
+ publicKey: null,
178
+ status: 'failed',
179
+ message: result.hint,
180
+ }
152
181
  }
153
182
  }
154
183
 
@@ -161,9 +190,9 @@ export default class DotfilesSetup extends Command {
161
190
  // Try to get public key from key file
162
191
  if (!publicKey) {
163
192
  try {
164
- const { homedir } = await import('node:os')
165
- const { join } = await import('node:path')
166
- const { readFile } = await import('node:fs/promises')
193
+ const {homedir} = await import('node:os')
194
+ const {join} = await import('node:path')
195
+ const {readFile} = await import('node:fs/promises')
167
196
  const keyPath = join(homedir(), '.config', 'chezmoi', 'key.txt')
168
197
  const keyContent = await readFile(keyPath, 'utf8').catch(() => '')
169
198
  const match = keyContent.match(/# public key: (age1[a-z0-9]+)/i)