devvami 1.0.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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +255 -0
  3. package/bin/dev.cmd +3 -0
  4. package/bin/dev.js +5 -0
  5. package/bin/run.cmd +3 -0
  6. package/bin/run.js +5 -0
  7. package/oclif.manifest.json +1238 -0
  8. package/package.json +161 -0
  9. package/src/commands/auth/login.js +89 -0
  10. package/src/commands/changelog.js +102 -0
  11. package/src/commands/costs/get.js +73 -0
  12. package/src/commands/create/repo.js +196 -0
  13. package/src/commands/docs/list.js +110 -0
  14. package/src/commands/docs/projects.js +92 -0
  15. package/src/commands/docs/read.js +172 -0
  16. package/src/commands/docs/search.js +103 -0
  17. package/src/commands/doctor.js +115 -0
  18. package/src/commands/init.js +222 -0
  19. package/src/commands/open.js +75 -0
  20. package/src/commands/pipeline/logs.js +41 -0
  21. package/src/commands/pipeline/rerun.js +66 -0
  22. package/src/commands/pipeline/status.js +62 -0
  23. package/src/commands/pr/create.js +114 -0
  24. package/src/commands/pr/detail.js +83 -0
  25. package/src/commands/pr/review.js +51 -0
  26. package/src/commands/pr/status.js +70 -0
  27. package/src/commands/repo/list.js +113 -0
  28. package/src/commands/search.js +62 -0
  29. package/src/commands/tasks/assigned.js +131 -0
  30. package/src/commands/tasks/list.js +133 -0
  31. package/src/commands/tasks/today.js +73 -0
  32. package/src/commands/upgrade.js +52 -0
  33. package/src/commands/whoami.js +85 -0
  34. package/src/formatters/cost.js +54 -0
  35. package/src/formatters/markdown.js +108 -0
  36. package/src/formatters/openapi.js +146 -0
  37. package/src/formatters/status.js +48 -0
  38. package/src/formatters/table.js +87 -0
  39. package/src/help.js +312 -0
  40. package/src/hooks/init.js +9 -0
  41. package/src/hooks/postrun.js +18 -0
  42. package/src/index.js +1 -0
  43. package/src/services/auth.js +83 -0
  44. package/src/services/aws-costs.js +80 -0
  45. package/src/services/clickup.js +288 -0
  46. package/src/services/config.js +59 -0
  47. package/src/services/docs.js +210 -0
  48. package/src/services/github.js +377 -0
  49. package/src/services/platform.js +48 -0
  50. package/src/services/shell.js +42 -0
  51. package/src/services/version-check.js +58 -0
  52. package/src/types.js +228 -0
  53. package/src/utils/banner.js +48 -0
  54. package/src/utils/errors.js +61 -0
  55. package/src/utils/gradient.js +130 -0
  56. package/src/utils/open-browser.js +29 -0
  57. package/src/utils/typewriter.js +48 -0
  58. package/src/validators/repo-name.js +42 -0
@@ -0,0 +1,110 @@
1
+ import { Command, Flags } from '@oclif/core'
2
+ import chalk from 'chalk'
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'
7
+
8
+ /**
9
+ * @param {string} type
10
+ * @returns {string}
11
+ */
12
+ function typeColor(type) {
13
+ if (type === 'readme') return chalk.cyan(type)
14
+ if (type === 'swagger') return chalk.yellow(type)
15
+ if (type === 'asyncapi') return chalk.green(type)
16
+ return chalk.dim(type)
17
+ }
18
+
19
+ /**
20
+ * @param {number} bytes
21
+ * @returns {string}
22
+ */
23
+ function formatSize(bytes) {
24
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)}KB`
25
+ return `${bytes}B`
26
+ }
27
+
28
+ export default class DocsList extends Command {
29
+ static description = 'Lista i file di documentazione del repository'
30
+
31
+ static examples = [
32
+ '<%= config.bin %> docs list',
33
+ '<%= config.bin %> docs list --repo my-service',
34
+ '<%= config.bin %> docs list --search "arch"',
35
+ '<%= config.bin %> docs list --json',
36
+ ]
37
+
38
+ static enableJsonFlag = true
39
+
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)' }),
43
+ }
44
+
45
+ async run() {
46
+ const { flags } = await this.parse(DocsList)
47
+ const isJson = flags.json
48
+ const config = await loadConfig()
49
+
50
+ // Resolve owner/repo
51
+ let owner, repo
52
+ if (flags.repo) {
53
+ owner = config.org
54
+ if (!owner) this.error('GitHub org not configured. Run `dvmi init` to set up your environment.')
55
+ repo = flags.repo
56
+ } else {
57
+ try {
58
+ ;({ owner, repo } = await detectCurrentRepo())
59
+ } catch (err) {
60
+ this.error(/** @type {Error} */ (err).message)
61
+ }
62
+ }
63
+
64
+ const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching documentation...') }).start()
65
+ let entries
66
+ try {
67
+ entries = await listDocs(owner, repo)
68
+ } catch (err) {
69
+ spinner?.stop()
70
+ this.error(/** @type {Error} */ (err).message)
71
+ }
72
+ spinner?.stop()
73
+
74
+ // Search filter
75
+ const q = flags.search?.toLowerCase()
76
+ const filtered = q
77
+ ? entries.filter((e) => e.name.toLowerCase().includes(q) || e.path.toLowerCase().includes(q))
78
+ : entries
79
+
80
+ if (isJson) return { repo, owner, entries: filtered, total: filtered.length }
81
+
82
+ if (entries.length === 0) {
83
+ this.log(chalk.dim(`No documentation found in ${owner}/${repo}.`))
84
+ return { repo, owner, entries: [], total: 0 }
85
+ }
86
+
87
+ if (filtered.length === 0) {
88
+ this.log(chalk.dim(`No documentation matching "${flags.search}" in ${owner}/${repo}.`))
89
+ return { repo, owner, entries: [], total: 0 }
90
+ }
91
+
92
+ const filterInfo = q ? chalk.dim(` — search: ${chalk.white(`"${flags.search}"`)}`): ''
93
+ this.log(
94
+ chalk.bold(`\nDocumentation in ${owner}/${repo}`) +
95
+ filterInfo +
96
+ chalk.dim(` (${filtered.length}${filtered.length < entries.length ? `/${entries.length}` : ''})`) +
97
+ '\n',
98
+ )
99
+
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
+ ]))
106
+ this.log('')
107
+
108
+ return { repo, owner, entries: filtered, total: filtered.length }
109
+ }
110
+ }
@@ -0,0 +1,92 @@
1
+ import { Command, Flags } from '@oclif/core'
2
+ import chalk from 'chalk'
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'
8
+
9
+ export default class DocsProjects extends Command {
10
+ static description = 'Mostra la documentazione disponibile per ogni repository dell\'organizzazione'
11
+
12
+ static examples = [
13
+ '<%= config.bin %> docs projects',
14
+ '<%= config.bin %> docs projects --search "service"',
15
+ '<%= config.bin %> docs projects --json',
16
+ ]
17
+
18
+ static enableJsonFlag = true
19
+
20
+ static flags = {
21
+ search: Flags.string({ char: 's', description: 'Filtra per nome repository (case-insensitive)' }),
22
+ }
23
+
24
+ async run() {
25
+ const { flags } = await this.parse(DocsProjects)
26
+ const isJson = flags.json
27
+ const config = await loadConfig()
28
+
29
+ if (!config.org) {
30
+ this.error('GitHub org not configured. Run `dvmi init` to set up your environment.')
31
+ }
32
+
33
+ // 1. Fetch all repos
34
+ const repoSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching repositories...') }).start()
35
+ let repos
36
+ try {
37
+ repos = await listRepos(config.org)
38
+ } catch (err) {
39
+ repoSpinner?.stop()
40
+ this.error(/** @type {Error} */ (err).message)
41
+ }
42
+ repoSpinner?.stop()
43
+
44
+ if (repos.length === 0) {
45
+ this.log(chalk.dim(`No repositories found in organization "${config.org}".`))
46
+ return { org: config.org, projects: [], total: 0 }
47
+ }
48
+
49
+ // 2. Filter by search
50
+ const q = flags.search?.toLowerCase()
51
+ const filteredRepos = q ? repos.filter((r) => r.name.toLowerCase().includes(q)) : repos
52
+
53
+ if (filteredRepos.length === 0) {
54
+ this.log(chalk.dim(`No repositories matching "${flags.search}" in ${config.org}.`))
55
+ return { org: config.org, projects: [], total: 0 }
56
+ }
57
+
58
+ // 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()
60
+
61
+ const repoNames = filteredRepos.map((r) => r.name)
62
+ let projects
63
+ try {
64
+ projects = await listProjectsDocs(config.org, repoNames)
65
+ } catch (err) {
66
+ scanSpinner?.stop()
67
+ this.error(/** @type {Error} */ (err).message)
68
+ }
69
+ scanSpinner?.stop()
70
+
71
+ if (isJson) return { org: config.org, projects, total: projects.length }
72
+
73
+ const filterInfo = q ? chalk.dim(` — search: ${chalk.white(`"${flags.search}"`)}`) : ''
74
+ this.log(
75
+ chalk.bold(`\nDocumentation overview for ${config.org}`) +
76
+ filterInfo +
77
+ chalk.dim(` (${projects.length}${projects.length < repos.length ? `/${repos.length}` : ''})`) +
78
+ '\n',
79
+ )
80
+
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
+ ]))
88
+ this.log('')
89
+
90
+ return { org: config.org, projects, total: projects.length }
91
+ }
92
+ }
@@ -0,0 +1,172 @@
1
+ import { Command, Args, Flags } from '@oclif/core'
2
+ import chalk from 'chalk'
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'
10
+
11
+ /**
12
+ * @param {string} method
13
+ * @returns {string}
14
+ */
15
+ function methodColor(method) {
16
+ const colors = {
17
+ GET: chalk.cyan,
18
+ POST: chalk.green,
19
+ PUT: chalk.yellow,
20
+ PATCH: chalk.magenta,
21
+ DELETE: chalk.red,
22
+ HEAD: chalk.dim,
23
+ OPTIONS: chalk.dim,
24
+ }
25
+ const fn = colors[method]
26
+ return fn ? fn(method) : method
27
+ }
28
+
29
+ /**
30
+ * @param {string} op
31
+ * @returns {string}
32
+ */
33
+ function opColor(op) {
34
+ if (op === 'publish' || op === 'send') return chalk.green(op)
35
+ if (op === 'subscribe' || op === 'receive') return chalk.cyan(op)
36
+ return chalk.dim(op)
37
+ }
38
+
39
+ export default class DocsRead extends Command {
40
+ static description = 'Leggi un file di documentazione del repository nel terminale'
41
+
42
+ static examples = [
43
+ '<%= config.bin %> docs read',
44
+ '<%= config.bin %> docs read --repo my-service',
45
+ '<%= config.bin %> docs read docs/architecture.md',
46
+ '<%= config.bin %> docs read openapi.yaml',
47
+ '<%= config.bin %> docs read openapi.yaml --raw',
48
+ '<%= config.bin %> docs read --render',
49
+ '<%= config.bin %> docs read --json',
50
+ ]
51
+
52
+ static enableJsonFlag = true
53
+
54
+ static args = {
55
+ file: Args.string({ description: 'Percorso del file da leggere (default: README)', required: false }),
56
+ }
57
+
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 }),
62
+ }
63
+
64
+ async run() {
65
+ const { args, flags } = await this.parse(DocsRead)
66
+ const isJson = flags.json
67
+ const config = await loadConfig()
68
+
69
+ // Resolve owner/repo
70
+ let owner, repo
71
+ if (flags.repo) {
72
+ owner = config.org
73
+ if (!owner) this.error('GitHub org not configured. Run `dvmi init` to set up your environment.')
74
+ repo = flags.repo
75
+ } else {
76
+ try {
77
+ ;({ owner, repo } = await detectCurrentRepo())
78
+ } catch (err) {
79
+ this.error(/** @type {Error} */ (err).message)
80
+ }
81
+ }
82
+
83
+ // Resolve file path
84
+ let filePath = args.file
85
+ if (!filePath) {
86
+ const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Looking for README...') }).start()
87
+ let entries
88
+ try {
89
+ entries = await listDocs(owner, repo)
90
+ } catch (err) {
91
+ spinner?.stop()
92
+ this.error(/** @type {Error} */ (err).message)
93
+ }
94
+ spinner?.stop()
95
+ const readme = entries.find((e) => e.type === 'readme')
96
+ if (!readme) {
97
+ this.log(chalk.dim(`No README found in ${owner}/${repo}.`))
98
+ return { repo, owner, path: null, type: null, content: null, size: 0 }
99
+ }
100
+ filePath = readme.path
101
+ }
102
+
103
+ // 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
+ }
112
+ spinner2?.stop()
113
+
114
+ if (isJson) {
115
+ return { repo, owner, path: filePath, type: detectApiSpecType(filePath, content) ?? 'doc', content, size: content.length }
116
+ }
117
+
118
+ // Handle --render (Mermaid)
119
+ if (flags.render) {
120
+ const blocks = extractMermaidBlocks(content)
121
+ if (blocks.length === 0) {
122
+ this.log(chalk.dim('No Mermaid diagrams found in this document.'))
123
+ } else {
124
+ for (const block of blocks) {
125
+ const url = toMermaidLiveUrl(block)
126
+ await openBrowser(url)
127
+ this.log(chalk.green('✓') + ` Opened Mermaid diagram in browser: ${url}`)
128
+ }
129
+ }
130
+ }
131
+
132
+ const specType = detectApiSpecType(filePath, content)
133
+
134
+ // Render
135
+ if (!flags.raw && specType === 'swagger') {
136
+ const { endpoints, error } = parseOpenApi(content)
137
+ if (error || endpoints.length === 0) {
138
+ this.log(chalk.yellow(`⚠ Could not parse "${filePath}" as OpenAPI spec (showing raw content). ${error ?? ''}`))
139
+ this.log(content)
140
+ } else {
141
+ 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
+ ]))
148
+ this.log('')
149
+ }
150
+ } else if (!flags.raw && specType === 'asyncapi') {
151
+ const { channels, error } = parseAsyncApi(content)
152
+ if (error || channels.length === 0) {
153
+ this.log(chalk.yellow(`⚠ Could not parse "${filePath}" as AsyncAPI spec (showing raw content). ${error ?? ''}`))
154
+ this.log(content)
155
+ } else {
156
+ 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
+ ]))
163
+ this.log('')
164
+ }
165
+ } else {
166
+ // Markdown or raw
167
+ this.log(renderMarkdown(content))
168
+ }
169
+
170
+ return { repo, owner, path: filePath, type: specType ?? 'doc', content, size: content.length }
171
+ }
172
+ }
@@ -0,0 +1,103 @@
1
+ import { Command, Args, Flags } from '@oclif/core'
2
+ import chalk from 'chalk'
3
+ import ora from 'ora'
4
+ import { loadConfig } from '../../services/config.js'
5
+ import { searchDocs, detectCurrentRepo } from '../../services/docs.js'
6
+ import { renderTable } from '../../formatters/table.js'
7
+
8
+ const MAX_MATCHES_PER_FILE = 3
9
+
10
+ export default class DocsSearch extends Command {
11
+ static description = 'Cerca testo nella documentazione del repository'
12
+
13
+ static examples = [
14
+ '<%= config.bin %> docs search "authentication"',
15
+ '<%= config.bin %> docs search "deploy" --repo my-service',
16
+ '<%= config.bin %> docs search "endpoint" --json',
17
+ ]
18
+
19
+ static enableJsonFlag = true
20
+
21
+ static args = {
22
+ term: Args.string({ description: 'Termine di ricerca (case-insensitive)', required: true }),
23
+ }
24
+
25
+ static flags = {
26
+ repo: Flags.string({ char: 'r', description: 'Nome del repository (default: repo nella directory corrente)' }),
27
+ }
28
+
29
+ async run() {
30
+ const { args, flags } = await this.parse(DocsSearch)
31
+ const isJson = flags.json
32
+ const config = await loadConfig()
33
+
34
+ // Resolve owner/repo
35
+ let owner, repo
36
+ if (flags.repo) {
37
+ owner = config.org
38
+ if (!owner) this.error('GitHub org not configured. Run `dvmi init` to set up your environment.')
39
+ repo = flags.repo
40
+ } else {
41
+ try {
42
+ ;({ owner, repo } = await detectCurrentRepo())
43
+ } catch (err) {
44
+ this.error(/** @type {Error} */ (err).message)
45
+ }
46
+ }
47
+
48
+ const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')(`Searching "${args.term}" in docs...`) }).start()
49
+ let matches
50
+ try {
51
+ matches = await searchDocs(owner, repo, args.term)
52
+ } catch (err) {
53
+ spinner?.stop()
54
+ this.error(/** @type {Error} */ (err).message)
55
+ }
56
+ spinner?.stop()
57
+
58
+ if (isJson) return { repo, owner, term: args.term, matches, total: matches.length }
59
+
60
+ if (matches.length === 0) {
61
+ this.log(chalk.dim(`No matches found for "${args.term}" in ${owner}/${repo} documentation.`))
62
+ return { repo, owner, term: args.term, matches: [], total: 0 }
63
+ }
64
+
65
+ this.log(chalk.bold(`\nSearch results for "${args.term}" in ${owner}/${repo}`) + chalk.dim(` (${matches.length} match${matches.length === 1 ? '' : 'es'})\n`))
66
+
67
+ // Group by file and limit rows
68
+ /** @type {Map<string, import('../../types.js').SearchMatch[]>} */
69
+ const byFile = new Map()
70
+ for (const m of matches) {
71
+ const list = byFile.get(m.file) ?? []
72
+ list.push(m)
73
+ byFile.set(m.file, list)
74
+ }
75
+
76
+ /** @type {Array<import('../../types.js').SearchMatch & { _extra?: string }>} */
77
+ const rows = []
78
+ for (const [, fileMatches] of byFile) {
79
+ const shown = fileMatches.slice(0, MAX_MATCHES_PER_FILE)
80
+ rows.push(...shown)
81
+ const extra = fileMatches.length - shown.length
82
+ if (extra > 0) {
83
+ rows.push({ file: '', line: 0, context: chalk.dim(`(+${extra} more in this file)`), occurrences: 0 })
84
+ }
85
+ }
86
+
87
+ const q = args.term.toLowerCase()
88
+ this.log(renderTable(rows, [
89
+ { header: 'File', key: 'file', width: 35 },
90
+ { header: 'Line', key: 'line', width: 5, format: (v) => Number(v) === 0 ? '' : String(v) },
91
+ { header: 'Context', key: 'context', width: 65, format: (v) => {
92
+ const s = String(v)
93
+ // highlight term
94
+ const re = new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')
95
+ return s.replace(re, (m) => chalk.yellow.bold(m))
96
+ }},
97
+ { header: 'Matches', key: 'occurrences', width: 8, format: (v) => Number(v) === 0 ? '' : `${v}` },
98
+ ]))
99
+ this.log('')
100
+
101
+ return { repo, owner, term: args.term, matches, total: matches.length }
102
+ }
103
+ }
@@ -0,0 +1,115 @@
1
+ import { Command, Flags } from '@oclif/core'
2
+ import chalk from 'chalk'
3
+ import ora from 'ora'
4
+ import { typewriterLine } from '../utils/typewriter.js'
5
+ import { which, exec } from '../services/shell.js'
6
+ import { checkGitHubAuth, checkAWSAuth } from '../services/auth.js'
7
+ import { formatDoctorCheck, formatDoctorSummary } from '../formatters/status.js'
8
+
9
+ /** @import { DoctorCheck } from '../types.js' */
10
+
11
+ export default class Doctor extends Command {
12
+ static description = 'Diagnostica ambiente di sviluppo'
13
+
14
+ static examples = [
15
+ '<%= config.bin %> doctor',
16
+ '<%= config.bin %> doctor --json',
17
+ '<%= config.bin %> doctor --verbose',
18
+ ]
19
+
20
+ static enableJsonFlag = true
21
+
22
+ static flags = {
23
+ verbose: Flags.boolean({ description: 'Mostra dettagli aggiuntivi', default: false }),
24
+ }
25
+
26
+ async run() {
27
+ const { flags } = await this.parse(Doctor)
28
+ const isJson = flags.json
29
+
30
+ const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Running diagnostics...') }).start()
31
+
32
+ /** @type {DoctorCheck[]} */
33
+ const checks = []
34
+
35
+ // Software checks
36
+ const softwareChecks = [
37
+ { name: 'Node.js', cmd: 'node', args: ['--version'], required: '>=24' },
38
+ { name: 'nvm', cmd: 'nvm', args: ['--version'], required: null },
39
+ { name: 'npm', cmd: 'npm', args: ['--version'], required: null },
40
+ { name: 'Git', cmd: 'git', args: ['--version'], required: null },
41
+ { name: 'gh CLI', cmd: 'gh', args: ['--version'], required: null },
42
+ { name: 'Docker', cmd: 'docker', args: ['--version'], required: null },
43
+ { name: 'AWS CLI', cmd: 'aws', args: ['--version'], required: null },
44
+ { name: 'aws-vault', cmd: 'aws-vault', args: ['--version'], required: null },
45
+ ]
46
+
47
+ for (const check of softwareChecks) {
48
+ const path = await which(check.cmd)
49
+ if (!path) {
50
+ checks.push({
51
+ name: check.name,
52
+ status: check.required ? 'fail' : 'warn',
53
+ version: null,
54
+ required: check.required,
55
+ hint: `Install ${check.name}`,
56
+ })
57
+ continue
58
+ }
59
+ const result = await exec(check.cmd, check.args)
60
+ const version = result.stdout.replace(/\n.*/s, '').trim()
61
+ checks.push({
62
+ name: check.name,
63
+ status: 'ok',
64
+ version,
65
+ required: check.required,
66
+ hint: null,
67
+ })
68
+ }
69
+
70
+ // Auth checks
71
+ const ghAuth = await checkGitHubAuth()
72
+ checks.push({
73
+ name: 'GitHub auth',
74
+ status: ghAuth.authenticated ? 'ok' : 'fail',
75
+ version: ghAuth.authenticated ? ghAuth.username ?? null : null,
76
+ required: null,
77
+ hint: ghAuth.authenticated ? null : 'Run `dvmi auth login`',
78
+ })
79
+
80
+ const awsAuth = await checkAWSAuth()
81
+ checks.push({
82
+ name: 'AWS auth',
83
+ status: awsAuth.authenticated ? 'ok' : 'warn',
84
+ version: awsAuth.authenticated ? awsAuth.account ?? null : null,
85
+ required: null,
86
+ hint: awsAuth.authenticated ? null : 'Run `dvmi auth login --aws`',
87
+ })
88
+
89
+ spinner?.stop()
90
+
91
+ const summary = {
92
+ ok: checks.filter((c) => c.status === 'ok').length,
93
+ warn: checks.filter((c) => c.status === 'warn').length,
94
+ fail: checks.filter((c) => c.status === 'fail').length,
95
+ }
96
+
97
+ if (isJson) return { checks, summary }
98
+
99
+ await typewriterLine('Environment Diagnostics')
100
+ for (const check of checks) {
101
+ this.log(' ' + formatDoctorCheck(check))
102
+ }
103
+ this.log('\n' + formatDoctorSummary(summary))
104
+
105
+ const issues = checks.filter((c) => c.status !== 'ok')
106
+ if (issues.length > 0) {
107
+ this.log('\n' + chalk.yellow('Issues found:'))
108
+ for (const issue of issues) {
109
+ if (issue.hint) this.log(` → ${issue.hint}`)
110
+ }
111
+ }
112
+
113
+ return { checks, summary }
114
+ }
115
+ }