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,10 +1,10 @@
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 { select } from '@inquirer/prompts'
5
- import { listPrompts } from '../../services/prompts.js'
6
- import { formatPromptTable, formatPromptBody } from '../../formatters/prompts.js'
7
- import { DvmiError } from '../../utils/errors.js'
4
+ import {select} from '@inquirer/prompts'
5
+ import {listPrompts} from '../../services/prompts.js'
6
+ import {formatPromptTable, formatPromptBody} from '../../formatters/prompts.js'
7
+ import {DvmiError} from '../../utils/errors.js'
8
8
 
9
9
  /** @import { Prompt } from '../../types.js' */
10
10
 
@@ -27,7 +27,7 @@ export default class PromptsList extends Command {
27
27
  }
28
28
 
29
29
  async run() {
30
- const { flags } = await this.parse(PromptsList)
30
+ const {flags} = await this.parse(PromptsList)
31
31
  const isJson = flags.json
32
32
 
33
33
  const spinner = isJson
@@ -45,7 +45,7 @@ export default class PromptsList extends Command {
45
45
  } catch (err) {
46
46
  spinner?.fail()
47
47
  if (err instanceof DvmiError) {
48
- this.error(err.message, { exit: err.exitCode, suggestions: [err.hint] })
48
+ this.error(err.message, {exit: err.exitCode, suggestions: [err.hint]})
49
49
  }
50
50
  throw err
51
51
  }
@@ -65,7 +65,7 @@ export default class PromptsList extends Command {
65
65
  : prompts
66
66
 
67
67
  if (isJson) {
68
- return { prompts: filtered, total: filtered.length }
68
+ return {prompts: filtered, total: filtered.length}
69
69
  }
70
70
 
71
71
  if (filtered.length === 0) {
@@ -73,7 +73,7 @@ export default class PromptsList extends Command {
73
73
  ? chalk.dim(`No prompts matching "${flags.filter}".`)
74
74
  : chalk.yellow('No prompts found in the repository.')
75
75
  this.log(msg)
76
- return { prompts: [], total: 0 }
76
+ return {prompts: [], total: 0}
77
77
  }
78
78
 
79
79
  const filterInfo = query ? chalk.dim(` — filter: ${chalk.white(`"${flags.filter}"`)}`) : ''
@@ -87,8 +87,8 @@ export default class PromptsList extends Command {
87
87
 
88
88
  // Interactive selection to view full prompt content
89
89
  try {
90
- const choices = filtered.map((p) => ({ name: p.title, value: p }))
91
- choices.push({ name: chalk.dim('← Exit'), value: /** @type {Prompt} */ (null) })
90
+ const choices = filtered.map((p) => ({name: p.title, value: p}))
91
+ choices.push({name: chalk.dim('← Exit'), value: /** @type {Prompt} */ (null)})
92
92
 
93
93
  const selected = await select({
94
94
  message: 'Select a prompt to view its content (or Exit):',
@@ -102,6 +102,6 @@ export default class PromptsList extends Command {
102
102
  // User pressed Ctrl+C — exit gracefully
103
103
  }
104
104
 
105
- return { prompts: filtered, total: filtered.length }
105
+ return {prompts: filtered, total: filtered.length}
106
106
  }
107
107
  }
@@ -1,12 +1,12 @@
1
- import { Command, Args, Flags } from '@oclif/core'
1
+ import {Command, Args, Flags} from '@oclif/core'
2
2
  import ora from 'ora'
3
3
  import chalk from 'chalk'
4
- import { select, confirm } from '@inquirer/prompts'
5
- import { join } from 'node:path'
6
- import { readdir } from 'node:fs/promises'
7
- import { resolveLocalPrompt, invokeTool, SUPPORTED_TOOLS } from '../../services/prompts.js'
8
- import { loadConfig } from '../../services/config.js'
9
- import { DvmiError } from '../../utils/errors.js'
4
+ import {select, confirm} from '@inquirer/prompts'
5
+ import {join} from 'node:path'
6
+ import {readdir} from 'node:fs/promises'
7
+ import {resolveLocalPrompt, invokeTool, SUPPORTED_TOOLS} from '../../services/prompts.js'
8
+ import {loadConfig} from '../../services/config.js'
9
+ import {DvmiError} from '../../utils/errors.js'
10
10
 
11
11
  /** @import { AITool } from '../../types.js' */
12
12
 
@@ -23,7 +23,7 @@ async function walkPrompts(dir, base) {
23
23
  const results = []
24
24
  let entries
25
25
  try {
26
- entries = await readdir(dir, { withFileTypes: true })
26
+ entries = await readdir(dir, {withFileTypes: true})
27
27
  } catch {
28
28
  return results
29
29
  }
@@ -67,7 +67,7 @@ export default class PromptsRun extends Command {
67
67
  }
68
68
 
69
69
  async run() {
70
- const { args, flags } = await this.parse(PromptsRun)
70
+ const {args, flags} = await this.parse(PromptsRun)
71
71
  const isJson = flags.json
72
72
 
73
73
  // Load config
@@ -78,8 +78,7 @@ export default class PromptsRun extends Command {
78
78
  /* use defaults */
79
79
  }
80
80
 
81
- const localDir =
82
- process.env.DVMI_PROMPTS_DIR ?? config.promptsDir ?? join(process.cwd(), DEFAULT_PROMPTS_DIR)
81
+ const localDir = process.env.DVMI_PROMPTS_DIR ?? config.promptsDir ?? join(process.cwd(), DEFAULT_PROMPTS_DIR)
83
82
 
84
83
  // Resolve tool: --tool flag > config.aiTool
85
84
  const toolName = /** @type {AITool | undefined} */ (flags.tool ?? config.aiTool)
@@ -111,7 +110,7 @@ export default class PromptsRun extends Command {
111
110
  prompt = await resolveLocalPrompt(args.path, localDir)
112
111
  } catch (err) {
113
112
  if (err instanceof DvmiError) {
114
- this.error(err.message, { exit: err.exitCode, suggestions: [err.hint] })
113
+ this.error(err.message, {exit: err.exitCode, suggestions: [err.hint]})
115
114
  }
116
115
  throw err
117
116
  }
@@ -142,7 +141,7 @@ export default class PromptsRun extends Command {
142
141
 
143
142
  relativePath = await select({
144
143
  message: 'Select a local prompt to run:',
145
- choices: localPaths.map((p) => ({ name: p, value: p })),
144
+ choices: localPaths.map((p) => ({name: p, value: p})),
146
145
  })
147
146
  }
148
147
 
@@ -168,7 +167,7 @@ export default class PromptsRun extends Command {
168
167
  } catch (err) {
169
168
  spinner.fail()
170
169
  if (err instanceof DvmiError) {
171
- this.error(err.message, { exit: err.exitCode, suggestions: [err.hint] })
170
+ this.error(err.message, {exit: err.exitCode, suggestions: [err.hint]})
172
171
  }
173
172
  throw err
174
173
  }
@@ -180,14 +179,12 @@ export default class PromptsRun extends Command {
180
179
  // This protects against prompt injection from tampered local files (originally
181
180
  // downloaded from remote repositories). Skipped in CI/non-interactive environments.
182
181
  if (!process.env.CI && process.stdin.isTTY) {
183
- const preview = prompt.body.length > 500
184
- ? prompt.body.slice(0, 500) + chalk.dim('\n…[truncated]')
185
- : prompt.body
182
+ const preview = prompt.body.length > 500 ? prompt.body.slice(0, 500) + chalk.dim('\n…[truncated]') : prompt.body
186
183
  this.log(chalk.yellow('Prompt preview:'))
187
184
  this.log(chalk.dim('─'.repeat(50)))
188
185
  this.log(chalk.dim(preview))
189
186
  this.log(chalk.dim('─'.repeat(50)) + '\n')
190
- const ok = await confirm({ message: `Run this prompt with ${toolName}?`, default: true })
187
+ const ok = await confirm({message: `Run this prompt with ${toolName}?`, default: true})
191
188
  if (!ok) {
192
189
  this.log(chalk.dim('Aborted.'))
193
190
  return
@@ -199,7 +196,7 @@ export default class PromptsRun extends Command {
199
196
  await invokeTool(toolName, prompt.body)
200
197
  } catch (err) {
201
198
  if (err instanceof DvmiError) {
202
- this.error(err.message, { exit: err.exitCode, suggestions: [err.hint] })
199
+ this.error(err.message, {exit: err.exitCode, suggestions: [err.hint]})
203
200
  }
204
201
  throw err
205
202
  }
@@ -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
3
  import chalk from 'chalk'
4
- import { listRepos } from '../../services/github.js'
5
- import { loadConfig } from '../../services/config.js'
6
- import { renderTable } from '../../formatters/table.js'
4
+ import {listRepos} from '../../services/github.js'
5
+ import {loadConfig} from '../../services/config.js'
6
+ import {renderTable} from '../../formatters/table.js'
7
7
 
8
8
  /**
9
9
  * @param {string} lang
@@ -13,22 +13,22 @@ function langColor(lang) {
13
13
  const map = {
14
14
  javascript: chalk.yellow,
15
15
  typescript: chalk.blue,
16
- python: chalk.green,
17
- java: chalk.red,
18
- go: chalk.cyan,
19
- ruby: chalk.magenta,
20
- rust: chalk.hex('#CE422B'),
21
- kotlin: chalk.hex('#7F52FF'),
22
- swift: chalk.hex('#F05138'),
23
- php: chalk.hex('#777BB4'),
24
- shell: chalk.greenBright,
16
+ python: chalk.green,
17
+ java: chalk.red,
18
+ go: chalk.cyan,
19
+ ruby: chalk.magenta,
20
+ rust: chalk.hex('#CE422B'),
21
+ kotlin: chalk.hex('#7F52FF'),
22
+ swift: chalk.hex('#F05138'),
23
+ php: chalk.hex('#777BB4'),
24
+ shell: chalk.greenBright,
25
25
  }
26
26
  const fn = map[lang.toLowerCase()]
27
27
  return fn ? fn(lang) : chalk.dim(lang)
28
28
  }
29
29
 
30
30
  export default class RepoList extends Command {
31
- static description = 'Lista repository dell\'organizzazione'
31
+ static description = "Lista repository dell'organizzazione"
32
32
 
33
33
  static examples = [
34
34
  '<%= config.bin %> repo list',
@@ -41,13 +41,13 @@ export default class RepoList extends Command {
41
41
  static enableJsonFlag = true
42
42
 
43
43
  static flags = {
44
- language: Flags.string({ description: 'Filtra per linguaggio' }),
45
- topic: Flags.string({ description: 'Filtra per topic' }),
46
- search: Flags.string({ char: 's', description: 'Cerca in nome e descrizione (case-insensitive)' }),
44
+ language: Flags.string({description: 'Filtra per linguaggio'}),
45
+ topic: Flags.string({description: 'Filtra per topic'}),
46
+ search: Flags.string({char: 's', description: 'Cerca in nome e descrizione (case-insensitive)'}),
47
47
  }
48
48
 
49
49
  async run() {
50
- const { flags } = await this.parse(RepoList)
50
+ const {flags} = await this.parse(RepoList)
51
51
  const isJson = flags.json
52
52
  const config = await loadConfig()
53
53
 
@@ -55,7 +55,9 @@ export default class RepoList extends Command {
55
55
  this.error('GitHub org not configured. Run `dvmi init` to set up your environment.')
56
56
  }
57
57
 
58
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching repositories...') }).start()
58
+ const spinner = isJson
59
+ ? null
60
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching repositories...')}).start()
59
61
  const repos = await listRepos(config.org, {
60
62
  language: flags.language,
61
63
  topic: flags.topic,
@@ -65,49 +67,63 @@ export default class RepoList extends Command {
65
67
  // Search filter (name + description)
66
68
  const searchQuery = flags.search?.toLowerCase()
67
69
  const filtered = searchQuery
68
- ? repos.filter((r) =>
69
- r.name.toLowerCase().includes(searchQuery) ||
70
- r.description.toLowerCase().includes(searchQuery),
70
+ ? repos.filter(
71
+ (r) => r.name.toLowerCase().includes(searchQuery) || r.description.toLowerCase().includes(searchQuery),
71
72
  )
72
73
  : repos
73
74
 
74
- if (isJson) return { repositories: filtered, total: filtered.length }
75
+ if (isJson) return {repositories: filtered, total: filtered.length}
75
76
 
76
77
  if (repos.length === 0) {
77
78
  this.log(chalk.yellow('No repositories found matching your filters.'))
78
- return { repositories: [], total: 0 }
79
+ return {repositories: [], total: 0}
79
80
  }
80
81
 
81
82
  if (filtered.length === 0) {
82
83
  this.log(chalk.dim(`No repositories matching "${flags.search}".`))
83
- return { repositories: [], total: 0 }
84
+ return {repositories: [], total: 0}
84
85
  }
85
86
 
86
87
  // Build filter info line
87
88
  const filterInfo = [
88
89
  flags.language && chalk.dim(`language: ${chalk.white(flags.language)}`),
89
- flags.topic && chalk.dim(`topic: ${chalk.white(flags.topic)}`),
90
- flags.search && chalk.dim(`search: ${chalk.white(`"${flags.search}"`)}`),
91
- ].filter(Boolean).join(chalk.dim(' · '))
90
+ flags.topic && chalk.dim(`topic: ${chalk.white(flags.topic)}`),
91
+ flags.search && chalk.dim(`search: ${chalk.white(`"${flags.search}"`)}`),
92
+ ]
93
+ .filter(Boolean)
94
+ .join(chalk.dim(' · '))
92
95
 
93
96
  this.log(
94
97
  chalk.bold(`\nRepositories in ${config.org}`) +
95
- (filterInfo ? chalk.dim(' — ') + filterInfo : '') +
96
- chalk.dim(` (${filtered.length}${filtered.length < repos.length ? `/${repos.length}` : ''})`) +
97
- '\n',
98
+ (filterInfo ? chalk.dim(' — ') + filterInfo : '') +
99
+ chalk.dim(` (${filtered.length}${filtered.length < repos.length ? `/${repos.length}` : ''})`) +
100
+ '\n',
98
101
  )
99
102
 
100
- this.log(renderTable(filtered, [
101
- { header: 'Name', key: 'name', width: 40 },
102
- { header: 'Language', key: 'language', width: 14, format: (v) => v || '—', colorize: (v) => v === '—' ? chalk.dim(v) : langColor(v) },
103
- { header: 'Last push', key: 'pushedAt', width: 12, format: (v) => {
104
- const d = new Date(String(v))
105
- return isNaN(d.getTime()) ? '' : d.toLocaleDateString()
106
- }},
107
- { header: 'Description', key: 'description', width: 60, format: (v) => String(v || '—') },
108
- ]))
103
+ this.log(
104
+ renderTable(filtered, [
105
+ {header: 'Name', key: 'name', width: 40},
106
+ {
107
+ header: 'Language',
108
+ key: 'language',
109
+ width: 14,
110
+ format: (v) => v || '—',
111
+ colorize: (v) => (v === '—' ? chalk.dim(v) : langColor(v)),
112
+ },
113
+ {
114
+ header: 'Last push',
115
+ key: 'pushedAt',
116
+ width: 12,
117
+ format: (v) => {
118
+ const d = new Date(String(v))
119
+ return isNaN(d.getTime()) ? '—' : d.toLocaleDateString()
120
+ },
121
+ },
122
+ {header: 'Description', key: 'description', width: 60, format: (v) => String(v || '—')},
123
+ ]),
124
+ )
109
125
 
110
126
  this.log('')
111
- return { repositories: filtered, total: filtered.length }
127
+ return {repositories: filtered, total: filtered.length}
112
128
  }
113
129
  }
@@ -1,12 +1,12 @@
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 { searchCode } from '../services/github.js'
5
- import { loadConfig } from '../services/config.js'
6
- import { renderTable } from '../formatters/table.js'
4
+ import {searchCode} from '../services/github.js'
5
+ import {loadConfig} from '../services/config.js'
6
+ import {renderTable} from '../formatters/table.js'
7
7
 
8
8
  export default class Search extends Command {
9
- static description = 'Cerca codice nei repository dell\'organizzazione'
9
+ static description = "Cerca codice nei repository dell'organizzazione"
10
10
 
11
11
  static examples = [
12
12
  '<%= config.bin %> search "getUserById"',
@@ -17,17 +17,17 @@ export default class Search extends Command {
17
17
  static enableJsonFlag = true
18
18
 
19
19
  static args = {
20
- term: Args.string({ description: 'Termine di ricerca', required: true }),
20
+ term: Args.string({description: 'Termine di ricerca', required: true}),
21
21
  }
22
22
 
23
23
  static flags = {
24
- language: Flags.string({ description: 'Filtra per linguaggio' }),
25
- repo: Flags.string({ description: 'Cerca in un repo specifico' }),
26
- limit: Flags.integer({ description: 'Max risultati', default: 20 }),
24
+ language: Flags.string({description: 'Filtra per linguaggio'}),
25
+ repo: Flags.string({description: 'Cerca in un repo specifico'}),
26
+ limit: Flags.integer({description: 'Max risultati', default: 20}),
27
27
  }
28
28
 
29
29
  async run() {
30
- const { args, flags } = await this.parse(Search)
30
+ const {args, flags} = await this.parse(Search)
31
31
  const isJson = flags.json
32
32
  const config = await loadConfig()
33
33
 
@@ -43,20 +43,22 @@ export default class Search extends Command {
43
43
  })
44
44
  spinner?.stop()
45
45
 
46
- if (isJson) return { results, total: results.length }
46
+ if (isJson) return {results, total: results.length}
47
47
 
48
48
  if (results.length === 0) {
49
49
  this.log(chalk.yellow(`No results found for "${args.term}" in the organization.`))
50
- return { results: [], total: 0 }
50
+ return {results: [], total: 0}
51
51
  }
52
52
 
53
53
  this.log(chalk.bold(`\n${results.length} result(s) for "${args.term}":\n`))
54
- this.log(renderTable(results, [
55
- { header: 'Repo', key: 'repo', width: 25 },
56
- { header: 'File', key: 'file', width: 45 },
57
- { header: 'Match', key: 'match' },
58
- ]))
54
+ this.log(
55
+ renderTable(results, [
56
+ {header: 'Repo', key: 'repo', width: 25},
57
+ {header: 'File', key: 'file', width: 45},
58
+ {header: 'Match', key: 'match'},
59
+ ]),
60
+ )
59
61
 
60
- return { results, total: results.length }
62
+ return {results, total: results.length}
61
63
  }
62
64
  }
@@ -1,30 +1,28 @@
1
- import { Command, Flags } from '@oclif/core'
2
- import { confirm, select } from '@inquirer/prompts'
1
+ import {Command, Flags} from '@oclif/core'
2
+ import {confirm, select} from '@inquirer/prompts'
3
3
  import ora from 'ora'
4
4
  import chalk from 'chalk'
5
- import { execa } from 'execa'
6
- import { detectPlatform } from '../../services/platform.js'
7
- import { exec } from '../../services/shell.js'
8
- import { buildSteps, checkToolStatus, listGpgKeys, deriveOverallStatus } from '../../services/security.js'
9
- import { formatEducationalIntro, formatStepHeader, formatSecuritySummary } from '../../formatters/security.js'
5
+ import {execa} from 'execa'
6
+ import {detectPlatform} from '../../services/platform.js'
7
+ import {exec} from '../../services/shell.js'
8
+ import {buildSteps, checkToolStatus, listGpgKeys, deriveOverallStatus} from '../../services/security.js'
9
+ import {formatEducationalIntro, formatStepHeader, formatSecuritySummary} from '../../formatters/security.js'
10
10
  /** @import { SetupSession, SetupStep, StepResult, PlatformInfo } from '../../types.js' */
11
11
 
12
12
  export default class SecuritySetup extends Command {
13
- static description = 'Interactive wizard to install and configure credential protection tools (aws-vault, pass, GPG, Git Credential Manager, macOS Keychain)'
13
+ static description =
14
+ 'Interactive wizard to install and configure credential protection tools (aws-vault, pass, GPG, Git Credential Manager, macOS Keychain)'
14
15
 
15
- static examples = [
16
- '<%= config.bin %> security setup',
17
- '<%= config.bin %> security setup --json',
18
- ]
16
+ static examples = ['<%= config.bin %> security setup', '<%= config.bin %> security setup --json']
19
17
 
20
18
  static enableJsonFlag = true
21
19
 
22
20
  static flags = {
23
- help: Flags.help({ char: 'h' }),
21
+ help: Flags.help({char: 'h'}),
24
22
  }
25
23
 
26
24
  async run() {
27
- const { flags } = await this.parse(SecuritySetup)
25
+ const {flags} = await this.parse(SecuritySetup)
28
26
  const isJson = flags.json
29
27
 
30
28
  // FR-018: Detect non-interactive environments
@@ -34,22 +32,19 @@ export default class SecuritySetup extends Command {
34
32
  if ((isCI || isNonInteractive) && !isJson) {
35
33
  this.error(
36
34
  'This command requires an interactive terminal (TTY). Run with --json for a non-interactive health check.',
37
- { exit: 1 },
35
+ {exit: 1},
38
36
  )
39
37
  }
40
38
 
41
39
  // Detect platform
42
40
  const platformInfo = await detectPlatform()
43
- const { platform } = platformInfo
41
+ const {platform} = platformInfo
44
42
 
45
43
  // FR-019: Sudo pre-flight on Linux/WSL2
46
44
  if (platform !== 'macos' && !isJson) {
47
45
  const sudoCheck = await exec('sudo', ['-n', 'true'])
48
46
  if (sudoCheck.exitCode !== 0) {
49
- this.error(
50
- 'sudo access is required to install packages. Run `sudo -v` to authenticate and retry.',
51
- { exit: 1 },
52
- )
47
+ this.error('sudo access is required to install packages. Run `sudo -v` to authenticate and retry.', {exit: 1})
53
48
  }
54
49
  }
55
50
 
@@ -68,7 +63,11 @@ export default class SecuritySetup extends Command {
68
63
  // ---------------------------------------------------------------------------
69
64
  // Pre-check: show current tool status
70
65
  // ---------------------------------------------------------------------------
71
- const spinner = ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking current tool status...') }).start()
66
+ const spinner = ora({
67
+ spinner: 'arc',
68
+ color: false,
69
+ text: chalk.hex('#FF6B2B')('Checking current tool status...'),
70
+ }).start()
72
71
  const currentStatus = await checkToolStatus(platform)
73
72
  spinner.stop()
74
73
 
@@ -100,7 +99,7 @@ export default class SecuritySetup extends Command {
100
99
  })
101
100
  if (!understood) {
102
101
  this.log('Setup cancelled.')
103
- return { platform, selection: null, tools: currentStatus, overallStatus: deriveOverallStatus(currentStatus) }
102
+ return {platform, selection: null, tools: currentStatus, overallStatus: deriveOverallStatus(currentStatus)}
104
103
  }
105
104
 
106
105
  // ---------------------------------------------------------------------------
@@ -109,9 +108,9 @@ export default class SecuritySetup extends Command {
109
108
  const selectionValue = await select({
110
109
  message: 'What would you like to set up?',
111
110
  choices: [
112
- { name: 'Both AWS and Git credentials (recommended)', value: 'both' },
113
- { name: 'AWS credentials only (aws-vault)', value: 'aws' },
114
- { name: 'Git credentials only (macOS Keychain / GCM)', value: 'git' },
111
+ {name: 'Both AWS and Git credentials (recommended)', value: 'both'},
112
+ {name: 'AWS credentials only (aws-vault)', value: 'aws'},
113
+ {name: 'Git credentials only (macOS Keychain / GCM)', value: 'git'},
115
114
  ],
116
115
  })
117
116
 
@@ -131,7 +130,7 @@ export default class SecuritySetup extends Command {
131
130
  name: `${k.name} <${k.email}> (${k.id})`,
132
131
  value: k.id,
133
132
  })),
134
- { name: 'Create a new GPG key', value: '__new__' },
133
+ {name: 'Create a new GPG key', value: '__new__'},
135
134
  ]
136
135
  const chosen = await select({
137
136
  message: 'Select a GPG key for pass and Git Credential Manager:',
@@ -145,7 +144,7 @@ export default class SecuritySetup extends Command {
145
144
  // ---------------------------------------------------------------------------
146
145
  // Build steps
147
146
  // ---------------------------------------------------------------------------
148
- const steps = buildSteps(platformInfo, selection, { gpgId })
147
+ const steps = buildSteps(platformInfo, selection, {gpgId})
149
148
 
150
149
  /** @type {SetupSession} */
151
150
  const session = {
@@ -166,9 +165,9 @@ export default class SecuritySetup extends Command {
166
165
 
167
166
  // FR-014: confirmation prompt before system-level changes
168
167
  if (step.requiresConfirmation) {
169
- const proceed = await confirm({ message: `Proceed with: ${step.label}?`, default: true })
168
+ const proceed = await confirm({message: `Proceed with: ${step.label}?`, default: true})
170
169
  if (!proceed) {
171
- session.results.set(step.id, { status: 'skipped', message: 'Skipped by user' })
170
+ session.results.set(step.id, {status: 'skipped', message: 'Skipped by user'})
172
171
  this.log(chalk.dim(' Skipped.'))
173
172
  continue
174
173
  }
@@ -179,17 +178,17 @@ export default class SecuritySetup extends Command {
179
178
  this.log(chalk.cyan('\n GPG will now prompt you for a passphrase in your terminal.'))
180
179
  this.log(chalk.dim(' Follow the interactive prompts to complete key generation.\n'))
181
180
  try {
182
- await execa('gpg', ['--full-generate-key'], { stdio: 'inherit', reject: true })
181
+ await execa('gpg', ['--full-generate-key'], {stdio: 'inherit', reject: true})
183
182
  // Refresh the gpgId from newly created key
184
183
  const newKeys = await listGpgKeys()
185
184
  if (newKeys.length > 0) {
186
185
  gpgId = newKeys[0].id
187
186
  // gpgId is now set — subsequent step closures capture it via the shared context object
188
187
  }
189
- session.results.set(step.id, { status: 'success', message: `GPG key created (${gpgId || 'new key'})` })
188
+ session.results.set(step.id, {status: 'success', message: `GPG key created (${gpgId || 'new key'})`})
190
189
  this.log(chalk.green(' ✔ GPG key created'))
191
190
  } catch {
192
- const result = { status: /** @type {'failed'} */ ('failed'), hint: 'Run manually: gpg --full-generate-key' }
191
+ const result = {status: /** @type {'failed'} */ ('failed'), hint: 'Run manually: gpg --full-generate-key'}
193
192
  session.results.set(step.id, result)
194
193
  this.log(chalk.red(' ✗ GPG key creation failed'))
195
194
  this.log(chalk.dim(` → ${result.hint}`))
@@ -200,7 +199,7 @@ export default class SecuritySetup extends Command {
200
199
  }
201
200
 
202
201
  // Regular step with spinner
203
- const stepSpinner = ora({ spinner: 'arc', color: false, text: chalk.dim(step.label) }).start()
202
+ const stepSpinner = ora({spinner: 'arc', color: false, text: chalk.dim(step.label)}).start()
204
203
 
205
204
  let result
206
205
  try {
@@ -243,7 +242,12 @@ export default class SecuritySetup extends Command {
243
242
  platform,
244
243
  selection,
245
244
  tools: currentStatus,
246
- overallStatus: session.overallStatus === 'completed' ? 'success' : session.overallStatus === 'failed' ? 'partial' : 'not-configured',
245
+ overallStatus:
246
+ session.overallStatus === 'completed'
247
+ ? 'success'
248
+ : session.overallStatus === 'failed'
249
+ ? 'partial'
250
+ : 'not-configured',
247
251
  }
248
252
  }
249
253
  }