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,85 @@
1
+ import { Command } from '@oclif/core'
2
+ import chalk from 'chalk'
3
+ import ora from 'ora'
4
+ import { createOctokit } from '../services/github.js'
5
+ import { checkAWSAuth } from '../services/auth.js'
6
+ import { getCurrentVersion } from '../services/version-check.js'
7
+ import { CONFIG_PATH, loadConfig } from '../services/config.js'
8
+ import { getUser, isAuthenticated } from '../services/clickup.js'
9
+
10
+ export default class Whoami extends Command {
11
+ static description = 'Mostra la tua identita su GitHub, AWS e ClickUp'
12
+
13
+ static examples = [
14
+ '<%= config.bin %> whoami',
15
+ '<%= config.bin %> whoami --json',
16
+ ]
17
+
18
+ static enableJsonFlag = true
19
+
20
+ async run() {
21
+ const { flags } = await this.parse(Whoami)
22
+ const isJson = flags.json
23
+
24
+ const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching identity...') }).start()
25
+
26
+ const [ghResult, awsResult, version, cuResult] = await Promise.allSettled([
27
+ (async () => {
28
+ const octokit = await createOctokit()
29
+ const { data: user } = await octokit.rest.users.getAuthenticated()
30
+ return { username: user.login, name: user.name ?? '', org: '', teams: [] }
31
+ })(),
32
+ checkAWSAuth(),
33
+ getCurrentVersion(),
34
+ (async () => {
35
+ if (!(await isAuthenticated())) return null
36
+ const [user, config] = await Promise.all([getUser(), loadConfig()])
37
+ return { username: user.username, teamName: config.clickup?.teamName ?? null }
38
+ })(),
39
+ ])
40
+
41
+ spinner?.stop()
42
+
43
+ const github =
44
+ ghResult.status === 'fulfilled'
45
+ ? ghResult.value
46
+ : { username: null, error: '[NOT AUTHENTICATED]' }
47
+
48
+ const aws =
49
+ awsResult.status === 'fulfilled' && awsResult.value.authenticated
50
+ ? { accountId: awsResult.value.account, role: awsResult.value.role }
51
+ : { accountId: null, error: '[NOT AUTHENTICATED]' }
52
+
53
+ const clickup =
54
+ cuResult.status === 'fulfilled' && cuResult.value
55
+ ? cuResult.value
56
+ : { username: null, teamName: null, error: '[NOT AUTHENTICATED]' }
57
+
58
+ const cli = {
59
+ version: version.status === 'fulfilled' ? version.value : '?',
60
+ configPath: CONFIG_PATH,
61
+ }
62
+
63
+ const result = { github, aws, clickup, cli }
64
+
65
+ if (isJson) return result
66
+
67
+ this.log(chalk.bold('\nGitHub'))
68
+ this.log(` User: ${github.username ? chalk.cyan('@' + github.username) : chalk.red('[NOT AUTHENTICATED]')}`)
69
+ if (github.name) this.log(` Name: ${github.name}`)
70
+
71
+ this.log(chalk.bold('\nAWS'))
72
+ this.log(` Account: ${aws.accountId ?? chalk.red('[NOT AUTHENTICATED]')}`)
73
+ if (aws.role) this.log(` Role: ${aws.role}`)
74
+
75
+ this.log(chalk.bold('\nClickUp'))
76
+ this.log(` User: ${clickup.username ? chalk.cyan(clickup.username) : chalk.red('[NOT AUTHENTICATED]')}`)
77
+ if (clickup.teamName) this.log(` Team: ${clickup.teamName}`)
78
+
79
+ this.log(chalk.bold('\nCLI'))
80
+ this.log(` Version: ${cli.version}`)
81
+ this.log(` Config: ${cli.configPath}`)
82
+
83
+ return result
84
+ }
85
+ }
@@ -0,0 +1,54 @@
1
+ /** @import { AWSCostEntry } from '../types.js' */
2
+
3
+ /**
4
+ * Format a USD amount as currency string.
5
+ * @param {number} amount
6
+ * @returns {string}
7
+ */
8
+ export function formatCurrency(amount) {
9
+ return `$${amount.toFixed(2)}`
10
+ }
11
+
12
+ /**
13
+ * Calculate total cost from entries.
14
+ * @param {AWSCostEntry[]} entries
15
+ * @returns {number}
16
+ */
17
+ export function calculateTotal(entries) {
18
+ return entries.reduce((sum, e) => sum + e.amount, 0)
19
+ }
20
+
21
+ /**
22
+ * Format a trend percentage.
23
+ * @param {number} current
24
+ * @param {number} previous
25
+ * @returns {string}
26
+ */
27
+ export function formatTrend(current, previous) {
28
+ if (previous === 0) return 'N/A'
29
+ const pct = ((current - previous) / previous) * 100
30
+ const sign = pct >= 0 ? '+' : ''
31
+ return `${sign}${pct.toFixed(1)}%`
32
+ }
33
+
34
+ /**
35
+ * Format cost entries as a printable table string.
36
+ * @param {AWSCostEntry[]} entries
37
+ * @param {string} serviceName
38
+ * @returns {string}
39
+ */
40
+ export function formatCostTable(entries, serviceName) {
41
+ const total = calculateTotal(entries)
42
+ const rows = entries
43
+ .sort((a, b) => b.amount - a.amount)
44
+ .map((e) => ` ${e.serviceName.padEnd(40)} ${formatCurrency(e.amount)}`)
45
+ .join('\n')
46
+ const divider = '─'.repeat(50)
47
+ return [
48
+ `Costs for: ${serviceName}`,
49
+ divider,
50
+ rows,
51
+ divider,
52
+ ` ${'Total'.padEnd(40)} ${formatCurrency(total)}`,
53
+ ].join('\n')
54
+ }
@@ -0,0 +1,108 @@
1
+ import { marked } from 'marked'
2
+ import chalk from 'chalk'
3
+ import { deflate } from 'pako'
4
+
5
+ // Custom terminal renderer — outputs ANSI-formatted text using chalk.
6
+ // marked-terminal@7 is incompatible with all currently released versions of marked
7
+ // due to an internal API break (this.o.text undefined).
8
+ // This inline renderer has no external dependencies beyond chalk (already in deps).
9
+ const terminalRenderer = {
10
+ heading(text, level) {
11
+ const stripped = text.replace(/<[^>]*>/g, '')
12
+ if (level === 1) return '\n' + chalk.bold.white(stripped) + '\n\n'
13
+ if (level === 2) return '\n' + chalk.bold(stripped) + '\n\n'
14
+ return '\n' + chalk.bold.dim(stripped) + '\n\n'
15
+ },
16
+ paragraph(text) {
17
+ return text + '\n\n'
18
+ },
19
+ strong(text) {
20
+ return chalk.bold(text)
21
+ },
22
+ em(text) {
23
+ return chalk.italic(text)
24
+ },
25
+ codespan(code) {
26
+ return chalk.bgGray.white(` ${code} `)
27
+ },
28
+ code(code, _lang) {
29
+ const lines = code.split('\n').map((l) => ' ' + chalk.gray(l))
30
+ return '\n' + lines.join('\n') + '\n\n'
31
+ },
32
+ blockquote(quote) {
33
+ return quote.split('\n').map((l) => chalk.dim('│ ') + chalk.italic(l)).join('\n') + '\n'
34
+ },
35
+ link(href, _title, text) {
36
+ return `${text} ${chalk.dim(`(${href})`)}`
37
+ },
38
+ image(href, _title, text) {
39
+ return `[image: ${text}] ${chalk.dim(`(${href})`)}`
40
+ },
41
+ list(body, _ordered) {
42
+ return body + '\n'
43
+ },
44
+ listitem(text) {
45
+ return ' • ' + text + '\n'
46
+ },
47
+ hr() {
48
+ return chalk.dim('─'.repeat(60)) + '\n\n'
49
+ },
50
+ br() {
51
+ return '\n'
52
+ },
53
+ del(text) {
54
+ return chalk.strikethrough(text)
55
+ },
56
+ text(text) {
57
+ return text
58
+ },
59
+ html(html) {
60
+ return html.replace(/<[^>]*>/g, '')
61
+ },
62
+ }
63
+
64
+ marked.use({ renderer: terminalRenderer })
65
+
66
+ /**
67
+ * Render a markdown string as ANSI-formatted terminal output.
68
+ * @param {string} content - Raw markdown string
69
+ * @returns {string} ANSI-formatted string ready for terminal output
70
+ */
71
+ export function renderMarkdown(content) {
72
+ return marked(content)
73
+ }
74
+
75
+ /**
76
+ * Extract all Mermaid diagram code blocks from a markdown string.
77
+ * @param {string} content - Raw markdown string
78
+ * @returns {string[]} Array of mermaid diagram source strings (without fences)
79
+ */
80
+ export function extractMermaidBlocks(content) {
81
+ const regex = /```mermaid\n([\s\S]*?)```/g
82
+ const blocks = []
83
+ let match
84
+ while ((match = regex.exec(content)) !== null) {
85
+ blocks.push(match[1].trim())
86
+ }
87
+ return blocks
88
+ }
89
+
90
+ /**
91
+ * Encode a Mermaid diagram as a mermaid.live URL (pako-compressed).
92
+ * @param {string} diagramCode - Mermaid diagram source code
93
+ * @returns {string} Full mermaid.live view URL
94
+ */
95
+ export function toMermaidLiveUrl(diagramCode) {
96
+ const state = JSON.stringify({
97
+ code: diagramCode,
98
+ mermaid: JSON.stringify({ theme: 'default' }),
99
+ updateDiagram: true,
100
+ grid: true,
101
+ panZoom: true,
102
+ rough: false,
103
+ })
104
+ const data = new TextEncoder().encode(state)
105
+ const compressed = deflate(data, { level: 9 })
106
+ const encoded = Buffer.from(compressed).toString('base64url')
107
+ return `https://mermaid.live/view#pako:${encoded}`
108
+ }
@@ -0,0 +1,146 @@
1
+ import { load } from 'js-yaml'
2
+
3
+ /** @import { APIEndpoint, AsyncChannel } from '../types.js' */
4
+
5
+ /**
6
+ * Parse a YAML or JSON string, returning null on error.
7
+ * @param {string} content
8
+ * @returns {Record<string, unknown>|null}
9
+ */
10
+ export function parseYamlOrJson(content) {
11
+ try {
12
+ return JSON.parse(content)
13
+ } catch {
14
+ try {
15
+ return /** @type {Record<string, unknown>} */ (load(content))
16
+ } catch {
17
+ return null
18
+ }
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Check whether a parsed document is an OpenAPI (3.x) or Swagger (2.0) spec.
24
+ * @param {Record<string, unknown>} doc
25
+ * @returns {boolean}
26
+ */
27
+ export function isOpenApi(doc) {
28
+ return Boolean(doc?.openapi || doc?.swagger)
29
+ }
30
+
31
+ /**
32
+ * Check whether a parsed document is an AsyncAPI spec (2.x or 3.x).
33
+ * @param {Record<string, unknown>} doc
34
+ * @returns {boolean}
35
+ */
36
+ export function isAsyncApi(doc) {
37
+ return Boolean(doc?.asyncapi)
38
+ }
39
+
40
+ /**
41
+ * Parse an OpenAPI/Swagger document into a list of APIEndpoints.
42
+ * @param {string} content - Raw YAML or JSON string
43
+ * @returns {{ endpoints: APIEndpoint[], error: string|null }}
44
+ */
45
+ export function parseOpenApi(content) {
46
+ const doc = parseYamlOrJson(content)
47
+ if (!doc || !isOpenApi(doc)) {
48
+ return { endpoints: [], error: 'Not a valid OpenAPI/Swagger document' }
49
+ }
50
+
51
+ /** @type {APIEndpoint[]} */
52
+ const endpoints = []
53
+ const paths = /** @type {Record<string, Record<string, unknown>>} */ (doc.paths ?? {})
54
+
55
+ for (const [path, methods] of Object.entries(paths)) {
56
+ if (!methods || typeof methods !== 'object') continue
57
+ for (const [method, op] of Object.entries(methods)) {
58
+ if (!['get', 'post', 'put', 'patch', 'delete', 'head', 'options'].includes(method)) continue
59
+ const operation = /** @type {Record<string, unknown>} */ (op)
60
+ const rawParams = /** @type {Array<Record<string, unknown>>} */ (operation.parameters ?? [])
61
+ const parameters = rawParams
62
+ .map((p) => (p.required ? `${p.name}*` : String(p.name)))
63
+ .join(', ')
64
+ endpoints.push({
65
+ method: method.toUpperCase(),
66
+ path,
67
+ summary: String(operation.summary ?? ''),
68
+ parameters,
69
+ })
70
+ }
71
+ }
72
+
73
+ return { endpoints, error: null }
74
+ }
75
+
76
+ /**
77
+ * Parse an AsyncAPI document (2.x or 3.x) into a list of AsyncChannels.
78
+ * @param {string} content - Raw YAML or JSON string
79
+ * @returns {{ channels: AsyncChannel[], error: string|null }}
80
+ */
81
+ export function parseAsyncApi(content) {
82
+ const doc = parseYamlOrJson(content)
83
+ if (!doc || !isAsyncApi(doc)) {
84
+ return { channels: [], error: 'Not a valid AsyncAPI document' }
85
+ }
86
+
87
+ /** @type {AsyncChannel[]} */
88
+ const channels = []
89
+ const version = String(doc.asyncapi ?? '')
90
+ const rawChannels = /** @type {Record<string, unknown>} */ (doc.channels ?? {})
91
+
92
+ if (version.startsWith('3')) {
93
+ // AsyncAPI 3.x: channels + operations
94
+ const rawOps = /** @type {Record<string, Record<string, unknown>>} */ (doc.operations ?? {})
95
+ for (const [channelName] of Object.entries(rawChannels)) {
96
+ const matchingOps = Object.values(rawOps).filter((op) => {
97
+ const ch = /** @type {Record<string, unknown>} */ (op.channel ?? {})
98
+ return String(ch.$ref ?? '').includes(channelName) || String(ch ?? '') === channelName
99
+ })
100
+ if (matchingOps.length === 0) {
101
+ channels.push({ channel: channelName, operation: '—', summary: '', message: '—' })
102
+ }
103
+ for (const op of matchingOps) {
104
+ const msgTitle = resolveMessageTitle(op.messages)
105
+ channels.push({
106
+ channel: channelName,
107
+ operation: String(op.action ?? '—'),
108
+ summary: String(op.summary ?? ''),
109
+ message: msgTitle,
110
+ })
111
+ }
112
+ }
113
+ } else {
114
+ // AsyncAPI 2.x: channels[name].publish / .subscribe
115
+ for (const [channelName, channelDef] of Object.entries(rawChannels)) {
116
+ const def = /** @type {Record<string, unknown>} */ (channelDef ?? {})
117
+ for (const op of ['publish', 'subscribe']) {
118
+ if (!def[op]) continue
119
+ const opDef = /** @type {Record<string, unknown>} */ (def[op])
120
+ const msgDef = /** @type {Record<string, unknown>} */ (opDef.message ?? {})
121
+ const msgTitle = String(msgDef.name ?? msgDef.title ?? '—')
122
+ channels.push({
123
+ channel: channelName,
124
+ operation: op,
125
+ summary: String(opDef.summary ?? ''),
126
+ message: msgTitle,
127
+ })
128
+ }
129
+ }
130
+ }
131
+
132
+ return { channels, error: null }
133
+ }
134
+
135
+ /**
136
+ * Resolve a message title from an AsyncAPI 3.x messages ref list.
137
+ * @param {unknown} messages
138
+ * @returns {string}
139
+ */
140
+ function resolveMessageTitle(messages) {
141
+ if (!messages || typeof messages !== 'object') return '—'
142
+ const msgs = Object.values(/** @type {Record<string, unknown>} */ (messages))
143
+ if (msgs.length === 0) return '—'
144
+ const first = /** @type {Record<string, unknown>} */ (msgs[0])
145
+ return String(first.name ?? first.title ?? '—')
146
+ }
@@ -0,0 +1,48 @@
1
+ import chalk from 'chalk'
2
+ import { colorStatus } from './table.js'
3
+
4
+ /** @import { DoctorCheck } from '../types.js' */
5
+
6
+ /**
7
+ * Format a DoctorCheck for terminal output.
8
+ * @param {DoctorCheck} check
9
+ * @returns {string}
10
+ */
11
+ export function formatDoctorCheck(check) {
12
+ const badge = colorStatus(check.status)
13
+ const version = check.version ? chalk.gray(` ${check.version}`) : ''
14
+ const hint = check.status !== 'ok' && check.hint ? chalk.dim(`\n → ${check.hint}`) : ''
15
+ return `${badge} ${check.name}${version}${hint}`
16
+ }
17
+
18
+ /**
19
+ * Format a summary line for doctor output.
20
+ * @param {{ ok: number, warn: number, fail: number }} summary
21
+ * @returns {string}
22
+ */
23
+ export function formatDoctorSummary(summary) {
24
+ const parts = [
25
+ chalk.green(`${summary.ok} ok`),
26
+ chalk.yellow(`${summary.warn} warnings`),
27
+ chalk.red(`${summary.fail} failures`),
28
+ ]
29
+ return parts.join(', ')
30
+ }
31
+
32
+ /**
33
+ * Format a CI status badge.
34
+ * @param {'pass'|'fail'|'pending'|string} status
35
+ * @returns {string}
36
+ */
37
+ export function formatCIStatus(status) {
38
+ return colorStatus(status)
39
+ }
40
+
41
+ /**
42
+ * Format a review status badge.
43
+ * @param {'approved'|'changes_requested'|'pending'|string} status
44
+ * @returns {string}
45
+ */
46
+ export function formatReviewStatus(status) {
47
+ return colorStatus(status)
48
+ }
@@ -0,0 +1,87 @@
1
+ import chalk from 'chalk'
2
+
3
+ /**
4
+ * @typedef {Object} TableColumn
5
+ * @property {string} header - Column header text
6
+ * @property {string} key - Key to extract from row object
7
+ * @property {number} [width] - Fixed max column width (truncates with … if exceeded)
8
+ * @property {function(*): string} [format] - Custom cell formatter (plain text, used for width calc)
9
+ * @property {function(string): string} [colorize] - Chalk color applied to formatted value at render time
10
+ */
11
+
12
+ /**
13
+ * Truncate a string to max length, appending … if needed.
14
+ * @param {string} str
15
+ * @param {number} max
16
+ * @returns {string}
17
+ */
18
+ function truncate(str, max) {
19
+ if (str.length <= max) return str
20
+ return str.slice(0, max - 1) + '…'
21
+ }
22
+
23
+ /**
24
+ * Get the plain (no ANSI) formatted value for a cell.
25
+ * @param {TableColumn} col
26
+ * @param {*} rawVal
27
+ * @returns {string}
28
+ */
29
+ function plainCell(col, rawVal) {
30
+ const formatted = col.format ? col.format(rawVal) : String(rawVal ?? '')
31
+ return col.width ? truncate(formatted, col.width) : formatted
32
+ }
33
+
34
+ /**
35
+ * Render a list of objects as a terminal table.
36
+ * @param {Record<string, unknown>[]} rows
37
+ * @param {TableColumn[]} columns
38
+ * @returns {string}
39
+ */
40
+ export function renderTable(rows, columns) {
41
+ if (rows.length === 0) return ''
42
+
43
+ // Calculate column widths from plain text (ANSI-safe)
44
+ const widths = columns.map((col) => {
45
+ if (col.width) return col.width
46
+ const headerLen = col.header.length
47
+ const maxDataLen = rows.reduce((max, row) => {
48
+ return Math.max(max, plainCell(col, row[col.key]).length)
49
+ }, 0)
50
+ return Math.max(headerLen, maxDataLen)
51
+ })
52
+
53
+ // Header row
54
+ const header = columns
55
+ .map((col, i) => chalk.bold.white(col.header.padEnd(widths[i])))
56
+ .join(' ')
57
+
58
+ // Divider
59
+ const divider = chalk.dim(widths.map((w) => '─'.repeat(w)).join(' '))
60
+
61
+ // Data rows
62
+ const dataRows = rows.map((row) =>
63
+ columns
64
+ .map((col, i) => {
65
+ const plain = plainCell(col, row[col.key])
66
+ const padding = ' '.repeat(Math.max(0, widths[i] - plain.length))
67
+ const colored = col.colorize ? col.colorize(plain) : plain
68
+ return colored + padding
69
+ })
70
+ .join(' '),
71
+ )
72
+
73
+ return [header, divider, ...dataRows].join('\n')
74
+ }
75
+
76
+ /**
77
+ * Format a status value with color.
78
+ * @param {'ok'|'warn'|'fail'|'pass'|'success'|'failure'|string} status
79
+ * @returns {string}
80
+ */
81
+ export function colorStatus(status) {
82
+ const s = status?.toLowerCase() ?? ''
83
+ if (['ok', 'pass', 'success', 'approved'].includes(s)) return chalk.green('✓')
84
+ if (['warn', 'pending', 'in_progress', 'queued'].includes(s)) return chalk.yellow('⚠')
85
+ if (['fail', 'failure', 'error', 'changes_requested'].includes(s)) return chalk.red('✗')
86
+ return chalk.gray('○')
87
+ }