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,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 { input, confirm } from '@inquirer/prompts'
5
- import { listTemplates, createFromTemplate, setBranchProtection, enableDependabot } from '../../services/github.js'
6
- import { loadConfig } from '../../services/config.js'
7
- import { validateRepoName } from '../../validators/repo-name.js'
8
- import { renderTable } from '../../formatters/table.js'
9
- import { exec } from '../../services/shell.js'
4
+ import {input, confirm} from '@inquirer/prompts'
5
+ import {listTemplates, createFromTemplate, setBranchProtection, enableDependabot} from '../../services/github.js'
6
+ import {loadConfig} from '../../services/config.js'
7
+ import {validateRepoName} from '../../validators/repo-name.js'
8
+ import {renderTable} from '../../formatters/table.js'
9
+ import {exec} from '../../services/shell.js'
10
10
 
11
11
  /**
12
12
  * @param {string} lang
@@ -16,15 +16,15 @@ function langColor(lang) {
16
16
  const map = {
17
17
  javascript: chalk.yellow,
18
18
  typescript: chalk.blue,
19
- python: chalk.green,
20
- java: chalk.red,
21
- go: chalk.cyan,
22
- ruby: chalk.magenta,
23
- rust: chalk.hex('#CE422B'),
24
- kotlin: chalk.hex('#7F52FF'),
25
- swift: chalk.hex('#F05138'),
26
- php: chalk.hex('#777BB4'),
27
- shell: chalk.greenBright,
19
+ python: chalk.green,
20
+ java: chalk.red,
21
+ go: chalk.cyan,
22
+ ruby: chalk.magenta,
23
+ rust: chalk.hex('#CE422B'),
24
+ kotlin: chalk.hex('#7F52FF'),
25
+ swift: chalk.hex('#F05138'),
26
+ php: chalk.hex('#777BB4'),
27
+ shell: chalk.greenBright,
28
28
  }
29
29
  const fn = map[lang.toLowerCase()]
30
30
  return fn ? fn(lang) : chalk.dim(lang)
@@ -43,21 +43,21 @@ export default class CreateRepo extends Command {
43
43
  static enableJsonFlag = true
44
44
 
45
45
  static args = {
46
- template: Args.string({ description: 'Nome del template', required: false }),
46
+ template: Args.string({description: 'Nome del template', required: false}),
47
47
  }
48
48
 
49
49
  static flags = {
50
- list: Flags.boolean({ description: 'Lista template disponibili', default: false }),
51
- search: Flags.string({ char: 's', description: 'Cerca in nome e descrizione dei template (case-insensitive)' }),
52
- name: Flags.string({ description: 'Nome del nuovo repository' }),
53
- description: Flags.string({ description: 'Descrizione del repository', default: '' }),
54
- private: Flags.boolean({ description: 'Repository privato (default)', default: true }),
55
- public: Flags.boolean({ description: 'Repository pubblico', default: false }),
56
- 'dry-run': Flags.boolean({ description: 'Preview senza eseguire', default: false }),
50
+ list: Flags.boolean({description: 'Lista template disponibili', default: false}),
51
+ search: Flags.string({char: 's', description: 'Cerca in nome e descrizione dei template (case-insensitive)'}),
52
+ name: Flags.string({description: 'Nome del nuovo repository'}),
53
+ description: Flags.string({description: 'Descrizione del repository', default: ''}),
54
+ private: Flags.boolean({description: 'Repository privato (default)', default: true}),
55
+ public: Flags.boolean({description: 'Repository pubblico', default: false}),
56
+ 'dry-run': Flags.boolean({description: 'Preview senza eseguire', default: false}),
57
57
  }
58
58
 
59
59
  async run() {
60
- const { args, flags } = await this.parse(CreateRepo)
60
+ const {args, flags} = await this.parse(CreateRepo)
61
61
  const isJson = flags.json
62
62
  const isDryRun = flags['dry-run']
63
63
  const config = await loadConfig()
@@ -68,51 +68,58 @@ export default class CreateRepo extends Command {
68
68
 
69
69
  // --list mode
70
70
  if (flags.list || !args.template) {
71
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching templates...') }).start()
71
+ const spinner = isJson
72
+ ? null
73
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching templates...')}).start()
72
74
  const templates = await listTemplates(config.org)
73
75
  spinner?.stop()
74
76
 
75
77
  // Search filter
76
78
  const searchQuery = flags.search?.toLowerCase()
77
79
  const filtered = searchQuery
78
- ? templates.filter((t) =>
79
- t.name.toLowerCase().includes(searchQuery) ||
80
- t.description.toLowerCase().includes(searchQuery),
80
+ ? templates.filter(
81
+ (t) => t.name.toLowerCase().includes(searchQuery) || t.description.toLowerCase().includes(searchQuery),
81
82
  )
82
83
  : templates
83
84
 
84
- if (isJson) return { templates: filtered }
85
+ if (isJson) return {templates: filtered}
85
86
 
86
87
  if (templates.length === 0) {
87
88
  this.log(chalk.yellow('No templates found in the organization.'))
88
89
  this.log(chalk.dim('Templates are GitHub repos marked as "Template repository".'))
89
- return { templates: [] }
90
+ return {templates: []}
90
91
  }
91
92
 
92
93
  if (filtered.length === 0) {
93
94
  this.log(chalk.dim(`No templates matching "${flags.search}".`))
94
- return { templates: [] }
95
+ return {templates: []}
95
96
  }
96
97
 
97
- const filterInfo = flags.search
98
- ? chalk.dim(' — search: ') + chalk.white(`"${flags.search}"`)
99
- : ''
98
+ const filterInfo = flags.search ? chalk.dim(' — search: ') + chalk.white(`"${flags.search}"`) : ''
100
99
 
101
100
  this.log(
102
101
  chalk.bold('\nAvailable templates') +
103
- filterInfo +
104
- chalk.dim(` (${filtered.length}${filtered.length < templates.length ? `/${templates.length}` : ''})`) +
105
- '\n',
102
+ filterInfo +
103
+ chalk.dim(` (${filtered.length}${filtered.length < templates.length ? `/${templates.length}` : ''})`) +
104
+ '\n',
106
105
  )
107
106
 
108
- this.log(renderTable(filtered, [
109
- { header: 'Name', key: 'name', width: 35 },
110
- { header: 'Language', key: 'language', width: 14, format: (v) => v || '—', colorize: (v) => v === '—' ? chalk.dim(v) : langColor(v) },
111
- { header: 'Description', key: 'description', width: 60, format: (v) => String(v || '—') },
112
- ]))
107
+ this.log(
108
+ renderTable(filtered, [
109
+ {header: 'Name', key: 'name', width: 35},
110
+ {
111
+ header: 'Language',
112
+ key: 'language',
113
+ width: 14,
114
+ format: (v) => v || '—',
115
+ colorize: (v) => (v === '—' ? chalk.dim(v) : langColor(v)),
116
+ },
117
+ {header: 'Description', key: 'description', width: 60, format: (v) => String(v || '—')},
118
+ ]),
119
+ )
113
120
 
114
121
  this.log('')
115
- return { templates: filtered }
122
+ return {templates: filtered}
116
123
  }
117
124
 
118
125
  // Create mode
@@ -126,7 +133,7 @@ export default class CreateRepo extends Command {
126
133
  // Get repo name
127
134
  let repoName = flags.name
128
135
  if (!repoName && !isJson) {
129
- repoName = await input({ message: 'Repository name:' })
136
+ repoName = await input({message: 'Repository name:'})
130
137
  } else if (!repoName) {
131
138
  this.error('--name is required in non-interactive mode')
132
139
  }
@@ -142,13 +149,16 @@ export default class CreateRepo extends Command {
142
149
  const ok = await confirm({
143
150
  message: `Create ${isPrivate ? 'private' : 'public'} repo "${config.org}/${repoName}" from "${args.template}"?`,
144
151
  })
145
- if (!ok) { this.log('Aborted.'); return }
152
+ if (!ok) {
153
+ this.log('Aborted.')
154
+ return
155
+ }
146
156
  }
147
157
 
148
158
  if (isDryRun) {
149
159
  const preview = {
150
- repository: { name: repoName, org: config.org, template: args.template, private: isPrivate },
151
- postScaffolding: { branchProtection: 'would configure', dependabot: 'would enable', codeowners: 'would create' },
160
+ repository: {name: repoName, org: config.org, template: args.template, private: isPrivate},
161
+ postScaffolding: {branchProtection: 'would configure', dependabot: 'would enable', codeowners: 'would create'},
152
162
  }
153
163
  if (isJson) return preview
154
164
  this.log(chalk.bold('\nDry run preview:'))
@@ -157,7 +167,9 @@ export default class CreateRepo extends Command {
157
167
  }
158
168
 
159
169
  // Create repo
160
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Creating repository...') }).start()
170
+ const spinner = isJson
171
+ ? null
172
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Creating repository...')}).start()
161
173
  const repo = await createFromTemplate({
162
174
  templateOwner: config.org,
163
175
  templateRepo: args.template,
@@ -169,22 +181,28 @@ export default class CreateRepo extends Command {
169
181
  spinner?.succeed(`Repository created: ${repo.htmlUrl}`)
170
182
 
171
183
  // Post-scaffolding
172
- const bpSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Configuring branch protection...') }).start()
184
+ const bpSpinner = isJson
185
+ ? null
186
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Configuring branch protection...')}).start()
173
187
  await setBranchProtection(config.org, repoName).catch(() => null)
174
188
  bpSpinner?.succeed('Branch protection configured')
175
189
 
176
- const depSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Enabling Dependabot...') }).start()
190
+ const depSpinner = isJson
191
+ ? null
192
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Enabling Dependabot...')}).start()
177
193
  await enableDependabot(config.org, repoName).catch(() => null)
178
194
  depSpinner?.succeed('Dependabot enabled')
179
195
 
180
196
  // Clone
181
- const cloneSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Cloning repository...') }).start()
197
+ const cloneSpinner = isJson
198
+ ? null
199
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Cloning repository...')}).start()
182
200
  await exec('gh', ['repo', 'clone', `${config.org}/${repoName}`])
183
201
  cloneSpinner?.succeed(`Cloned to ./${repoName}`)
184
202
 
185
203
  const result = {
186
- repository: { name: repoName, url: repo.htmlUrl, localPath: `./${repoName}` },
187
- postScaffolding: { branchProtection: 'ok', dependabot: 'ok', codeowners: 'ok' },
204
+ repository: {name: repoName, url: repo.htmlUrl, localPath: `./${repoName}`},
205
+ postScaffolding: {branchProtection: 'ok', dependabot: 'ok', codeowners: 'ok'},
188
206
  }
189
207
 
190
208
  if (!isJson) {
@@ -1,17 +1,17 @@
1
- import { Command, Flags } from '@oclif/core'
1
+ import {Command, Flags} from '@oclif/core'
2
2
  import chalk from 'chalk'
3
3
  import ora from 'ora'
4
- import { loadConfig } from '../../services/config.js'
5
- import { listDocs, detectCurrentRepo } from '../../services/docs.js'
6
- import { renderTable } from '../../formatters/table.js'
4
+ import {loadConfig} from '../../services/config.js'
5
+ import {listDocs, detectCurrentRepo} from '../../services/docs.js'
6
+ import {renderTable} from '../../formatters/table.js'
7
7
 
8
8
  /**
9
9
  * @param {string} type
10
10
  * @returns {string}
11
11
  */
12
12
  function typeColor(type) {
13
- if (type === 'readme') return chalk.cyan(type)
14
- if (type === 'swagger') return chalk.yellow(type)
13
+ if (type === 'readme') return chalk.cyan(type)
14
+ if (type === 'swagger') return chalk.yellow(type)
15
15
  if (type === 'asyncapi') return chalk.green(type)
16
16
  return chalk.dim(type)
17
17
  }
@@ -38,12 +38,12 @@ export default class DocsList extends Command {
38
38
  static enableJsonFlag = true
39
39
 
40
40
  static flags = {
41
- repo: Flags.string({ char: 'r', description: 'Nome del repository (default: repo nella directory corrente)' }),
42
- search: Flags.string({ char: 's', description: 'Filtra per nome o percorso (case-insensitive)' }),
41
+ repo: Flags.string({char: 'r', description: 'Nome del repository (default: repo nella directory corrente)'}),
42
+ search: Flags.string({char: 's', description: 'Filtra per nome o percorso (case-insensitive)'}),
43
43
  }
44
44
 
45
45
  async run() {
46
- const { flags } = await this.parse(DocsList)
46
+ const {flags} = await this.parse(DocsList)
47
47
  const isJson = flags.json
48
48
  const config = await loadConfig()
49
49
 
@@ -55,13 +55,15 @@ export default class DocsList extends Command {
55
55
  repo = flags.repo
56
56
  } else {
57
57
  try {
58
- ;({ owner, repo } = await detectCurrentRepo())
58
+ ;({owner, repo} = await detectCurrentRepo())
59
59
  } catch (err) {
60
60
  this.error(/** @type {Error} */ (err).message)
61
61
  }
62
62
  }
63
63
 
64
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching documentation...') }).start()
64
+ const spinner = isJson
65
+ ? null
66
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching documentation...')}).start()
65
67
  let entries
66
68
  try {
67
69
  entries = await listDocs(owner, repo)
@@ -77,34 +79,36 @@ export default class DocsList extends Command {
77
79
  ? entries.filter((e) => e.name.toLowerCase().includes(q) || e.path.toLowerCase().includes(q))
78
80
  : entries
79
81
 
80
- if (isJson) return { repo, owner, entries: filtered, total: filtered.length }
82
+ if (isJson) return {repo, owner, entries: filtered, total: filtered.length}
81
83
 
82
84
  if (entries.length === 0) {
83
85
  this.log(chalk.dim(`No documentation found in ${owner}/${repo}.`))
84
- return { repo, owner, entries: [], total: 0 }
86
+ return {repo, owner, entries: [], total: 0}
85
87
  }
86
88
 
87
89
  if (filtered.length === 0) {
88
90
  this.log(chalk.dim(`No documentation matching "${flags.search}" in ${owner}/${repo}.`))
89
- return { repo, owner, entries: [], total: 0 }
91
+ return {repo, owner, entries: [], total: 0}
90
92
  }
91
93
 
92
- const filterInfo = q ? chalk.dim(` — search: ${chalk.white(`"${flags.search}"`)}`): ''
94
+ const filterInfo = q ? chalk.dim(` — search: ${chalk.white(`"${flags.search}"`)}`) : ''
93
95
  this.log(
94
96
  chalk.bold(`\nDocumentation in ${owner}/${repo}`) +
95
- filterInfo +
96
- chalk.dim(` (${filtered.length}${filtered.length < entries.length ? `/${entries.length}` : ''})`) +
97
- '\n',
97
+ filterInfo +
98
+ chalk.dim(` (${filtered.length}${filtered.length < entries.length ? `/${entries.length}` : ''})`) +
99
+ '\n',
98
100
  )
99
101
 
100
- this.log(renderTable(filtered, [
101
- { header: 'Type', key: 'type', width: 10, colorize: typeColor },
102
- { header: 'Name', key: 'name', width: 30 },
103
- { header: 'Path', key: 'path', width: 50 },
104
- { header: 'Size', key: 'size', width: 8, format: (v) => formatSize(Number(v)) },
105
- ]))
102
+ this.log(
103
+ renderTable(filtered, [
104
+ {header: 'Type', key: 'type', width: 10, colorize: typeColor},
105
+ {header: 'Name', key: 'name', width: 30},
106
+ {header: 'Path', key: 'path', width: 50},
107
+ {header: 'Size', key: 'size', width: 8, format: (v) => formatSize(Number(v))},
108
+ ]),
109
+ )
106
110
  this.log('')
107
111
 
108
- return { repo, owner, entries: filtered, total: filtered.length }
112
+ return {repo, owner, entries: filtered, total: filtered.length}
109
113
  }
110
114
  }
@@ -1,13 +1,13 @@
1
- import { Command, Flags } from '@oclif/core'
1
+ import {Command, Flags} from '@oclif/core'
2
2
  import chalk from 'chalk'
3
3
  import ora from 'ora'
4
- import { loadConfig } from '../../services/config.js'
5
- import { listRepos } from '../../services/github.js'
6
- import { listProjectsDocs } from '../../services/docs.js'
7
- import { renderTable } from '../../formatters/table.js'
4
+ import {loadConfig} from '../../services/config.js'
5
+ import {listRepos} from '../../services/github.js'
6
+ import {listProjectsDocs} from '../../services/docs.js'
7
+ import {renderTable} from '../../formatters/table.js'
8
8
 
9
9
  export default class DocsProjects extends Command {
10
- static description = 'Mostra la documentazione disponibile per ogni repository dell\'organizzazione'
10
+ static description = "Mostra la documentazione disponibile per ogni repository dell'organizzazione"
11
11
 
12
12
  static examples = [
13
13
  '<%= config.bin %> docs projects',
@@ -18,11 +18,11 @@ export default class DocsProjects extends Command {
18
18
  static enableJsonFlag = true
19
19
 
20
20
  static flags = {
21
- search: Flags.string({ char: 's', description: 'Filtra per nome repository (case-insensitive)' }),
21
+ search: Flags.string({char: 's', description: 'Filtra per nome repository (case-insensitive)'}),
22
22
  }
23
23
 
24
24
  async run() {
25
- const { flags } = await this.parse(DocsProjects)
25
+ const {flags} = await this.parse(DocsProjects)
26
26
  const isJson = flags.json
27
27
  const config = await loadConfig()
28
28
 
@@ -31,7 +31,9 @@ export default class DocsProjects extends Command {
31
31
  }
32
32
 
33
33
  // 1. Fetch all repos
34
- const repoSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching repositories...') }).start()
34
+ const repoSpinner = isJson
35
+ ? null
36
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching repositories...')}).start()
35
37
  let repos
36
38
  try {
37
39
  repos = await listRepos(config.org)
@@ -43,7 +45,7 @@ export default class DocsProjects extends Command {
43
45
 
44
46
  if (repos.length === 0) {
45
47
  this.log(chalk.dim(`No repositories found in organization "${config.org}".`))
46
- return { org: config.org, projects: [], total: 0 }
48
+ return {org: config.org, projects: [], total: 0}
47
49
  }
48
50
 
49
51
  // 2. Filter by search
@@ -52,11 +54,17 @@ export default class DocsProjects extends Command {
52
54
 
53
55
  if (filteredRepos.length === 0) {
54
56
  this.log(chalk.dim(`No repositories matching "${flags.search}" in ${config.org}.`))
55
- return { org: config.org, projects: [], total: 0 }
57
+ return {org: config.org, projects: [], total: 0}
56
58
  }
57
59
 
58
60
  // 3. Scan each repo for docs
59
- const scanSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')(`Scanning docs in ${filteredRepos.length} repositories...`) }).start()
61
+ const scanSpinner = isJson
62
+ ? null
63
+ : ora({
64
+ spinner: 'arc',
65
+ color: false,
66
+ text: chalk.hex('#FF6B2B')(`Scanning docs in ${filteredRepos.length} repositories...`),
67
+ }).start()
60
68
 
61
69
  const repoNames = filteredRepos.map((r) => r.name)
62
70
  let projects
@@ -68,25 +76,51 @@ export default class DocsProjects extends Command {
68
76
  }
69
77
  scanSpinner?.stop()
70
78
 
71
- if (isJson) return { org: config.org, projects, total: projects.length }
79
+ if (isJson) return {org: config.org, projects, total: projects.length}
72
80
 
73
81
  const filterInfo = q ? chalk.dim(` — search: ${chalk.white(`"${flags.search}"`)}`) : ''
74
82
  this.log(
75
83
  chalk.bold(`\nDocumentation overview for ${config.org}`) +
76
- filterInfo +
77
- chalk.dim(` (${projects.length}${projects.length < repos.length ? `/${repos.length}` : ''})`) +
78
- '\n',
84
+ filterInfo +
85
+ chalk.dim(` (${projects.length}${projects.length < repos.length ? `/${repos.length}` : ''})`) +
86
+ '\n',
79
87
  )
80
88
 
81
- this.log(renderTable(projects, [
82
- { header: 'Repository', key: 'repo', width: 40 },
83
- { header: 'README', key: 'hasReadme', width: 8, format: (v) => v ? '✓' : '—', colorize: (v) => v === '✓' ? chalk.green(v) : chalk.dim(v) },
84
- { header: 'Docs', key: 'docsCount', width: 6, format: (v) => Number(v) > 0 ? String(v) : '—', colorize: (v) => v !== '—' ? chalk.cyan(v) : chalk.dim(v) },
85
- { header: 'Swagger', key: 'hasSwagger', width: 9, format: (v) => v ? '✓' : '—', colorize: (v) => v === '✓' ? chalk.yellow(v) : chalk.dim(v) },
86
- { header: 'AsyncAPI', key: 'hasAsyncApi', width: 10, format: (v) => v ? '✓' : '—', colorize: (v) => v === '✓' ? chalk.green(v) : chalk.dim(v) },
87
- ]))
89
+ this.log(
90
+ renderTable(projects, [
91
+ {header: 'Repository', key: 'repo', width: 40},
92
+ {
93
+ header: 'README',
94
+ key: 'hasReadme',
95
+ width: 8,
96
+ format: (v) => (v ? '✓' : '—'),
97
+ colorize: (v) => (v === '✓' ? chalk.green(v) : chalk.dim(v)),
98
+ },
99
+ {
100
+ header: 'Docs',
101
+ key: 'docsCount',
102
+ width: 6,
103
+ format: (v) => (Number(v) > 0 ? String(v) : '—'),
104
+ colorize: (v) => (v !== '—' ? chalk.cyan(v) : chalk.dim(v)),
105
+ },
106
+ {
107
+ header: 'Swagger',
108
+ key: 'hasSwagger',
109
+ width: 9,
110
+ format: (v) => (v ? '✓' : '—'),
111
+ colorize: (v) => (v === '✓' ? chalk.yellow(v) : chalk.dim(v)),
112
+ },
113
+ {
114
+ header: 'AsyncAPI',
115
+ key: 'hasAsyncApi',
116
+ width: 10,
117
+ format: (v) => (v ? '✓' : '—'),
118
+ colorize: (v) => (v === '✓' ? chalk.green(v) : chalk.dim(v)),
119
+ },
120
+ ]),
121
+ )
88
122
  this.log('')
89
123
 
90
- return { org: config.org, projects, total: projects.length }
124
+ return {org: config.org, projects, total: projects.length}
91
125
  }
92
126
  }
@@ -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 { loadConfig } from '../../services/config.js'
5
- import { listDocs, readFile, detectCurrentRepo, detectApiSpecType } from '../../services/docs.js'
6
- import { renderMarkdown, extractMermaidBlocks, toMermaidLiveUrl } from '../../formatters/markdown.js'
7
- import { parseOpenApi, parseAsyncApi } from '../../formatters/openapi.js'
8
- import { renderTable } from '../../formatters/table.js'
9
- import { openBrowser } from '../../utils/open-browser.js'
4
+ import {loadConfig} from '../../services/config.js'
5
+ import {listDocs, readFile, detectCurrentRepo, detectApiSpecType} from '../../services/docs.js'
6
+ import {renderMarkdown, extractMermaidBlocks, toMermaidLiveUrl} from '../../formatters/markdown.js'
7
+ import {parseOpenApi, parseAsyncApi} from '../../formatters/openapi.js'
8
+ import {renderTable} from '../../formatters/table.js'
9
+ import {openBrowser} from '../../utils/open-browser.js'
10
10
 
11
11
  /**
12
12
  * @param {string} method
@@ -52,17 +52,17 @@ export default class DocsRead extends Command {
52
52
  static enableJsonFlag = true
53
53
 
54
54
  static args = {
55
- file: Args.string({ description: 'Percorso del file da leggere (default: README)', required: false }),
55
+ file: Args.string({description: 'Percorso del file da leggere (default: README)', required: false}),
56
56
  }
57
57
 
58
58
  static flags = {
59
- repo: Flags.string({ char: 'r', description: 'Nome del repository (default: repo nella directory corrente)' }),
60
- raw: Flags.boolean({ description: 'Mostra contenuto grezzo senza parsing speciale', default: false }),
61
- render: Flags.boolean({ description: 'Apri i diagrammi Mermaid nel browser via mermaid.live', default: false }),
59
+ repo: Flags.string({char: 'r', description: 'Nome del repository (default: repo nella directory corrente)'}),
60
+ raw: Flags.boolean({description: 'Mostra contenuto grezzo senza parsing speciale', default: false}),
61
+ render: Flags.boolean({description: 'Apri i diagrammi Mermaid nel browser via mermaid.live', default: false}),
62
62
  }
63
63
 
64
64
  async run() {
65
- const { args, flags } = await this.parse(DocsRead)
65
+ const {args, flags} = await this.parse(DocsRead)
66
66
  const isJson = flags.json
67
67
  const config = await loadConfig()
68
68
 
@@ -74,7 +74,7 @@ export default class DocsRead extends Command {
74
74
  repo = flags.repo
75
75
  } else {
76
76
  try {
77
- ;({ owner, repo } = await detectCurrentRepo())
77
+ ;({owner, repo} = await detectCurrentRepo())
78
78
  } catch (err) {
79
79
  this.error(/** @type {Error} */ (err).message)
80
80
  }
@@ -83,7 +83,9 @@ export default class DocsRead extends Command {
83
83
  // Resolve file path
84
84
  let filePath = args.file
85
85
  if (!filePath) {
86
- const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Looking for README...') }).start()
86
+ const spinner = isJson
87
+ ? null
88
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Looking for README...')}).start()
87
89
  let entries
88
90
  try {
89
91
  entries = await listDocs(owner, repo)
@@ -95,24 +97,35 @@ export default class DocsRead extends Command {
95
97
  const readme = entries.find((e) => e.type === 'readme')
96
98
  if (!readme) {
97
99
  this.log(chalk.dim(`No README found in ${owner}/${repo}.`))
98
- return { repo, owner, path: null, type: null, content: null, size: 0 }
100
+ return {repo, owner, path: null, type: null, content: null, size: 0}
99
101
  }
100
102
  filePath = readme.path
101
103
  }
102
104
 
103
105
  // Fetch content
104
- const spinner2 = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')(`Reading ${filePath}...`) }).start()
105
- let content
106
- try {
107
- content = await readFile(owner, repo, filePath)
108
- } catch {
109
- spinner2?.stop()
110
- this.error(`File "${filePath}" not found in ${owner}/${repo}. Run \`dvmi docs list\` to see available documentation.`)
111
- }
106
+ const spinner2 = isJson
107
+ ? null
108
+ : ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')(`Reading ${filePath}...`)}).start()
109
+ let content
110
+ try {
111
+ content = await readFile(owner, repo, filePath)
112
+ } catch {
113
+ spinner2?.stop()
114
+ this.error(
115
+ `File "${filePath}" not found in ${owner}/${repo}. Run \`dvmi docs list\` to see available documentation.`,
116
+ )
117
+ }
112
118
  spinner2?.stop()
113
119
 
114
120
  if (isJson) {
115
- return { repo, owner, path: filePath, type: detectApiSpecType(filePath, content) ?? 'doc', content, size: content.length }
121
+ return {
122
+ repo,
123
+ owner,
124
+ path: filePath,
125
+ type: detectApiSpecType(filePath, content) ?? 'doc',
126
+ content,
127
+ size: content.length,
128
+ }
116
129
  }
117
130
 
118
131
  // Handle --render (Mermaid)
@@ -133,33 +146,37 @@ export default class DocsRead extends Command {
133
146
 
134
147
  // Render
135
148
  if (!flags.raw && specType === 'swagger') {
136
- const { endpoints, error } = parseOpenApi(content)
149
+ const {endpoints, error} = parseOpenApi(content)
137
150
  if (error || endpoints.length === 0) {
138
151
  this.log(chalk.yellow(`⚠ Could not parse "${filePath}" as OpenAPI spec (showing raw content). ${error ?? ''}`))
139
152
  this.log(content)
140
153
  } else {
141
154
  this.log(chalk.bold(`\nAPI Endpoints — ${filePath}\n`))
142
- this.log(renderTable(endpoints, [
143
- { header: 'Method', key: 'method', width: 8, colorize: methodColor },
144
- { header: 'Path', key: 'path', width: 45 },
145
- { header: 'Summary', key: 'summary', width: 40 },
146
- { header: 'Parameters', key: 'parameters', width: 30, format: (v) => v || '—' },
147
- ]))
155
+ this.log(
156
+ renderTable(endpoints, [
157
+ {header: 'Method', key: 'method', width: 8, colorize: methodColor},
158
+ {header: 'Path', key: 'path', width: 45},
159
+ {header: 'Summary', key: 'summary', width: 40},
160
+ {header: 'Parameters', key: 'parameters', width: 30, format: (v) => v || '—'},
161
+ ]),
162
+ )
148
163
  this.log('')
149
164
  }
150
165
  } else if (!flags.raw && specType === 'asyncapi') {
151
- const { channels, error } = parseAsyncApi(content)
166
+ const {channels, error} = parseAsyncApi(content)
152
167
  if (error || channels.length === 0) {
153
168
  this.log(chalk.yellow(`⚠ Could not parse "${filePath}" as AsyncAPI spec (showing raw content). ${error ?? ''}`))
154
169
  this.log(content)
155
170
  } else {
156
171
  this.log(chalk.bold(`\nAsyncAPI Channels — ${filePath}\n`))
157
- this.log(renderTable(channels, [
158
- { header: 'Channel', key: 'channel', width: 35 },
159
- { header: 'Operation', key: 'operation', width: 12, colorize: opColor },
160
- { header: 'Summary', key: 'summary', width: 40 },
161
- { header: 'Message', key: 'message', width: 25, format: (v) => v || '—' },
162
- ]))
172
+ this.log(
173
+ renderTable(channels, [
174
+ {header: 'Channel', key: 'channel', width: 35},
175
+ {header: 'Operation', key: 'operation', width: 12, colorize: opColor},
176
+ {header: 'Summary', key: 'summary', width: 40},
177
+ {header: 'Message', key: 'message', width: 25, format: (v) => v || '—'},
178
+ ]),
179
+ )
163
180
  this.log('')
164
181
  }
165
182
  } else {
@@ -167,6 +184,6 @@ export default class DocsRead extends Command {
167
184
  this.log(renderMarkdown(content))
168
185
  }
169
186
 
170
- return { repo, owner, path: filePath, type: specType ?? 'doc', content, size: content.length }
187
+ return {repo, owner, path: filePath, type: specType ?? 'doc', content, size: content.length}
171
188
  }
172
189
  }