devvami 1.4.2 → 1.5.1

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 +72 -0
  2. package/oclif.manifest.json +275 -235
  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 +257 -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 +215 -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 +349 -0
  62. package/src/services/ai-env-deployer.js +650 -0
  63. package/src/services/ai-env-scanner.js +983 -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 +117 -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 +1184 -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 +1089 -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
package/src/types.js CHANGED
@@ -332,6 +332,123 @@
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'|'rule'|'skill'|'agent'} CategoryType
341
+ */
342
+
343
+ /**
344
+ * @typedef {'vscode-copilot'|'claude-code'|'claude-desktop'|'opencode'|'gemini-cli'|'copilot-cli'|'cursor'|'windsurf'|'continue-dev'|'zed'|'amazon-q'} 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} RuleParams
364
+ * @property {string} content - Rules/instructions content (multi-line Markdown)
365
+ * @property {string} [description] - Short description of the rule
366
+ */
367
+
368
+ /**
369
+ * @typedef {Object} SkillParams
370
+ * @property {string} content - Skill definition content (multi-line)
371
+ * @property {string} [description] - Short description of the skill
372
+ */
373
+
374
+ /**
375
+ * @typedef {Object} AgentParams
376
+ * @property {string} instructions - Agent instructions (multi-line)
377
+ * @property {string} [description] - Short description of the agent
378
+ */
379
+
380
+ /**
381
+ * @typedef {Object} CategoryEntry
382
+ * @property {string} id - UUID v4, auto-generated
383
+ * @property {string} name - Unique within its type; used as filename/key when deploying
384
+ * @property {CategoryType} type - Category type
385
+ * @property {boolean} active - true = deployed to environments, false = removed but kept in store
386
+ * @property {EnvironmentId[]} environments - Target environments for deployment
387
+ * @property {MCPParams|CommandParams|RuleParams|SkillParams|AgentParams} params - Type-specific parameters
388
+ * @property {string} createdAt - ISO 8601 timestamp
389
+ * @property {string} updatedAt - ISO 8601 timestamp
390
+ */
391
+
392
+ /**
393
+ * @typedef {Object} AIConfigStore
394
+ * @property {number} version - Schema version
395
+ * @property {CategoryEntry[]} entries - All managed configuration entries
396
+ */
397
+
398
+ /**
399
+ * @typedef {Object} PathStatus
400
+ * @property {string} path - Absolute path
401
+ * @property {boolean} exists - Whether the path exists on disk
402
+ * @property {boolean} readable - Whether the file could be parsed (for JSON/TOML files)
403
+ */
404
+
405
+ /**
406
+ * @typedef {Object} CategoryCounts
407
+ * @property {number} mcp
408
+ * @property {number} command
409
+ * @property {number} rule
410
+ * @property {number} skill
411
+ * @property {number} agent
412
+ */
413
+
414
+ /**
415
+ * @typedef {Object} NativeEntry
416
+ * Runtime only — not persisted. Represents an item found in an environment's config
417
+ * file that is NOT managed by dvmi.
418
+ * @property {string} name - Entry name (extracted from config key or filename)
419
+ * @property {CategoryType} type - Category type
420
+ * @property {EnvironmentId} environmentId - Source environment
421
+ * @property {'project'|'global'} level - Whether from project-level or global-level config
422
+ * @property {string} sourcePath - Absolute path to the source config file
423
+ * @property {object} params - Normalized parameters (same structure as managed entry params)
424
+ */
425
+
426
+ /**
427
+ * @typedef {Object} DriftInfo
428
+ * Runtime only — not persisted. Describes a managed entry whose deployed state
429
+ * diverges from dvmi's stored expected state.
430
+ * @property {string} entryId - ID of the managed CategoryEntry that drifted
431
+ * @property {EnvironmentId} environmentId - Environment where drift was detected
432
+ * @property {object} expected - What dvmi expects (from store)
433
+ * @property {object} actual - What was found in the file
434
+ */
435
+
436
+ /**
437
+ * @typedef {Object} DetectedEnvironment
438
+ * @property {EnvironmentId} id - Environment identifier
439
+ * @property {string} name - Display name (e.g. "Claude Code")
440
+ * @property {boolean} detected - Whether any config files were found
441
+ * @property {PathStatus[]} projectPaths - Project-level paths and their existence status
442
+ * @property {PathStatus[]} globalPaths - Global-level paths and their existence status
443
+ * @property {string[]} unreadable - Paths that exist but failed to parse
444
+ * @property {CategoryType[]} supportedCategories - Category types this environment supports
445
+ * @property {CategoryCounts} counts - Per-category item counts from dvmi-managed entries
446
+ * @property {CategoryCounts} nativeCounts - Per-category native item counts (items in config files)
447
+ * @property {NativeEntry[]} nativeEntries - All native entries found for this environment
448
+ * @property {DriftInfo[]} driftedEntries - Managed entries that have drifted from expected state
449
+ * @property {'project'|'global'|'both'} scope - Where detection occurred
450
+ */
451
+
335
452
  /**
336
453
  * @typedef {Object} PlatformInfo
337
454
  * @property {Platform} platform
@@ -1,5 +1,5 @@
1
- import { loadConfigSync } from '../services/config.js'
2
- import { execa } from 'execa'
1
+ import {loadConfigSync} from '../services/config.js'
2
+ import {execa} from 'execa'
3
3
 
4
4
  /**
5
5
  * Returns the aws-vault exec prefix to prepend to AWS CLI commands.
@@ -44,10 +44,7 @@ export function awsVaultPrefix(config = null) {
44
44
  * @returns {boolean}
45
45
  */
46
46
  export function hasAwsCredentialEnv() {
47
- return Boolean(
48
- process.env.AWS_ACCESS_KEY_ID ||
49
- process.env.AWS_SESSION_TOKEN,
50
- )
47
+ return Boolean(process.env.AWS_ACCESS_KEY_ID || process.env.AWS_SESSION_TOKEN)
51
48
  }
52
49
 
53
50
  /**
@@ -79,24 +76,14 @@ export async function reexecCurrentCommandWithAwsVault(config = null) {
79
76
  if (process.env.DVMI_AWS_VAULT_REEXEC === '1') return null
80
77
 
81
78
  try {
82
- const child = await execa(
83
- 'aws-vault',
84
- [
85
- 'exec',
86
- profile,
87
- '--',
88
- process.execPath,
89
- ...process.argv.slice(1),
90
- ],
91
- {
92
- reject: false,
93
- stdio: 'inherit',
94
- env: {
95
- ...process.env,
96
- DVMI_AWS_VAULT_REEXEC: '1',
97
- },
79
+ const child = await execa('aws-vault', ['exec', profile, '--', process.execPath, ...process.argv.slice(1)], {
80
+ reject: false,
81
+ stdio: 'inherit',
82
+ env: {
83
+ ...process.env,
84
+ DVMI_AWS_VAULT_REEXEC: '1',
98
85
  },
99
- )
86
+ })
100
87
 
101
88
  return child.exitCode ?? 1
102
89
  } catch {
@@ -117,25 +104,15 @@ export async function reexecCurrentCommandWithAwsVaultProfile(profile, extraEnv
117
104
  if (!profile) return null
118
105
 
119
106
  try {
120
- const child = await execa(
121
- 'aws-vault',
122
- [
123
- 'exec',
124
- profile,
125
- '--',
126
- process.execPath,
127
- ...process.argv.slice(1),
128
- ],
129
- {
130
- reject: false,
131
- stdio: 'inherit',
132
- env: {
133
- ...process.env,
134
- DVMI_AWS_VAULT_REEXEC: '1',
135
- ...extraEnv,
136
- },
107
+ const child = await execa('aws-vault', ['exec', profile, '--', process.execPath, ...process.argv.slice(1)], {
108
+ reject: false,
109
+ stdio: 'inherit',
110
+ env: {
111
+ ...process.env,
112
+ DVMI_AWS_VAULT_REEXEC: '1',
113
+ ...extraEnv,
137
114
  },
138
- )
115
+ })
139
116
 
140
117
  return child.exitCode ?? 1
141
118
  } catch {
@@ -1,6 +1,6 @@
1
1
  import figlet from 'figlet'
2
2
  import chalk from 'chalk'
3
- import { BRAND_GRADIENT, animateGradientBanner, isColorEnabled } from './gradient.js'
3
+ import {BRAND_GRADIENT, animateGradientBanner, isColorEnabled} from './gradient.js'
4
4
 
5
5
  // Brand colors
6
6
  export const ORANGE = '#FF6B2B'
@@ -24,13 +24,11 @@ function figletAsync(text, opts) {
24
24
  * @returns {Promise<void>}
25
25
  */
26
26
  export async function printBanner() {
27
- const art = await figletAsync('DVMI', { font: 'ANSI Shadow' })
28
- const artLines = art.split('\n').filter((l) => l.trim() !== '')
29
- const width = Math.max(...artLines.map((l) => l.length)) + 4
27
+ const art = await figletAsync('DVMI', {font: 'ANSI Shadow'})
28
+ const artLines = art.split('\n').filter((l) => l.trim() !== '')
29
+ const width = Math.max(...artLines.map((l) => l.length)) + 4
30
30
 
31
- const tagline = isColorEnabled
32
- ? chalk.hex(BLUE).bold(' Devvami Developer CLI')
33
- : ' Devvami Developer CLI'
31
+ const tagline = isColorEnabled ? chalk.hex(BLUE).bold(' Devvami Developer CLI') : ' Devvami Developer CLI'
34
32
 
35
33
  const separator = isColorEnabled
36
34
  ? chalk.hex(BLUE).dim('─'.repeat(Math.min(width, 60)))
@@ -2,14 +2,14 @@
2
2
  * Base CLI error with an actionable hint for the user.
3
3
  */
4
4
  export class DvmiError extends Error {
5
- /**
6
- * @param {string} message - Human-readable error message
7
- * @param {string} hint - Actionable suggestion to resolve the error
8
- * @param {number} [exitCode] - Process exit code (default: 1)
9
- */
10
- constructor(message, hint, exitCode = 1) {
11
- super(message)
12
- this.name = 'DvmiError'
5
+ /**
6
+ * @param {string} message - Human-readable error message
7
+ * @param {string} hint - Actionable suggestion to resolve the error
8
+ * @param {number} [exitCode] - Process exit code (default: 1)
9
+ */
10
+ constructor(message, hint, exitCode = 1) {
11
+ super(message)
12
+ this.name = 'DvmiError'
13
13
  /** @type {string} */
14
14
  this.hint = hint
15
15
  /** @type {number} */
@@ -21,43 +21,39 @@ export class DvmiError extends Error {
21
21
  * Validation error for invalid user input (exit code 2).
22
22
  */
23
23
  export class ValidationError extends DvmiError {
24
- /**
25
- * @param {string} message
26
- * @param {string} hint
27
- */
28
- constructor(message, hint) {
29
- super(message, hint, 2)
30
- this.name = 'ValidationError'
31
- // oclif reads this.oclif.exit to determine the process exit code
32
- this.oclif = { exit: 2 }
33
- }
34
- }
24
+ /**
25
+ * @param {string} message
26
+ * @param {string} hint
27
+ */
28
+ constructor(message, hint) {
29
+ super(message, hint, 2)
30
+ this.name = 'ValidationError'
31
+ // oclif reads this.oclif.exit to determine the process exit code
32
+ this.oclif = {exit: 2}
33
+ }
34
+ }
35
35
 
36
- /**
37
- * Auth error for missing or expired authentication.
38
- */
39
- export class AuthError extends DvmiError {
40
- /**
41
- * @param {string} service - Service name (e.g. "GitHub", "AWS")
42
- */
43
- constructor(service) {
44
- super(
45
- `${service} authentication required`,
46
- `Run \`dvmi auth login\` to authenticate`,
47
- 1,
48
- )
49
- this.name = 'AuthError'
50
- }
51
- }
36
+ /**
37
+ * Auth error for missing or expired authentication.
38
+ */
39
+ export class AuthError extends DvmiError {
40
+ /**
41
+ * @param {string} service - Service name (e.g. "GitHub", "AWS")
42
+ */
43
+ constructor(service) {
44
+ super(`${service} authentication required`, `Run \`dvmi auth login\` to authenticate`, 1)
45
+ this.name = 'AuthError'
46
+ }
47
+ }
52
48
 
53
- /**
54
- * Format an error for display in the terminal.
55
- * @param {Error} err
56
- * @returns {string}
57
- */
58
- export function formatError(err) {
59
- if (err instanceof DvmiError) {
60
- return `Error: ${err.message}\nHint: ${err.hint}`
61
- }
62
- return `Error: ${err.message}`
63
- }
49
+ /**
50
+ * Format an error for display in the terminal.
51
+ * @param {Error} err
52
+ * @returns {string}
53
+ */
54
+ export function formatError(err) {
55
+ if (err instanceof DvmiError) {
56
+ return `Error: ${err.message}\nHint: ${err.hint}`
57
+ }
58
+ return `Error: ${err.message}`
59
+ }
@@ -18,7 +18,7 @@ import yaml from 'js-yaml'
18
18
  export function parseFrontmatter(content) {
19
19
  const match = content.match(/^---\r?\n([\s\S]*?)---\r?\n?([\s\S]*)$/)
20
20
  if (!match) {
21
- return { frontmatter: {}, body: content }
21
+ return {frontmatter: {}, body: content}
22
22
  }
23
23
  const rawYaml = match[1]
24
24
  const body = match[2] ?? ''
@@ -28,9 +28,9 @@ export function parseFrontmatter(content) {
28
28
  parsed && typeof parsed === 'object' && !Array.isArray(parsed)
29
29
  ? /** @type {Record<string, unknown>} */ (parsed)
30
30
  : {}
31
- return { frontmatter, body }
31
+ return {frontmatter, body}
32
32
  } catch {
33
- return { frontmatter: {}, body: content }
33
+ return {frontmatter: {}, body: content}
34
34
  }
35
35
  }
36
36
 
@@ -47,6 +47,6 @@ export function serializeFrontmatter(frontmatter, body) {
47
47
  if (!frontmatter || Object.keys(frontmatter).length === 0) {
48
48
  return body
49
49
  }
50
- const yamlStr = yaml.dump(frontmatter, { lineWidth: -1 }).trimEnd()
50
+ const yamlStr = yaml.dump(frontmatter, {lineWidth: -1}).trimEnd()
51
51
  return `---\n${yamlStr}\n---\n${body}`
52
52
  }
@@ -12,9 +12,9 @@ import readline from 'node:readline'
12
12
 
13
13
  /** @type {GradientStop[]} */
14
14
  export const BRAND_GRADIENT = [
15
- [0, 212, 255], // #00D4FF — ciano elettrico
16
- [0, 100, 255], // #0064FF — blu vivido
17
- [100, 0, 220], // #6400DC — indaco profondo
15
+ [0, 212, 255], // #00D4FF — ciano elettrico
16
+ [0, 100, 255], // #0064FF — blu vivido
17
+ [100, 0, 220], // #6400DC — indaco profondo
18
18
  ]
19
19
 
20
20
  // ──────────────────────────────────────────────────────────────────────────────
@@ -51,24 +51,26 @@ export function gradientText(text, stops, phase = 0) {
51
51
 
52
52
  const segments = stops.length - 1
53
53
 
54
- return chars.map((char, i) => {
55
- if (char === ' ') return char
54
+ return chars
55
+ .map((char, i) => {
56
+ if (char === ' ') return char
56
57
 
57
- // Normalise t in [0, 1] with phase shift
58
- const t = ((i / Math.max(len - 1, 1)) + phase) % 1
58
+ // Normalise t in [0, 1] with phase shift
59
+ const t = (i / Math.max(len - 1, 1) + phase) % 1
59
60
 
60
- const seg = Math.min(Math.floor(t * segments), segments - 1)
61
- const localT = t * segments - seg
61
+ const seg = Math.min(Math.floor(t * segments), segments - 1)
62
+ const localT = t * segments - seg
62
63
 
63
- const [r1, g1, b1] = stops[seg]
64
- const [r2, g2, b2] = stops[seg + 1]
64
+ const [r1, g1, b1] = stops[seg]
65
+ const [r2, g2, b2] = stops[seg + 1]
65
66
 
66
- const r = Math.round(r1 + (r2 - r1) * localT)
67
- const g = Math.round(g1 + (g2 - g1) * localT)
68
- const b = Math.round(b1 + (b2 - b1) * localT)
67
+ const r = Math.round(r1 + (r2 - r1) * localT)
68
+ const g = Math.round(g1 + (g2 - g1) * localT)
69
+ const b = Math.round(b1 + (b2 - b1) * localT)
69
70
 
70
- return chalk.rgb(r, g, b)(char)
71
- }).join('')
71
+ return chalk.rgb(r, g, b)(char)
72
+ })
73
+ .join('')
72
74
  }
73
75
 
74
76
  // ──────────────────────────────────────────────────────────────────────────────
@@ -1,6 +1,6 @@
1
1
  import open from 'open'
2
- import { detectPlatform } from '../services/platform.js'
3
- import { exec } from '../services/shell.js'
2
+ import {detectPlatform} from '../services/platform.js'
3
+ import {exec} from '../services/shell.js'
4
4
 
5
5
  /**
6
6
  * Open a URL in the default browser, using the platform-appropriate command.
@@ -8,7 +8,7 @@ import { exec } from '../services/shell.js'
8
8
  * @returns {Promise<void>}
9
9
  */
10
10
  export async function openBrowser(url) {
11
- const { platform, openCommand } = await detectPlatform()
11
+ const {platform, openCommand} = await detectPlatform()
12
12
 
13
13
  if (platform === 'macos') {
14
14
  await open(url)