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,7 +1,7 @@
1
- import { createOctokit } from './github.js'
2
- import { exec } from './shell.js'
3
- import { isOpenApi, isAsyncApi } from '../formatters/openapi.js'
4
- import { load } from 'js-yaml'
1
+ import {createOctokit} from './github.js'
2
+ import {exec} from './shell.js'
3
+ import {isOpenApi, isAsyncApi} from '../formatters/openapi.js'
4
+ import {load} from 'js-yaml'
5
5
 
6
6
  /** @import { DocumentEntry, RepoDocsIndex, SearchMatch, DetectedRepo } from '../types.js' */
7
7
 
@@ -18,7 +18,7 @@ export async function detectCurrentRepo() {
18
18
  if (!match) {
19
19
  throw new Error('Could not detect GitHub repository from git remote. Use --repo to specify a repository.')
20
20
  }
21
- return { owner: match[1], repo: match[2] }
21
+ return {owner: match[1], repo: match[2]}
22
22
  }
23
23
 
24
24
  /**
@@ -27,22 +27,22 @@ export async function detectCurrentRepo() {
27
27
  * @returns {DocumentEntry|null}
28
28
  */
29
29
  function classifyEntry(entry) {
30
- const { size } = entry
30
+ const {size} = entry
31
31
  const path = entry.path
32
32
  if (size === 0) return null
33
33
  const name = path.split('/').pop() ?? path
34
34
 
35
35
  if (/^readme\.(md|rst|txt)$/i.test(path)) {
36
- return { name, path, type: 'readme', size }
36
+ return {name, path, type: 'readme', size}
37
37
  }
38
38
  if (/(openapi|swagger)\.(ya?ml|json)$/i.test(path)) {
39
- return { name, path, type: 'swagger', size }
39
+ return {name, path, type: 'swagger', size}
40
40
  }
41
41
  if (/asyncapi\.(ya?ml|json)$/i.test(path)) {
42
- return { name, path, type: 'asyncapi', size }
42
+ return {name, path, type: 'asyncapi', size}
43
43
  }
44
44
  if (path.startsWith('docs/') && /\.(md|rst|txt)$/.test(path)) {
45
- return { name, path, type: 'doc', size }
45
+ return {name, path, type: 'doc', size}
46
46
  }
47
47
  return null
48
48
  }
@@ -54,7 +54,7 @@ function classifyEntry(entry) {
54
54
  * @returns {number}
55
55
  */
56
56
  function sortEntries(a, b) {
57
- const order = { readme: 0, swagger: 1, asyncapi: 2, doc: 3 }
57
+ const order = {readme: 0, swagger: 1, asyncapi: 2, doc: 3}
58
58
  const diff = order[a.type] - order[b.type]
59
59
  return diff !== 0 ? diff : a.path.localeCompare(b.path)
60
60
  }
@@ -69,18 +69,18 @@ export async function listDocs(owner, repo) {
69
69
  const octokit = await createOctokit()
70
70
 
71
71
  // 1. Get default branch
72
- const { data: repoData } = await octokit.rest.repos.get({ owner, repo })
72
+ const {data: repoData} = await octokit.rest.repos.get({owner, repo})
73
73
  const defaultBranch = repoData.default_branch
74
74
 
75
75
  // 2. Get HEAD SHA
76
- const { data: ref } = await octokit.rest.git.getRef({
76
+ const {data: ref} = await octokit.rest.git.getRef({
77
77
  owner,
78
78
  repo,
79
79
  ref: `heads/${defaultBranch}`,
80
80
  })
81
81
 
82
82
  // 3. Fetch full recursive tree
83
- const { data: tree } = await octokit.rest.git.getTree({
83
+ const {data: tree} = await octokit.rest.git.getTree({
84
84
  owner,
85
85
  repo,
86
86
  tree_sha: ref.object.sha,
@@ -91,7 +91,7 @@ export async function listDocs(owner, repo) {
91
91
  const entries = []
92
92
  for (const e of tree.tree) {
93
93
  if (e.type !== 'blob') continue
94
- const entry = classifyEntry({ path: e.path ?? '', size: e.size ?? 0 })
94
+ const entry = classifyEntry({path: e.path ?? '', size: e.size ?? 0})
95
95
  if (entry) entries.push(entry)
96
96
  }
97
97
  return entries.sort(sortEntries)
@@ -106,7 +106,7 @@ export async function listDocs(owner, repo) {
106
106
  */
107
107
  export async function readFile(owner, repo, path) {
108
108
  const octokit = await createOctokit()
109
- const { data } = await octokit.rest.repos.getContent({ owner, repo, path })
109
+ const {data} = await octokit.rest.repos.getContent({owner, repo, path})
110
110
  if (Array.isArray(data) || data.type !== 'file') {
111
111
  throw new Error(`"${path}" is not a file.`)
112
112
  }
@@ -198,9 +198,7 @@ export function detectApiSpecType(path, content) {
198
198
  if (/asyncapi\.(ya?ml|json)$/i.test(path)) return 'asyncapi'
199
199
  // Try to detect from content
200
200
  try {
201
- const doc = /^\s*\{/.test(content.trim())
202
- ? JSON.parse(content)
203
- : load(content)
201
+ const doc = /^\s*\{/.test(content.trim()) ? JSON.parse(content) : load(content)
204
202
  if (doc && typeof doc === 'object') {
205
203
  if (isOpenApi(/** @type {Record<string, unknown>} */ (doc))) return 'swagger'
206
204
  if (isAsyncApi(/** @type {Record<string, unknown>} */ (doc))) return 'asyncapi'
@@ -208,7 +206,8 @@ export function detectApiSpecType(path, content) {
208
206
  } catch (err) {
209
207
  // File content is not valid YAML/JSON — not an API spec, return null.
210
208
  // Log at debug level for troubleshooting without exposing parse errors to users.
211
- if (process.env.DVMI_DEBUG) process.stderr.write(`[detectApiSpecType] parse failed: ${/** @type {Error} */ (err).message}\n`)
209
+ if (process.env.DVMI_DEBUG)
210
+ process.stderr.write(`[detectApiSpecType] parse failed: ${/** @type {Error} */ (err).message}\n`)
212
211
  }
213
212
  return null
214
213
  }
@@ -1,8 +1,8 @@
1
- import { homedir } from 'node:os'
2
- import { existsSync } from 'node:fs'
3
- import { join } from 'node:path'
4
- import { which, exec, execOrThrow } from './shell.js'
5
- import { loadConfig, saveConfig } from './config.js'
1
+ import {homedir} from 'node:os'
2
+ import {existsSync} from 'node:fs'
3
+ import {join} from 'node:path'
4
+ import {which, exec, execOrThrow} from './shell.js'
5
+ import {loadConfig, saveConfig} from './config.js'
6
6
 
7
7
  /** @import { Platform, DotfileEntry, DotfileRecommendation, DotfilesSetupResult, DotfilesAddResult, SetupStep, StepResult, CLIConfig } from '../types.js' */
8
8
 
@@ -31,26 +31,116 @@ export const SENSITIVE_PATTERNS = [
31
31
  */
32
32
  export const DEFAULT_FILE_LIST = [
33
33
  // Shell
34
- { path: '~/.zshrc', category: 'shell', platforms: ['macos', 'linux', 'wsl2'], autoEncrypt: false, description: 'Zsh configuration' },
35
- { path: '~/.bashrc', category: 'shell', platforms: ['linux', 'wsl2'], autoEncrypt: false, description: 'Bash configuration' },
36
- { path: '~/.bash_profile', category: 'shell', platforms: ['macos', 'linux', 'wsl2'], autoEncrypt: false, description: 'Bash profile' },
37
- { path: '~/.zprofile', category: 'shell', platforms: ['macos'], autoEncrypt: false, description: 'Zsh login profile' },
38
- { path: '~/.config/fish/config.fish', category: 'shell', platforms: ['macos', 'linux', 'wsl2'], autoEncrypt: false, description: 'Fish shell configuration' },
34
+ {
35
+ path: '~/.zshrc',
36
+ category: 'shell',
37
+ platforms: ['macos', 'linux', 'wsl2'],
38
+ autoEncrypt: false,
39
+ description: 'Zsh configuration',
40
+ },
41
+ {
42
+ path: '~/.bashrc',
43
+ category: 'shell',
44
+ platforms: ['linux', 'wsl2'],
45
+ autoEncrypt: false,
46
+ description: 'Bash configuration',
47
+ },
48
+ {
49
+ path: '~/.bash_profile',
50
+ category: 'shell',
51
+ platforms: ['macos', 'linux', 'wsl2'],
52
+ autoEncrypt: false,
53
+ description: 'Bash profile',
54
+ },
55
+ {path: '~/.zprofile', category: 'shell', platforms: ['macos'], autoEncrypt: false, description: 'Zsh login profile'},
56
+ {
57
+ path: '~/.config/fish/config.fish',
58
+ category: 'shell',
59
+ platforms: ['macos', 'linux', 'wsl2'],
60
+ autoEncrypt: false,
61
+ description: 'Fish shell configuration',
62
+ },
39
63
  // Git
40
- { path: '~/.gitconfig', category: 'git', platforms: ['macos', 'linux', 'wsl2'], autoEncrypt: false, description: 'Git global config' },
41
- { path: '~/.gitignore_global', category: 'git', platforms: ['macos', 'linux', 'wsl2'], autoEncrypt: false, description: 'Global gitignore patterns' },
64
+ {
65
+ path: '~/.gitconfig',
66
+ category: 'git',
67
+ platforms: ['macos', 'linux', 'wsl2'],
68
+ autoEncrypt: false,
69
+ description: 'Git global config',
70
+ },
71
+ {
72
+ path: '~/.gitignore_global',
73
+ category: 'git',
74
+ platforms: ['macos', 'linux', 'wsl2'],
75
+ autoEncrypt: false,
76
+ description: 'Global gitignore patterns',
77
+ },
42
78
  // Editor
43
- { path: '~/.vimrc', category: 'editor', platforms: ['macos', 'linux', 'wsl2'], autoEncrypt: false, description: 'Vim configuration' },
44
- { path: '~/.config/nvim/init.vim', category: 'editor', platforms: ['macos', 'linux', 'wsl2'], autoEncrypt: false, description: 'Neovim configuration' },
45
- { path: '~/.config/nvim/init.lua', category: 'editor', platforms: ['macos', 'linux', 'wsl2'], autoEncrypt: false, description: 'Neovim Lua configuration' },
79
+ {
80
+ path: '~/.vimrc',
81
+ category: 'editor',
82
+ platforms: ['macos', 'linux', 'wsl2'],
83
+ autoEncrypt: false,
84
+ description: 'Vim configuration',
85
+ },
86
+ {
87
+ path: '~/.config/nvim/init.vim',
88
+ category: 'editor',
89
+ platforms: ['macos', 'linux', 'wsl2'],
90
+ autoEncrypt: false,
91
+ description: 'Neovim configuration',
92
+ },
93
+ {
94
+ path: '~/.config/nvim/init.lua',
95
+ category: 'editor',
96
+ platforms: ['macos', 'linux', 'wsl2'],
97
+ autoEncrypt: false,
98
+ description: 'Neovim Lua configuration',
99
+ },
46
100
  // Package / macOS-specific
47
- { path: '~/.Brewfile', category: 'package', platforms: ['macos'], autoEncrypt: false, description: 'Homebrew bundle file' },
48
- { path: '~/.config/nvim', category: 'editor', platforms: ['macos', 'linux', 'wsl2'], autoEncrypt: false, description: 'Neovim config directory' },
101
+ {
102
+ path: '~/.Brewfile',
103
+ category: 'package',
104
+ platforms: ['macos'],
105
+ autoEncrypt: false,
106
+ description: 'Homebrew bundle file',
107
+ },
108
+ {
109
+ path: '~/.config/nvim',
110
+ category: 'editor',
111
+ platforms: ['macos', 'linux', 'wsl2'],
112
+ autoEncrypt: false,
113
+ description: 'Neovim config directory',
114
+ },
49
115
  // Security
50
- { path: '~/.ssh/config', category: 'security', platforms: ['macos', 'linux', 'wsl2'], autoEncrypt: true, description: 'SSH client configuration (auto-encrypted)' },
51
- { path: '~/.ssh/id_ed25519', category: 'security', platforms: ['macos', 'linux', 'wsl2'], autoEncrypt: true, description: 'SSH private key (auto-encrypted)' },
52
- { path: '~/.ssh/id_rsa', category: 'security', platforms: ['macos', 'linux', 'wsl2'], autoEncrypt: true, description: 'SSH RSA private key (auto-encrypted)' },
53
- { path: '~/.gnupg/pubring.kbx', category: 'security', platforms: ['macos', 'linux', 'wsl2'], autoEncrypt: true, description: 'GPG public keyring (auto-encrypted)' },
116
+ {
117
+ path: '~/.ssh/config',
118
+ category: 'security',
119
+ platforms: ['macos', 'linux', 'wsl2'],
120
+ autoEncrypt: true,
121
+ description: 'SSH client configuration (auto-encrypted)',
122
+ },
123
+ {
124
+ path: '~/.ssh/id_ed25519',
125
+ category: 'security',
126
+ platforms: ['macos', 'linux', 'wsl2'],
127
+ autoEncrypt: true,
128
+ description: 'SSH private key (auto-encrypted)',
129
+ },
130
+ {
131
+ path: '~/.ssh/id_rsa',
132
+ category: 'security',
133
+ platforms: ['macos', 'linux', 'wsl2'],
134
+ autoEncrypt: true,
135
+ description: 'SSH RSA private key (auto-encrypted)',
136
+ },
137
+ {
138
+ path: '~/.gnupg/pubring.kbx',
139
+ category: 'security',
140
+ platforms: ['macos', 'linux', 'wsl2'],
141
+ autoEncrypt: true,
142
+ description: 'GPG public keyring (auto-encrypted)',
143
+ },
54
144
  ]
55
145
 
56
146
  // ---------------------------------------------------------------------------
@@ -149,11 +239,12 @@ function globToRegex(pattern) {
149
239
  const expanded = expandTilde(pattern)
150
240
  // Split on `**` to handle double-star separately
151
241
  const parts = expanded.split('**')
152
- const escaped = parts.map((part) =>
153
- part
154
- .replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex special chars
155
- .replace(/\*/g, '[^/]*') // single * any non-separator
156
- .replace(/\?/g, '[^/]'), // ? → any single non-separator char
242
+ const escaped = parts.map(
243
+ (part) =>
244
+ part
245
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex special chars
246
+ .replace(/\*/g, '[^/]*') // single * → any non-separator
247
+ .replace(/\?/g, '[^/]'), // ? → any single non-separator char
157
248
  )
158
249
  const src = escaped.join('.*') // ** → match anything including /
159
250
  return new RegExp(`^${src}$`, 'i')
@@ -246,14 +337,15 @@ export function buildSetupSteps(platform, options = {}) {
246
337
  run: async () => {
247
338
  const installed = await isChezmoiInstalled()
248
339
  if (!installed) {
249
- const hint = platform === 'macos'
250
- ? 'Run `brew install chezmoi` or visit https://chezmoi.io/install'
251
- : 'Run `sh -c "$(curl -fsLS get.chezmoi.io)"` or visit https://chezmoi.io/install'
252
- return { status: 'failed', hint }
340
+ const hint =
341
+ platform === 'macos'
342
+ ? 'Run `brew install chezmoi` or visit https://chezmoi.io/install'
343
+ : 'Run `sh -c "$(curl -fsLS get.chezmoi.io)"` or visit https://chezmoi.io/install'
344
+ return {status: 'failed', hint}
253
345
  }
254
346
  const result = await exec('chezmoi', ['--version'])
255
347
  const version = (result.stdout || result.stderr).trim()
256
- return { status: 'success', message: `chezmoi ${version}` }
348
+ return {status: 'success', message: `chezmoi ${version}`}
257
349
  },
258
350
  })
259
351
 
@@ -267,13 +359,13 @@ export function buildSetupSteps(platform, options = {}) {
267
359
  run: async () => {
268
360
  const config = options.existingConfig !== undefined ? options.existingConfig : await getChezmoiConfig()
269
361
  if (!config) {
270
- return { status: 'success', message: 'No existing configuration — fresh setup' }
362
+ return {status: 'success', message: 'No existing configuration — fresh setup'}
271
363
  }
272
364
  const hasEncryption = config.encryption?.tool === 'age' || !!config.age?.identity
273
365
  if (hasEncryption) {
274
- return { status: 'skipped', message: 'Age encryption already configured' }
366
+ return {status: 'skipped', message: 'Age encryption already configured'}
275
367
  }
276
- return { status: 'success', message: 'Existing config found without encryption — will add age' }
368
+ return {status: 'success', message: 'Existing config found without encryption — will add age'}
277
369
  },
278
370
  })
279
371
 
@@ -287,18 +379,18 @@ export function buildSetupSteps(platform, options = {}) {
287
379
  run: async () => {
288
380
  // Skip if key already exists
289
381
  if (existsSync(ageKeyPath)) {
290
- return { status: 'skipped', message: `Age key already exists at ${ageKeyPath}` }
382
+ return {status: 'skipped', message: `Age key already exists at ${ageKeyPath}`}
291
383
  }
292
384
  try {
293
385
  // chezmoi uses `age-keygen` via its own embedded command
294
386
  await execOrThrow('chezmoi', ['age', 'keygen', '-o', ageKeyPath])
295
- return { status: 'success', message: `Age key generated at ${ageKeyPath}` }
387
+ return {status: 'success', message: `Age key generated at ${ageKeyPath}`}
296
388
  } catch {
297
389
  // Fallback: try standalone age-keygen
298
390
  try {
299
391
  // age-keygen writes public key to stderr, private key to file
300
392
  await execOrThrow('age-keygen', ['-o', ageKeyPath])
301
- return { status: 'success', message: `Age key generated at ${ageKeyPath}` }
393
+ return {status: 'success', message: `Age key generated at ${ageKeyPath}`}
302
394
  } catch {
303
395
  return {
304
396
  status: 'failed',
@@ -337,11 +429,14 @@ export function buildSetupSteps(platform, options = {}) {
337
429
  .filter((l) => l !== undefined)
338
430
  .join('\n')
339
431
 
340
- const { writeFile, mkdir } = await import('node:fs/promises')
341
- await mkdir(chezmoiConfigDir, { recursive: true })
432
+ const {writeFile, mkdir} = await import('node:fs/promises')
433
+ await mkdir(chezmoiConfigDir, {recursive: true})
342
434
  await writeFile(configPath, tomlContent, 'utf8')
343
435
 
344
- return { status: 'success', message: `chezmoi.toml written with age encryption${publicKey ? ` (public key: ${publicKey.slice(0, 16)}...)` : ''}` }
436
+ return {
437
+ status: 'success',
438
+ message: `chezmoi.toml written with age encryption${publicKey ? ` (public key: ${publicKey.slice(0, 16)}...)` : ''}`,
439
+ }
345
440
  } catch (err) {
346
441
  return {
347
442
  status: 'failed',
@@ -363,14 +458,14 @@ export function buildSetupSteps(platform, options = {}) {
363
458
  await execOrThrow('chezmoi', ['init'])
364
459
  const configResult = await getChezmoiConfig()
365
460
  const sourceDir = configResult?.sourceDir ?? configResult?.sourcePath ?? null
366
- return { status: 'success', message: sourceDir ? `Source dir: ${sourceDir}` : 'chezmoi initialised' }
461
+ return {status: 'success', message: sourceDir ? `Source dir: ${sourceDir}` : 'chezmoi initialised'}
367
462
  } catch {
368
463
  // init may fail if already initialised — that's ok
369
464
  const configResult = await getChezmoiConfig()
370
465
  if (configResult) {
371
- return { status: 'skipped', message: 'chezmoi already initialised' }
466
+ return {status: 'skipped', message: 'chezmoi already initialised'}
372
467
  }
373
- return { status: 'failed', hint: 'Run `chezmoi doctor` to diagnose init failure' }
468
+ return {status: 'failed', hint: 'Run `chezmoi doctor` to diagnose init failure'}
374
469
  }
375
470
  },
376
471
  })
@@ -385,9 +480,9 @@ export function buildSetupSteps(platform, options = {}) {
385
480
  run: async () => {
386
481
  try {
387
482
  const config = await loadConfig()
388
- config.dotfiles = { ...config.dotfiles, enabled: true }
483
+ config.dotfiles = {...config.dotfiles, enabled: true}
389
484
  await saveConfig(config)
390
- return { status: 'success', message: 'dvmi config updated: dotfiles.enabled = true' }
485
+ return {status: 'success', message: 'dvmi config updated: dotfiles.enabled = true'}
391
486
  } catch (err) {
392
487
  return {
393
488
  status: 'failed',
@@ -456,8 +551,8 @@ export async function setupChezmoiInline(platform) {
456
551
  .filter((l) => l !== undefined)
457
552
  .join('\n')
458
553
 
459
- const { writeFile, mkdir } = await import('node:fs/promises')
460
- await mkdir(chezmoiConfigDir, { recursive: true })
554
+ const {writeFile, mkdir} = await import('node:fs/promises')
555
+ await mkdir(chezmoiConfigDir, {recursive: true})
461
556
  await writeFile(configPath, tomlContent, 'utf8')
462
557
 
463
558
  // Init chezmoi
@@ -469,7 +564,7 @@ export async function setupChezmoiInline(platform) {
469
564
 
470
565
  // Save dvmi config
471
566
  const dvmiConfig = await loadConfig()
472
- dvmiConfig.dotfiles = { ...(dvmiConfig.dotfiles ?? {}), enabled: true }
567
+ dvmiConfig.dotfiles = {...(dvmiConfig.dotfiles ?? {}), enabled: true}
473
568
  await saveConfig(dvmiConfig)
474
569
 
475
570
  return {
@@ -519,18 +614,21 @@ export function buildAddSteps(files, platform) {
519
614
  run: async () => {
520
615
  // V-001: file must exist
521
616
  if (!existsSync(absPath)) {
522
- return { status: 'skipped', message: `${file.path}: file not found` }
617
+ return {status: 'skipped', message: `${file.path}: file not found`}
523
618
  }
524
619
  // V-002: WSL2 Windows path rejection
525
620
  if (platform === 'wsl2' && isWSLWindowsPath(absPath)) {
526
- return { status: 'failed', hint: `${file.path}: Windows filesystem paths not supported on WSL2. Use Linux-native paths (~/) instead.` }
621
+ return {
622
+ status: 'failed',
623
+ hint: `${file.path}: Windows filesystem paths not supported on WSL2. Use Linux-native paths (~/) instead.`,
624
+ }
527
625
  }
528
626
  try {
529
627
  const args = ['add']
530
628
  if (file.encrypt) args.push('--encrypt')
531
629
  args.push(absPath)
532
630
  await execOrThrow('chezmoi', args)
533
- return { status: 'success', message: `${file.path} added${file.encrypt ? ' (encrypted)' : ''}` }
631
+ return {status: 'success', message: `${file.path} added${file.encrypt ? ' (encrypted)' : ''}`}
534
632
  } catch {
535
633
  return {
536
634
  status: 'failed',
@@ -1,6 +1,6 @@
1
- import { Octokit } from 'octokit'
2
- import { exec } from './shell.js'
3
- import { AuthError } from '../utils/errors.js'
1
+ import {Octokit} from 'octokit'
2
+ import {exec} from './shell.js'
3
+ import {AuthError} from '../utils/errors.js'
4
4
 
5
5
  /** @import { Template, Repository, PullRequest, PRComment, QAStep, PRDetail, PipelineRun } from '../types.js' */
6
6
 
@@ -21,7 +21,7 @@ async function getToken() {
21
21
  export async function createOctokit() {
22
22
  const token = await getToken()
23
23
  const baseUrl = process.env.GITHUB_API_URL ?? 'https://api.github.com'
24
- return new Octokit({ auth: token, baseUrl })
24
+ return new Octokit({auth: token, baseUrl})
25
25
  }
26
26
 
27
27
  /**
@@ -47,9 +47,7 @@ export async function listRepos(org, filters = {}) {
47
47
  isPrivate: r.private,
48
48
  }))
49
49
  if (filters.language) {
50
- results = results.filter(
51
- (r) => r.language?.toLowerCase() === filters.language?.toLowerCase(),
52
- )
50
+ results = results.filter((r) => r.language?.toLowerCase() === filters.language?.toLowerCase())
53
51
  }
54
52
  if (filters.topic) {
55
53
  results = results.filter((r) => r.topics.includes(filters.topic ?? ''))
@@ -87,7 +85,7 @@ export async function listTemplates(org) {
87
85
  */
88
86
  export async function createFromTemplate(opts) {
89
87
  const octokit = await createOctokit()
90
- const { data } = await octokit.rest.repos.createUsingTemplate({
88
+ const {data} = await octokit.rest.repos.createUsingTemplate({
91
89
  template_owner: opts.templateOwner,
92
90
  template_repo: opts.templateRepo,
93
91
  name: opts.name,
@@ -96,7 +94,7 @@ export async function createFromTemplate(opts) {
96
94
  private: opts.isPrivate,
97
95
  include_all_branches: false,
98
96
  })
99
- return { name: data.name, htmlUrl: data.html_url, cloneUrl: data.clone_url }
97
+ return {name: data.name, htmlUrl: data.html_url, cloneUrl: data.clone_url}
100
98
  }
101
99
 
102
100
  /**
@@ -113,7 +111,7 @@ export async function setBranchProtection(owner, repo) {
113
111
  branch: 'main',
114
112
  required_status_checks: null,
115
113
  enforce_admins: false,
116
- required_pull_request_reviews: { required_approving_review_count: 0 },
114
+ required_pull_request_reviews: {required_approving_review_count: 0},
117
115
  restrictions: null,
118
116
  allow_force_pushes: false,
119
117
  allow_deletions: false,
@@ -128,8 +126,8 @@ export async function setBranchProtection(owner, repo) {
128
126
  */
129
127
  export async function enableDependabot(owner, repo) {
130
128
  const octokit = await createOctokit()
131
- await octokit.rest.repos.enableAutomatedSecurityFixes({ owner, repo })
132
- await octokit.rest.repos.enableVulnerabilityAlerts({ owner, repo })
129
+ await octokit.rest.repos.enableAutomatedSecurityFixes({owner, repo})
130
+ await octokit.rest.repos.enableVulnerabilityAlerts({owner, repo})
133
131
  }
134
132
 
135
133
  /**
@@ -139,7 +137,7 @@ export async function enableDependabot(owner, repo) {
139
137
  */
140
138
  export async function createPR(opts) {
141
139
  const octokit = await createOctokit()
142
- const { data } = await octokit.rest.pulls.create({
140
+ const {data} = await octokit.rest.pulls.create({
143
141
  owner: opts.owner,
144
142
  repo: opts.repo,
145
143
  title: opts.title,
@@ -164,7 +162,7 @@ export async function createPR(opts) {
164
162
  reviewers: opts.reviewers,
165
163
  })
166
164
  }
167
- return { number: data.number, htmlUrl: data.html_url }
165
+ return {number: data.number, htmlUrl: data.html_url}
168
166
  }
169
167
 
170
168
  /**
@@ -174,7 +172,7 @@ export async function createPR(opts) {
174
172
  */
175
173
  export async function listMyPRs(org) {
176
174
  const octokit = await createOctokit()
177
- const { data: user } = await octokit.rest.users.getAuthenticated()
175
+ const {data: user} = await octokit.rest.users.getAuthenticated()
178
176
  const login = user.login
179
177
 
180
178
  const [authoredRes, reviewingRes] = await Promise.all([
@@ -227,9 +225,9 @@ export async function listWorkflowRuns(owner, repo, filters = {}) {
227
225
  owner,
228
226
  repo,
229
227
  per_page: filters.limit ?? 10,
230
- ...(filters.branch ? { branch: filters.branch } : {}),
228
+ ...(filters.branch ? {branch: filters.branch} : {}),
231
229
  }
232
- const { data } = await octokit.rest.actions.listWorkflowRunsForRepo(params)
230
+ const {data} = await octokit.rest.actions.listWorkflowRunsForRepo(params)
233
231
  return data.workflow_runs.map((run) => {
234
232
  const start = new Date(run.created_at)
235
233
  const end = run.updated_at ? new Date(run.updated_at) : new Date()
@@ -259,9 +257,9 @@ export async function listWorkflowRuns(owner, repo, filters = {}) {
259
257
  export async function rerunWorkflow(owner, repo, runId, failedOnly = false) {
260
258
  const octokit = await createOctokit()
261
259
  if (failedOnly) {
262
- await octokit.rest.actions.reRunWorkflowFailedJobs({ owner, repo, run_id: runId })
260
+ await octokit.rest.actions.reRunWorkflowFailedJobs({owner, repo, run_id: runId})
263
261
  } else {
264
- await octokit.rest.actions.reRunWorkflow({ owner, repo, run_id: runId })
262
+ await octokit.rest.actions.reRunWorkflow({owner, repo, run_id: runId})
265
263
  }
266
264
  }
267
265
 
@@ -277,7 +275,7 @@ export async function searchCode(org, query, opts = {}) {
277
275
  let q = `${query} org:${org}`
278
276
  if (opts.language) q += ` language:${opts.language}`
279
277
  if (opts.repo) q += ` repo:${org}/${opts.repo}`
280
- const { data } = await octokit.rest.search.code({ q, per_page: opts.limit ?? 20 })
278
+ const {data} = await octokit.rest.search.code({q, per_page: opts.limit ?? 20})
281
279
  return data.items.map((item) => ({
282
280
  repo: item.repository.name,
283
281
  file: item.path,
@@ -297,7 +295,7 @@ export function extractQASteps(body) {
297
295
  for (const line of body.split('\n')) {
298
296
  const match = line.match(/^\s*-\s*\[([xX ])\]\s+(.+)/)
299
297
  if (match) {
300
- steps.push({ text: match[2].trim(), checked: match[1].toLowerCase() === 'x' })
298
+ steps.push({text: match[2].trim(), checked: match[1].toLowerCase() === 'x'})
301
299
  }
302
300
  }
303
301
  return steps
@@ -331,9 +329,9 @@ export async function getPRDetail(owner, repo, prNumber) {
331
329
  const octokit = await createOctokit()
332
330
 
333
331
  const [prRes, commentsRes, reviewsRes] = await Promise.all([
334
- octokit.rest.pulls.get({ owner, repo, pull_number: prNumber }),
335
- octokit.rest.issues.listComments({ owner, repo, issue_number: prNumber, per_page: 100 }),
336
- octokit.rest.pulls.listReviews({ owner, repo, pull_number: prNumber, per_page: 100 }),
332
+ octokit.rest.pulls.get({owner, repo, pull_number: prNumber}),
333
+ octokit.rest.issues.listComments({owner, repo, issue_number: prNumber, per_page: 100}),
334
+ octokit.rest.pulls.listReviews({owner, repo, pull_number: prNumber, per_page: 100}),
337
335
  ])
338
336
 
339
337
  const pr = prRes.data