devvami 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +7 -0
  2. package/oclif.manifest.json +41 -1
  3. package/package.json +2 -1
  4. package/src/commands/auth/login.js +20 -16
  5. package/src/commands/changelog.js +12 -12
  6. package/src/commands/costs/get.js +14 -24
  7. package/src/commands/costs/trend.js +13 -24
  8. package/src/commands/create/repo.js +72 -54
  9. package/src/commands/docs/list.js +29 -25
  10. package/src/commands/docs/projects.js +58 -24
  11. package/src/commands/docs/read.js +56 -39
  12. package/src/commands/docs/search.js +37 -25
  13. package/src/commands/doctor.js +37 -35
  14. package/src/commands/dotfiles/add.js +51 -39
  15. package/src/commands/dotfiles/setup.js +62 -33
  16. package/src/commands/dotfiles/status.js +18 -18
  17. package/src/commands/dotfiles/sync.js +62 -46
  18. package/src/commands/init.js +143 -132
  19. package/src/commands/logs/index.js +10 -16
  20. package/src/commands/open.js +12 -12
  21. package/src/commands/pipeline/logs.js +8 -11
  22. package/src/commands/pipeline/rerun.js +21 -16
  23. package/src/commands/pipeline/status.js +28 -24
  24. package/src/commands/pr/create.js +40 -27
  25. package/src/commands/pr/detail.js +9 -7
  26. package/src/commands/pr/review.js +18 -19
  27. package/src/commands/pr/status.js +27 -21
  28. package/src/commands/prompts/browse.js +15 -15
  29. package/src/commands/prompts/download.js +15 -16
  30. package/src/commands/prompts/install-speckit.js +11 -12
  31. package/src/commands/prompts/list.js +12 -12
  32. package/src/commands/prompts/run.js +16 -19
  33. package/src/commands/repo/list.js +57 -41
  34. package/src/commands/search.js +20 -18
  35. package/src/commands/security/setup.js +38 -34
  36. package/src/commands/sync-config-ai/index.js +143 -0
  37. package/src/commands/tasks/assigned.js +43 -33
  38. package/src/commands/tasks/list.js +43 -33
  39. package/src/commands/tasks/today.js +32 -30
  40. package/src/commands/upgrade.js +18 -17
  41. package/src/commands/vuln/detail.js +8 -8
  42. package/src/commands/vuln/scan.js +95 -21
  43. package/src/commands/vuln/search.js +23 -18
  44. package/src/commands/welcome.js +2 -2
  45. package/src/commands/whoami.js +19 -23
  46. package/src/formatters/ai-config.js +127 -0
  47. package/src/formatters/charts.js +6 -23
  48. package/src/formatters/cost.js +1 -7
  49. package/src/formatters/dotfiles.js +48 -19
  50. package/src/formatters/markdown.js +11 -6
  51. package/src/formatters/openapi.js +7 -9
  52. package/src/formatters/prompts.js +69 -78
  53. package/src/formatters/security.js +2 -2
  54. package/src/formatters/status.js +1 -1
  55. package/src/formatters/table.js +1 -3
  56. package/src/formatters/vuln.js +33 -20
  57. package/src/help.js +162 -164
  58. package/src/hooks/init.js +1 -3
  59. package/src/hooks/postrun.js +5 -7
  60. package/src/index.js +1 -1
  61. package/src/services/ai-config-store.js +318 -0
  62. package/src/services/ai-env-deployer.js +444 -0
  63. package/src/services/ai-env-scanner.js +242 -0
  64. package/src/services/audit-detector.js +2 -2
  65. package/src/services/audit-runner.js +40 -31
  66. package/src/services/auth.js +9 -9
  67. package/src/services/awesome-copilot.js +7 -4
  68. package/src/services/aws-costs.js +22 -22
  69. package/src/services/clickup.js +26 -26
  70. package/src/services/cloudwatch-logs.js +5 -9
  71. package/src/services/config.js +13 -13
  72. package/src/services/docs.js +19 -20
  73. package/src/services/dotfiles.js +149 -51
  74. package/src/services/github.js +22 -24
  75. package/src/services/nvd.js +21 -31
  76. package/src/services/platform.js +2 -2
  77. package/src/services/prompts.js +23 -35
  78. package/src/services/security.js +135 -61
  79. package/src/services/shell.js +4 -4
  80. package/src/services/skills-sh.js +3 -9
  81. package/src/services/speckit.js +4 -7
  82. package/src/services/version-check.js +10 -10
  83. package/src/types.js +85 -0
  84. package/src/utils/aws-vault.js +18 -41
  85. package/src/utils/banner.js +5 -7
  86. package/src/utils/errors.js +42 -46
  87. package/src/utils/frontmatter.js +4 -4
  88. package/src/utils/gradient.js +18 -16
  89. package/src/utils/open-browser.js +3 -3
  90. package/src/utils/tui/form.js +1006 -0
  91. package/src/utils/tui/modal.js +15 -14
  92. package/src/utils/tui/navigable-table.js +25 -17
  93. package/src/utils/tui/tab-tui.js +800 -0
  94. package/src/utils/typewriter.js +3 -3
  95. package/src/utils/welcome.js +18 -21
  96. package/src/validators/repo-name.js +2 -2
@@ -1,8 +1,8 @@
1
- import { homedir } from 'node:os'
2
- import { join } from 'node:path'
3
- import { readFile, appendFile } from 'node:fs/promises'
4
- import { existsSync } from 'node:fs'
5
- import { which, exec, execOrThrow } from './shell.js'
1
+ import {homedir} from 'node:os'
2
+ import {join} from 'node:path'
3
+ import {readFile, appendFile} from 'node:fs/promises'
4
+ import {existsSync} from 'node:fs'
5
+ import {which, exec, execOrThrow} from './shell.js'
6
6
 
7
7
  /** @import { Platform, PlatformInfo, SecurityTool, SecurityToolStatus, SetupStep, StepResult, GpgKey } from '../types.js' */
8
8
 
@@ -74,14 +74,20 @@ export async function checkToolStatus(platform) {
74
74
 
75
75
  for (const tool of TOOL_DEFINITIONS) {
76
76
  if (!tool.platforms.includes(platform)) {
77
- results.push({ id: tool.id, displayName: tool.displayName, status: 'n/a', version: null, hint: null })
77
+ results.push({id: tool.id, displayName: tool.displayName, status: 'n/a', version: null, hint: null})
78
78
  continue
79
79
  }
80
80
 
81
81
  if (tool.id === 'aws-vault') {
82
82
  const path = await which('aws-vault')
83
83
  if (!path) {
84
- results.push({ id: tool.id, displayName: tool.displayName, status: 'not-installed', version: null, hint: 'Install aws-vault' })
84
+ results.push({
85
+ id: tool.id,
86
+ displayName: tool.displayName,
87
+ status: 'not-installed',
88
+ version: null,
89
+ hint: 'Install aws-vault',
90
+ })
85
91
  continue
86
92
  }
87
93
  const versionResult = await exec('aws-vault', ['--version'])
@@ -90,30 +96,60 @@ export async function checkToolStatus(platform) {
90
96
  if (platform !== 'macos') {
91
97
  const backend = process.env.AWS_VAULT_BACKEND
92
98
  if (backend !== 'pass') {
93
- results.push({ id: tool.id, displayName: tool.displayName, status: 'misconfigured', version: version || null, hint: 'Add export AWS_VAULT_BACKEND=pass to your shell profile' })
99
+ results.push({
100
+ id: tool.id,
101
+ displayName: tool.displayName,
102
+ status: 'misconfigured',
103
+ version: version || null,
104
+ hint: 'Add export AWS_VAULT_BACKEND=pass to your shell profile',
105
+ })
94
106
  continue
95
107
  }
96
108
  }
97
- results.push({ id: tool.id, displayName: tool.displayName, status: 'installed', version: version || null, hint: null })
109
+ results.push({
110
+ id: tool.id,
111
+ displayName: tool.displayName,
112
+ status: 'installed',
113
+ version: version || null,
114
+ hint: null,
115
+ })
98
116
  continue
99
117
  }
100
118
 
101
119
  if (tool.id === 'gpg') {
102
120
  const path = await which('gpg')
103
121
  if (!path) {
104
- results.push({ id: tool.id, displayName: tool.displayName, status: 'not-installed', version: null, hint: 'Install gnupg via your package manager' })
122
+ results.push({
123
+ id: tool.id,
124
+ displayName: tool.displayName,
125
+ status: 'not-installed',
126
+ version: null,
127
+ hint: 'Install gnupg via your package manager',
128
+ })
105
129
  continue
106
130
  }
107
131
  const versionResult = await exec('gpg', ['--version'])
108
132
  const match = versionResult.stdout.match(/gpg \(GnuPG\)\s+([\d.]+)/)
109
- results.push({ id: tool.id, displayName: tool.displayName, status: 'installed', version: match ? match[1] : null, hint: null })
133
+ results.push({
134
+ id: tool.id,
135
+ displayName: tool.displayName,
136
+ status: 'installed',
137
+ version: match ? match[1] : null,
138
+ hint: null,
139
+ })
110
140
  continue
111
141
  }
112
142
 
113
143
  if (tool.id === 'pass') {
114
144
  const path = await which('pass')
115
145
  if (!path) {
116
- results.push({ id: tool.id, displayName: tool.displayName, status: 'not-installed', version: null, hint: 'Install pass via your package manager' })
146
+ results.push({
147
+ id: tool.id,
148
+ displayName: tool.displayName,
149
+ status: 'not-installed',
150
+ version: null,
151
+ hint: 'Install pass via your package manager',
152
+ })
117
153
  continue
118
154
  }
119
155
  const versionResult = await exec('pass', ['--version'])
@@ -121,19 +157,37 @@ export async function checkToolStatus(platform) {
121
157
  // Check if pass is initialized
122
158
  const lsResult = await exec('pass', ['ls'])
123
159
  if (lsResult.exitCode !== 0) {
124
- results.push({ id: tool.id, displayName: tool.displayName, status: 'misconfigured', version: match ? match[1] : null, hint: 'Initialize pass with: pass init <gpg-key-id>' })
160
+ results.push({
161
+ id: tool.id,
162
+ displayName: tool.displayName,
163
+ status: 'misconfigured',
164
+ version: match ? match[1] : null,
165
+ hint: 'Initialize pass with: pass init <gpg-key-id>',
166
+ })
125
167
  continue
126
168
  }
127
- results.push({ id: tool.id, displayName: tool.displayName, status: 'installed', version: match ? match[1] : null, hint: null })
169
+ results.push({
170
+ id: tool.id,
171
+ displayName: tool.displayName,
172
+ status: 'installed',
173
+ version: match ? match[1] : null,
174
+ hint: null,
175
+ })
128
176
  continue
129
177
  }
130
178
 
131
179
  if (tool.id === 'osxkeychain') {
132
180
  const result = await exec('git', ['config', '--global', 'credential.helper'])
133
181
  if (result.stdout === 'osxkeychain') {
134
- results.push({ id: tool.id, displayName: tool.displayName, status: 'installed', version: null, hint: null })
182
+ results.push({id: tool.id, displayName: tool.displayName, status: 'installed', version: null, hint: null})
135
183
  } else {
136
- results.push({ id: tool.id, displayName: tool.displayName, status: 'not-installed', version: null, hint: 'Run: git config --global credential.helper osxkeychain' })
184
+ results.push({
185
+ id: tool.id,
186
+ displayName: tool.displayName,
187
+ status: 'not-installed',
188
+ version: null,
189
+ hint: 'Run: git config --global credential.helper osxkeychain',
190
+ })
137
191
  }
138
192
  continue
139
193
  }
@@ -141,17 +195,29 @@ export async function checkToolStatus(platform) {
141
195
  if (tool.id === 'gcm') {
142
196
  const path = await which('git-credential-manager')
143
197
  if (!path) {
144
- results.push({ id: tool.id, displayName: tool.displayName, status: 'not-installed', version: null, hint: 'Install Git Credential Manager' })
198
+ results.push({
199
+ id: tool.id,
200
+ displayName: tool.displayName,
201
+ status: 'not-installed',
202
+ version: null,
203
+ hint: 'Install Git Credential Manager',
204
+ })
145
205
  continue
146
206
  }
147
207
  const versionResult = await exec('git-credential-manager', ['--version'])
148
208
  const version = versionResult.stdout.trim() || null
149
209
  const storeResult = await exec('git', ['config', '--global', 'credential.credentialStore'])
150
210
  if (storeResult.stdout !== 'gpg') {
151
- results.push({ id: tool.id, displayName: tool.displayName, status: 'misconfigured', version, hint: 'Run: git config --global credential.credentialStore gpg' })
211
+ results.push({
212
+ id: tool.id,
213
+ displayName: tool.displayName,
214
+ status: 'misconfigured',
215
+ version,
216
+ hint: 'Run: git config --global credential.credentialStore gpg',
217
+ })
152
218
  continue
153
219
  }
154
- results.push({ id: tool.id, displayName: tool.displayName, status: 'installed', version, hint: null })
220
+ results.push({id: tool.id, displayName: tool.displayName, status: 'installed', version, hint: null})
155
221
  continue
156
222
  }
157
223
  }
@@ -267,7 +333,7 @@ export function deriveOverallStatus(tools) {
267
333
  * @returns {SetupStep[]}
268
334
  */
269
335
  export function buildSteps(platformInfo, selection, context = {}) {
270
- const { platform } = platformInfo
336
+ const {platform} = platformInfo
271
337
  const includeAws = selection === 'aws' || selection === 'both'
272
338
  const includeGit = selection === 'git' || selection === 'both'
273
339
 
@@ -291,7 +357,7 @@ export function buildSteps(platformInfo, selection, context = {}) {
291
357
  hintUrl: 'https://brew.sh',
292
358
  }
293
359
  }
294
- return { status: 'success', message: 'Homebrew is available' }
360
+ return {status: 'success', message: 'Homebrew is available'}
295
361
  },
296
362
  })
297
363
 
@@ -303,10 +369,10 @@ export function buildSteps(platformInfo, selection, context = {}) {
303
369
  requiresConfirmation: true,
304
370
  run: async () => {
305
371
  const existing = await which('aws-vault')
306
- if (existing) return { status: 'skipped', message: 'aws-vault already installed' }
372
+ if (existing) return {status: 'skipped', message: 'aws-vault already installed'}
307
373
  try {
308
374
  await execOrThrow('brew', ['install', 'aws-vault'])
309
- return { status: 'success', message: 'aws-vault installed via Homebrew' }
375
+ return {status: 'success', message: 'aws-vault installed via Homebrew'}
310
376
  } catch {
311
377
  return {
312
378
  status: 'failed',
@@ -326,10 +392,10 @@ export function buildSteps(platformInfo, selection, context = {}) {
326
392
  run: async () => {
327
393
  const result = await exec('aws-vault', ['--version'])
328
394
  if (result.exitCode !== 0) {
329
- return { status: 'failed', hint: 'aws-vault not found in PATH after install' }
395
+ return {status: 'failed', hint: 'aws-vault not found in PATH after install'}
330
396
  }
331
397
  const version = (result.stdout || result.stderr).trim()
332
- return { status: 'success', message: `aws-vault ${version}` }
398
+ return {status: 'success', message: `aws-vault ${version}`}
333
399
  },
334
400
  })
335
401
  }
@@ -344,9 +410,9 @@ export function buildSteps(platformInfo, selection, context = {}) {
344
410
  run: async () => {
345
411
  try {
346
412
  await execOrThrow('git', ['config', '--global', 'credential.helper', 'osxkeychain'])
347
- return { status: 'success', message: 'Git credential helper set to osxkeychain' }
413
+ return {status: 'success', message: 'Git credential helper set to osxkeychain'}
348
414
  } catch {
349
- return { status: 'failed', hint: 'Run manually: git config --global credential.helper osxkeychain' }
415
+ return {status: 'failed', hint: 'Run manually: git config --global credential.helper osxkeychain'}
350
416
  }
351
417
  },
352
418
  })
@@ -360,9 +426,9 @@ export function buildSteps(platformInfo, selection, context = {}) {
360
426
  run: async () => {
361
427
  const result = await exec('git', ['config', '--global', 'credential.helper'])
362
428
  if (result.stdout !== 'osxkeychain') {
363
- return { status: 'failed', hint: 'credential.helper is not set to osxkeychain' }
429
+ return {status: 'failed', hint: 'credential.helper is not set to osxkeychain'}
364
430
  }
365
- return { status: 'success', message: 'osxkeychain is configured' }
431
+ return {status: 'success', message: 'osxkeychain is configured'}
366
432
  },
367
433
  })
368
434
  }
@@ -378,11 +444,11 @@ export function buildSteps(platformInfo, selection, context = {}) {
378
444
  run: async () => {
379
445
  const path = await which('gpg')
380
446
  if (!path) {
381
- return { status: 'failed', hint: 'GPG not found — will be installed in the next step' }
447
+ return {status: 'failed', hint: 'GPG not found — will be installed in the next step'}
382
448
  }
383
449
  const result = await exec('gpg', ['--version'])
384
450
  const match = result.stdout.match(/gpg \(GnuPG\)\s+([\d.]+)/)
385
- return { status: 'success', message: `GPG ${match ? match[1] : 'found'}` }
451
+ return {status: 'success', message: `GPG ${match ? match[1] : 'found'}`}
386
452
  },
387
453
  })
388
454
 
@@ -395,12 +461,12 @@ export function buildSteps(platformInfo, selection, context = {}) {
395
461
  skippable: true,
396
462
  run: async () => {
397
463
  const path = await which('gpg')
398
- if (path) return { status: 'skipped', message: 'GPG already installed' }
464
+ if (path) return {status: 'skipped', message: 'GPG already installed'}
399
465
  try {
400
466
  await execOrThrow('sudo', ['apt-get', 'install', '-y', 'gnupg'])
401
- return { status: 'success', message: 'GPG installed' }
467
+ return {status: 'success', message: 'GPG installed'}
402
468
  } catch {
403
- return { status: 'failed', hint: 'Run manually: sudo apt-get install -y gnupg' }
469
+ return {status: 'failed', hint: 'Run manually: sudo apt-get install -y gnupg'}
404
470
  }
405
471
  },
406
472
  })
@@ -414,7 +480,7 @@ export function buildSteps(platformInfo, selection, context = {}) {
414
480
  gpgInteractive: true,
415
481
  run: async () => {
416
482
  const gpgId = context.gpgId
417
- if (gpgId) return { status: 'skipped', message: `Using existing GPG key ${gpgId}` }
483
+ if (gpgId) return {status: 'skipped', message: `Using existing GPG key ${gpgId}`}
418
484
  // When gpgInteractive=true, the command layer stops the spinner and spawns
419
485
  // gpg --full-generate-key with stdio:inherit so the user sets a strong passphrase.
420
486
  // We never generate a key with an empty passphrase — that would leave the private
@@ -436,12 +502,12 @@ export function buildSteps(platformInfo, selection, context = {}) {
436
502
  requiresConfirmation: true,
437
503
  run: async () => {
438
504
  const path = await which('pass')
439
- if (path) return { status: 'skipped', message: 'pass already installed' }
505
+ if (path) return {status: 'skipped', message: 'pass already installed'}
440
506
  try {
441
507
  await execOrThrow('sudo', ['apt-get', 'install', '-y', 'pass'])
442
- return { status: 'success', message: 'pass installed' }
508
+ return {status: 'success', message: 'pass installed'}
443
509
  } catch {
444
- return { status: 'failed', hint: 'Run manually: sudo apt-get install -y pass' }
510
+ return {status: 'failed', hint: 'Run manually: sudo apt-get install -y pass'}
445
511
  }
446
512
  },
447
513
  })
@@ -456,17 +522,17 @@ export function buildSteps(platformInfo, selection, context = {}) {
456
522
  // Skip if pass is already initialized
457
523
  const lsResult = await exec('pass', ['ls'])
458
524
  if (lsResult.exitCode === 0) {
459
- return { status: 'skipped', message: 'pass store already initialized' }
525
+ return {status: 'skipped', message: 'pass store already initialized'}
460
526
  }
461
527
  const gpgId = context.gpgId
462
528
  if (!gpgId) {
463
- return { status: 'failed', hint: 'No GPG key ID available — complete the create-gpg-key step first' }
529
+ return {status: 'failed', hint: 'No GPG key ID available — complete the create-gpg-key step first'}
464
530
  }
465
531
  try {
466
532
  await execOrThrow('pass', ['init', gpgId])
467
- return { status: 'success', message: `pass initialized with key ${gpgId}` }
533
+ return {status: 'success', message: `pass initialized with key ${gpgId}`}
468
534
  } catch {
469
- return { status: 'failed', hint: `Run manually: pass init ${gpgId}` }
535
+ return {status: 'failed', hint: `Run manually: pass init ${gpgId}`}
470
536
  }
471
537
  },
472
538
  })
@@ -479,12 +545,16 @@ export function buildSteps(platformInfo, selection, context = {}) {
479
545
  requiresConfirmation: true,
480
546
  run: async () => {
481
547
  const existing = await which('aws-vault')
482
- if (existing) return { status: 'skipped', message: 'aws-vault already installed' }
548
+ if (existing) return {status: 'skipped', message: 'aws-vault already installed'}
483
549
  const arch = process.arch === 'arm64' ? 'arm64' : 'amd64'
484
550
  const url = `https://github.com/99designs/aws-vault/releases/latest/download/aws-vault-linux-${arch}`
485
551
  try {
486
- await execOrThrow('sudo', ['sh', '-c', `curl -sSL '${url}' -o /usr/local/bin/aws-vault && chmod +x /usr/local/bin/aws-vault`])
487
- return { status: 'success', message: 'aws-vault installed to /usr/local/bin/aws-vault' }
552
+ await execOrThrow('sudo', [
553
+ 'sh',
554
+ '-c',
555
+ `curl -sSL '${url}' -o /usr/local/bin/aws-vault && chmod +x /usr/local/bin/aws-vault`,
556
+ ])
557
+ return {status: 'success', message: 'aws-vault installed to /usr/local/bin/aws-vault'}
488
558
  } catch {
489
559
  return {
490
560
  status: 'failed',
@@ -505,9 +575,9 @@ export function buildSteps(platformInfo, selection, context = {}) {
505
575
  try {
506
576
  await appendToShellProfile('export AWS_VAULT_BACKEND=pass')
507
577
  await appendToShellProfile('export GPG_TTY=$(tty)')
508
- return { status: 'success', message: 'AWS_VAULT_BACKEND=pass and GPG_TTY added to shell profile' }
578
+ return {status: 'success', message: 'AWS_VAULT_BACKEND=pass and GPG_TTY added to shell profile'}
509
579
  } catch {
510
- return { status: 'failed', hint: 'Add manually to ~/.bashrc or ~/.zshrc: export AWS_VAULT_BACKEND=pass' }
580
+ return {status: 'failed', hint: 'Add manually to ~/.bashrc or ~/.zshrc: export AWS_VAULT_BACKEND=pass'}
511
581
  }
512
582
  },
513
583
  })
@@ -521,10 +591,10 @@ export function buildSteps(platformInfo, selection, context = {}) {
521
591
  run: async () => {
522
592
  const result = await exec('aws-vault', ['--version'])
523
593
  if (result.exitCode !== 0) {
524
- return { status: 'failed', hint: 'aws-vault not found in PATH after install' }
594
+ return {status: 'failed', hint: 'aws-vault not found in PATH after install'}
525
595
  }
526
596
  const version = (result.stdout || result.stderr).trim()
527
- return { status: 'success', message: `aws-vault ${version}` }
597
+ return {status: 'success', message: `aws-vault ${version}`}
528
598
  },
529
599
  })
530
600
  }
@@ -541,9 +611,9 @@ export function buildSteps(platformInfo, selection, context = {}) {
541
611
  run: async () => {
542
612
  const bridgePath = '/mnt/c/Program Files/Git/mingw64/bin/git-credential-manager.exe'
543
613
  if (existsSync(bridgePath)) {
544
- return { status: 'success', message: 'Windows GCM bridge found — using Windows Credential Manager' }
614
+ return {status: 'success', message: 'Windows GCM bridge found — using Windows Credential Manager'}
545
615
  }
546
- return { status: 'skipped', message: 'Windows GCM not found — will install native Linux GCM' }
616
+ return {status: 'skipped', message: 'Windows GCM not found — will install native Linux GCM'}
547
617
  },
548
618
  })
549
619
  }
@@ -556,21 +626,25 @@ export function buildSteps(platformInfo, selection, context = {}) {
556
626
  requiresConfirmation: true,
557
627
  run: async () => {
558
628
  const existing = await which('git-credential-manager')
559
- if (existing) return { status: 'skipped', message: 'Git Credential Manager already installed' }
629
+ if (existing) return {status: 'skipped', message: 'Git Credential Manager already installed'}
560
630
  try {
561
- const latestResult = await exec('sh', ['-c', "curl -sSL https://api.github.com/repos/git-ecosystem/git-credential-manager/releases/latest | grep 'browser_download_url.*gcm.*linux.*amd64.*deb' | head -1 | cut -d '\"' -f 4"])
631
+ const latestResult = await exec('sh', [
632
+ '-c',
633
+ "curl -sSL https://api.github.com/repos/git-ecosystem/git-credential-manager/releases/latest | grep 'browser_download_url.*gcm.*linux.*amd64.*deb' | head -1 | cut -d '\"' -f 4",
634
+ ])
562
635
  const debUrl = latestResult.stdout.trim()
563
636
  if (!debUrl) throw new Error('Could not find GCM deb package URL')
564
637
  // Security: validate debUrl is a legitimate GitHub release asset URL before using it
565
638
  // This prevents command injection if the GitHub API response were tampered with (MITM / supply-chain)
566
- const SAFE_DEB_URL = /^https:\/\/github\.com\/git-ecosystem\/git-credential-manager\/releases\/download\/[a-zA-Z0-9._\-/]+\.deb$/
639
+ const SAFE_DEB_URL =
640
+ /^https:\/\/github\.com\/git-ecosystem\/git-credential-manager\/releases\/download\/[a-zA-Z0-9._\-/]+\.deb$/
567
641
  if (!SAFE_DEB_URL.test(debUrl)) {
568
642
  throw new Error(`Unexpected GCM package URL format: "${debUrl}"`)
569
643
  }
570
644
  // Use array args — no shell interpolation of the URL
571
645
  await execOrThrow('curl', ['-sSL', debUrl, '-o', '/tmp/gcm.deb'])
572
646
  await execOrThrow('sudo', ['dpkg', '-i', '/tmp/gcm.deb'])
573
- return { status: 'success', message: 'Git Credential Manager installed' }
647
+ return {status: 'success', message: 'Git Credential Manager installed'}
574
648
  } catch {
575
649
  return {
576
650
  status: 'failed',
@@ -590,9 +664,9 @@ export function buildSteps(platformInfo, selection, context = {}) {
590
664
  run: async () => {
591
665
  try {
592
666
  await execOrThrow('git-credential-manager', ['configure'])
593
- return { status: 'success', message: 'Git Credential Manager configured' }
667
+ return {status: 'success', message: 'Git Credential Manager configured'}
594
668
  } catch {
595
- return { status: 'failed', hint: 'Run manually: git-credential-manager configure' }
669
+ return {status: 'failed', hint: 'Run manually: git-credential-manager configure'}
596
670
  }
597
671
  },
598
672
  })
@@ -606,9 +680,9 @@ export function buildSteps(platformInfo, selection, context = {}) {
606
680
  run: async () => {
607
681
  try {
608
682
  await execOrThrow('git', ['config', '--global', 'credential.credentialStore', 'gpg'])
609
- return { status: 'success', message: 'GCM credential store set to gpg' }
683
+ return {status: 'success', message: 'GCM credential store set to gpg'}
610
684
  } catch {
611
- return { status: 'failed', hint: 'Run manually: git config --global credential.credentialStore gpg' }
685
+ return {status: 'failed', hint: 'Run manually: git config --global credential.credentialStore gpg'}
612
686
  }
613
687
  },
614
688
  })
@@ -622,9 +696,9 @@ export function buildSteps(platformInfo, selection, context = {}) {
622
696
  run: async () => {
623
697
  const result = await exec('git-credential-manager', ['--version'])
624
698
  if (result.exitCode !== 0) {
625
- return { status: 'failed', hint: 'git-credential-manager not found in PATH' }
699
+ return {status: 'failed', hint: 'git-credential-manager not found in PATH'}
626
700
  }
627
- return { status: 'success', message: `GCM ${result.stdout.trim()}` }
701
+ return {status: 'success', message: `GCM ${result.stdout.trim()}`}
628
702
  },
629
703
  })
630
704
  }
@@ -1,4 +1,4 @@
1
- import { execa } from 'execa'
1
+ import {execa} from 'execa'
2
2
 
3
3
  /** @import { ExecResult } from '../types.js' */
4
4
 
@@ -10,7 +10,7 @@ import { execa } from 'execa'
10
10
  * @returns {Promise<ExecResult>}
11
11
  */
12
12
  export async function exec(command, args = [], opts = {}) {
13
- const result = await execa(command, args, { reject: false, ...opts })
13
+ const result = await execa(command, args, {reject: false, ...opts})
14
14
  return {
15
15
  stdout: result.stdout?.trim() ?? '',
16
16
  stderr: result.stderr?.trim() ?? '',
@@ -24,7 +24,7 @@ export async function exec(command, args = [], opts = {}) {
24
24
  * @returns {Promise<string|null>} Resolved path or null if not found
25
25
  */
26
26
  export async function which(binary) {
27
- const result = await execa('which', [binary], { reject: false })
27
+ const result = await execa('which', [binary], {reject: false})
28
28
  if (result.exitCode !== 0 || !result.stdout) return null
29
29
  return result.stdout.trim()
30
30
  }
@@ -37,6 +37,6 @@ export async function which(binary) {
37
37
  * @returns {Promise<string>}
38
38
  */
39
39
  export async function execOrThrow(command, args = [], opts = {}) {
40
- const result = await execa(command, args, { reject: true, ...opts })
40
+ const result = await execa(command, args, {reject: true, ...opts})
41
41
  return result.stdout?.trim() ?? ''
42
42
  }
@@ -1,4 +1,4 @@
1
- import { DvmiError } from '../utils/errors.js'
1
+ import {DvmiError} from '../utils/errors.js'
2
2
 
3
3
  /** @import { Skill } from '../types.js' */
4
4
 
@@ -37,17 +37,11 @@ export async function searchSkills(query, limit = 50) {
37
37
  try {
38
38
  res = await fetch(url.toString())
39
39
  } catch {
40
- throw new DvmiError(
41
- 'Unable to reach skills.sh API',
42
- 'Check your internet connection and try again',
43
- )
40
+ throw new DvmiError('Unable to reach skills.sh API', 'Check your internet connection and try again')
44
41
  }
45
42
 
46
43
  if (!res.ok) {
47
- throw new DvmiError(
48
- `skills.sh API returned ${res.status}`,
49
- 'Try again later or visit https://skills.sh',
50
- )
44
+ throw new DvmiError(`skills.sh API returned ${res.status}`, 'Try again later or visit https://skills.sh')
51
45
  }
52
46
 
53
47
  /** @type {unknown} */
@@ -1,6 +1,6 @@
1
- import { execa } from 'execa'
2
- import { which, exec } from './shell.js'
3
- import { DvmiError } from '../utils/errors.js'
1
+ import {execa} from 'execa'
2
+ import {which, exec} from './shell.js'
3
+ import {DvmiError} from '../utils/errors.js'
4
4
 
5
5
  /** GitHub spec-kit package source for uv.
6
6
  * TODO: pin to a specific tagged release (e.g. #v1.x.x) once one is available upstream.
@@ -68,9 +68,6 @@ export async function runSpecifyInit(cwd, opts = {}) {
68
68
  })
69
69
 
70
70
  if (result.exitCode !== 0) {
71
- throw new DvmiError(
72
- '`specify init` exited with a non-zero code',
73
- 'Check the output above for details',
74
- )
71
+ throw new DvmiError('`specify init` exited with a non-zero code', 'Check the output above for details')
75
72
  }
76
73
  }
@@ -1,8 +1,8 @@
1
- import { readFile } from 'node:fs/promises'
2
- import { join } from 'node:path'
3
- import { fileURLToPath } from 'node:url'
4
- import { loadConfig, saveConfig } from './config.js'
5
- import { exec } from './shell.js'
1
+ import {readFile} from 'node:fs/promises'
2
+ import {join} from 'node:path'
3
+ import {fileURLToPath} from 'node:url'
4
+ import {loadConfig, saveConfig} from './config.js'
5
+ import {exec} from './shell.js'
6
6
 
7
7
  const PKG_PATH = join(fileURLToPath(import.meta.url), '..', '..', '..', 'package.json')
8
8
  const REPO = 'devvami/devvami'
@@ -22,7 +22,7 @@ export async function getCurrentVersion() {
22
22
  * @param {{ force?: boolean }} [opts]
23
23
  * @returns {Promise<string|null>}
24
24
  */
25
- export async function getLatestVersion({ force = false } = {}) {
25
+ export async function getLatestVersion({force = false} = {}) {
26
26
  const config = await loadConfig()
27
27
  const now = Date.now()
28
28
  const lastCheck = config.lastVersionCheck ? new Date(config.lastVersionCheck).getTime() : 0
@@ -38,7 +38,7 @@ export async function getLatestVersion({ force = false } = {}) {
38
38
  // Il tag è nel formato "v1.0.0" — rimuove il prefisso "v"
39
39
  const latest = result.stdout.trim().replace(/^v/, '') || null
40
40
  if (latest) {
41
- await saveConfig({ ...config, latestVersion: latest, lastVersionCheck: new Date().toISOString() })
41
+ await saveConfig({...config, latestVersion: latest, lastVersionCheck: new Date().toISOString()})
42
42
  }
43
43
  return latest
44
44
  } catch {
@@ -51,8 +51,8 @@ export async function getLatestVersion({ force = false } = {}) {
51
51
  * @param {{ force?: boolean }} [opts]
52
52
  * @returns {Promise<{ hasUpdate: boolean, current: string, latest: string|null }>}
53
53
  */
54
- export async function checkForUpdate({ force = false } = {}) {
55
- const [current, latest] = await Promise.all([getCurrentVersion(), getLatestVersion({ force })])
54
+ export async function checkForUpdate({force = false} = {}) {
55
+ const [current, latest] = await Promise.all([getCurrentVersion(), getLatestVersion({force})])
56
56
  const hasUpdate = Boolean(latest && latest !== current)
57
- return { hasUpdate, current, latest }
57
+ return {hasUpdate, current, latest}
58
58
  }
package/src/types.js CHANGED
@@ -332,6 +332,91 @@
332
332
  * @typedef {'macos'|'wsl2'|'linux'} Platform
333
333
  */
334
334
 
335
+ // ──────────────────────────────────────────────────────────────────────────────
336
+ // AI Config Sync TUI types
337
+ // ──────────────────────────────────────────────────────────────────────────────
338
+
339
+ /**
340
+ * @typedef {'mcp'|'command'|'skill'|'agent'} CategoryType
341
+ */
342
+
343
+ /**
344
+ * @typedef {'vscode-copilot'|'claude-code'|'opencode'|'gemini-cli'|'copilot-cli'} EnvironmentId
345
+ */
346
+
347
+ /**
348
+ * @typedef {Object} MCPParams
349
+ * @property {'stdio'|'sse'|'streamable-http'} transport - MCP transport type
350
+ * @property {string} [command] - Command to execute (required for stdio transport)
351
+ * @property {string[]} [args] - Command arguments
352
+ * @property {Record<string, string>} [env] - Environment variables
353
+ * @property {string} [url] - Server URL (required for sse/streamable-http transport)
354
+ */
355
+
356
+ /**
357
+ * @typedef {Object} CommandParams
358
+ * @property {string} content - Prompt/command text content (multi-line)
359
+ * @property {string} [description] - Short description of the command
360
+ */
361
+
362
+ /**
363
+ * @typedef {Object} SkillParams
364
+ * @property {string} content - Skill definition content (multi-line)
365
+ * @property {string} [description] - Short description of the skill
366
+ */
367
+
368
+ /**
369
+ * @typedef {Object} AgentParams
370
+ * @property {string} instructions - Agent instructions (multi-line)
371
+ * @property {string} [description] - Short description of the agent
372
+ */
373
+
374
+ /**
375
+ * @typedef {Object} CategoryEntry
376
+ * @property {string} id - UUID v4, auto-generated
377
+ * @property {string} name - Unique within its type; used as filename/key when deploying
378
+ * @property {CategoryType} type - Category type
379
+ * @property {boolean} active - true = deployed to environments, false = removed but kept in store
380
+ * @property {EnvironmentId[]} environments - Target environments for deployment
381
+ * @property {MCPParams|CommandParams|SkillParams|AgentParams} params - Type-specific parameters
382
+ * @property {string} createdAt - ISO 8601 timestamp
383
+ * @property {string} updatedAt - ISO 8601 timestamp
384
+ */
385
+
386
+ /**
387
+ * @typedef {Object} AIConfigStore
388
+ * @property {number} version - Schema version
389
+ * @property {CategoryEntry[]} entries - All managed configuration entries
390
+ */
391
+
392
+ /**
393
+ * @typedef {Object} PathStatus
394
+ * @property {string} path - Absolute path
395
+ * @property {boolean} exists - Whether the path exists on disk
396
+ * @property {boolean} readable - Whether the file could be parsed (for JSON/TOML files)
397
+ */
398
+
399
+ /**
400
+ * @typedef {Object} CategoryCounts
401
+ * @property {number} mcp
402
+ * @property {number} command
403
+ * @property {number} skill
404
+ * @property {number} agent
405
+ */
406
+
407
+ /**
408
+ * @typedef {Object} DetectedEnvironment
409
+ * @property {EnvironmentId} id - Environment identifier
410
+ * @property {string} name - Display name (e.g. "Claude Code")
411
+ * @property {boolean} detected - Whether any config files were found
412
+ * @property {PathStatus[]} projectPaths - Project-level paths and their existence status
413
+ * @property {PathStatus[]} globalPaths - Global-level paths and their existence status
414
+ * @property {string[]} unreadable - Paths that exist but failed to parse
415
+ * @property {CategoryType[]} supportedCategories - Category types this environment supports
416
+ * @property {CategoryCounts} counts - Per-category item counts from dvmi-managed entries
417
+ * @property {'project'|'global'|'both'} scope - Where detection occurred
418
+ */
419
+
335
420
  /**
336
421
  * @typedef {Object} PlatformInfo
337
422
  * @property {Platform} platform