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.
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/oclif.manifest.json +1238 -0
- package/package.json +161 -0
- package/src/commands/auth/login.js +89 -0
- package/src/commands/changelog.js +102 -0
- package/src/commands/costs/get.js +73 -0
- package/src/commands/create/repo.js +196 -0
- package/src/commands/docs/list.js +110 -0
- package/src/commands/docs/projects.js +92 -0
- package/src/commands/docs/read.js +172 -0
- package/src/commands/docs/search.js +103 -0
- package/src/commands/doctor.js +115 -0
- package/src/commands/init.js +222 -0
- package/src/commands/open.js +75 -0
- package/src/commands/pipeline/logs.js +41 -0
- package/src/commands/pipeline/rerun.js +66 -0
- package/src/commands/pipeline/status.js +62 -0
- package/src/commands/pr/create.js +114 -0
- package/src/commands/pr/detail.js +83 -0
- package/src/commands/pr/review.js +51 -0
- package/src/commands/pr/status.js +70 -0
- package/src/commands/repo/list.js +113 -0
- package/src/commands/search.js +62 -0
- package/src/commands/tasks/assigned.js +131 -0
- package/src/commands/tasks/list.js +133 -0
- package/src/commands/tasks/today.js +73 -0
- package/src/commands/upgrade.js +52 -0
- package/src/commands/whoami.js +85 -0
- package/src/formatters/cost.js +54 -0
- package/src/formatters/markdown.js +108 -0
- package/src/formatters/openapi.js +146 -0
- package/src/formatters/status.js +48 -0
- package/src/formatters/table.js +87 -0
- package/src/help.js +312 -0
- package/src/hooks/init.js +9 -0
- package/src/hooks/postrun.js +18 -0
- package/src/index.js +1 -0
- package/src/services/auth.js +83 -0
- package/src/services/aws-costs.js +80 -0
- package/src/services/clickup.js +288 -0
- package/src/services/config.js +59 -0
- package/src/services/docs.js +210 -0
- package/src/services/github.js +377 -0
- package/src/services/platform.js +48 -0
- package/src/services/shell.js +42 -0
- package/src/services/version-check.js +58 -0
- package/src/types.js +228 -0
- package/src/utils/banner.js +48 -0
- package/src/utils/errors.js +61 -0
- package/src/utils/gradient.js +130 -0
- package/src/utils/open-browser.js +29 -0
- package/src/utils/typewriter.js +48 -0
- 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
|
+
}
|