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
package/src/help.js ADDED
@@ -0,0 +1,312 @@
1
+ import { Help } from '@oclif/core'
2
+ import chalk from 'chalk'
3
+ import { isColorEnabled } from './utils/gradient.js'
4
+ import { printBanner } from './utils/banner.js'
5
+
6
+ // ─── Brand palette (flat — no gradient on help rows) ────────────────────────
7
+ const ORANGE = '#FF6B2B'
8
+ const LIGHT_ORANGE = '#FF9A5C'
9
+ const DIM_BLUE = '#4A9EFF'
10
+ const DIM_GRAY = '#888888'
11
+
12
+ // Strip ANSI escape codes
13
+ const ANSI_RE = /\x1B\[[0-?]*[ -/]*[@-~]/g
14
+ /**
15
+ * @param {string} s
16
+ * @returns {string}
17
+ */
18
+ const strip = (s) => s.replace(ANSI_RE, '')
19
+
20
+ // ─── Category definitions ────────────────────────────────────────────────────
21
+
22
+ /** @typedef {{ id: string, hint?: string }} CmdEntry */
23
+ /** @typedef {{ title: string, cmds: CmdEntry[] }} Category */
24
+
25
+ /** @type {Category[]} */
26
+ const CATEGORIES = [
27
+ {
28
+ title: 'GitHub & Documentazione',
29
+ cmds: [
30
+ { id: 'repo:list', hint: '[--language] [--search]' },
31
+ { id: 'docs:read', hint: '[FILE] [--repo] [--raw] [--render]' },
32
+ { id: 'docs:list', hint: '[--repo] [--search]' },
33
+ { id: 'docs:search', hint: '<TERM> [--repo]' },
34
+ { id: 'docs:projects', hint: '[--search]' },
35
+ { id: 'create:repo', hint: '[TEMPLATE] [--list] [--name]' },
36
+ { id: 'search', hint: '<QUERY>' },
37
+ { id: 'open', hint: '<TARGET>' },
38
+ ],
39
+ },
40
+ {
41
+ title: 'Pull Request',
42
+ cmds: [
43
+ { id: 'pr:create', hint: '' },
44
+ { id: 'pr:status', hint: '' },
45
+ { id: 'pr:detail', hint: '<PR_NUMBER> --repo <owner/repo>' },
46
+ { id: 'pr:review', hint: '' },
47
+ ],
48
+ },
49
+ {
50
+ title: 'Pipeline & DevOps',
51
+ cmds: [
52
+ { id: 'pipeline:status', hint: '[--repo] [--branch]' },
53
+ { id: 'pipeline:rerun', hint: '<RUN_ID> --repo <repo>' },
54
+ { id: 'pipeline:logs', hint: '<RUN_ID> --repo <repo>' },
55
+ { id: 'changelog', hint: '' },
56
+ ],
57
+ },
58
+ {
59
+ title: 'Tasks (ClickUp)',
60
+ cmds: [
61
+ { id: 'tasks:list', hint: '[--status] [--search]' },
62
+ { id: 'tasks:today', hint: '' },
63
+ ],
64
+ },
65
+ {
66
+ title: 'Cloud & Costi',
67
+ cmds: [
68
+ { id: 'costs:get', hint: '[--period] [--profile]' },
69
+ ],
70
+ },
71
+ {
72
+ title: 'Setup & Ambiente',
73
+ cmds: [
74
+ { id: 'init', hint: '[--dry-run]' },
75
+ { id: 'doctor', hint: '' },
76
+ { id: 'auth:login', hint: '' },
77
+ { id: 'whoami', hint: '' },
78
+ { id: 'upgrade', hint: '' },
79
+ ],
80
+ },
81
+ ]
82
+
83
+ // ─── Example commands shown at bottom of root help ──────────────────────────
84
+ const EXAMPLES = [
85
+ { cmd: 'dvmi docs read', note: 'Leggi il README del repo corrente' },
86
+ { cmd: 'dvmi docs read openapi.yaml', note: 'Tabella endpoints OpenAPI nel terminale' },
87
+ { cmd: 'dvmi docs search "authentication"', note: 'Cerca nei docs del repo corrente' },
88
+ { cmd: 'dvmi repo list --search "api"', note: 'Filtra repository per nome' },
89
+ { cmd: 'dvmi pr status', note: 'PR aperte e review in attesa' },
90
+ { cmd: 'dvmi pipeline status', note: 'Ultimi workflow CI/CD' },
91
+ { cmd: 'dvmi tasks list --search "bug"', note: 'Cerca task ClickUp' },
92
+ { cmd: 'dvmi costs get --json', note: 'Costi AWS in formato JSON' },
93
+ ]
94
+
95
+ // ─── Help class ─────────────────────────────────────────────────────────────
96
+
97
+ /**
98
+ * Custom help class.
99
+ * - showRootHelp: logo SNTG animato + layout comandi raggruppati per categoria
100
+ * - formatTopic / formatCommand: colorizza flag, descrizioni e esempi
101
+ * - Gradient solo sul logo; tutto il resto usa colori flat chalk
102
+ */
103
+ export default class CustomHelp extends Help {
104
+
105
+ /**
106
+ * Root help override: banner animato → layout categorizzato.
107
+ * Override di showRootHelp() (async) per evitare che formatRoot() (sync)
108
+ * debba attendere la Promise del banner.
109
+ * @returns {Promise<void>}
110
+ */
111
+ async showRootHelp() {
112
+ // Animated logo — identical to `dvmi init` (no-ops in CI/non-TTY)
113
+ await printBanner()
114
+ this.log(this.#buildRootLayout())
115
+ }
116
+
117
+ /**
118
+ * @param {import('@oclif/core').Interfaces.Topic[]} topics
119
+ * @returns {string}
120
+ */
121
+ formatTopics(topics) {
122
+ return this.#flatColorizeTopics(super.formatTopics(topics))
123
+ }
124
+
125
+ /**
126
+ * @param {import('@oclif/core').Interfaces.Topic} topic
127
+ * @param {import('@oclif/core').Command.Class[]} commands
128
+ * @returns {string}
129
+ */
130
+ formatTopic(topic, commands) {
131
+ return this.#colorizeRows(super.formatTopic(topic, commands))
132
+ }
133
+
134
+ /**
135
+ * @param {import('@oclif/core').Command.Class} command
136
+ * @returns {string}
137
+ */
138
+ formatCommand(command) {
139
+ return this.#colorizeRows(super.formatCommand(command))
140
+ }
141
+
142
+ // ─── Private helpers ──────────────────────────────────────────────────────
143
+
144
+ /**
145
+ * Build the full categorized root help layout.
146
+ * @returns {string}
147
+ */
148
+ #buildRootLayout() {
149
+ /** @type {Map<string, import('@oclif/core').Command.Cached>} */
150
+ const cmdMap = new Map(this.config.commands.map((c) => [c.id, c]))
151
+
152
+ const lines = []
153
+
154
+ // ── Usage ──────────────────────────────────────────────────────────────
155
+ lines.push(this.#sectionHeader('USAGE'))
156
+ lines.push(
157
+ ' ' + (isColorEnabled ? chalk.hex(ORANGE).bold('dvmi') : 'dvmi') +
158
+ chalk.dim(' <COMANDO> [FLAGS]\n'),
159
+ )
160
+
161
+ // ── Comandi per categoria ──────────────────────────────────────────────
162
+ lines.push(this.#sectionHeader('COMMANDS'))
163
+
164
+ for (const cat of CATEGORIES) {
165
+ lines.push(
166
+ ' ' + (isColorEnabled ? chalk.hex(ORANGE).bold(cat.title) : cat.title),
167
+ )
168
+
169
+ for (const entry of cat.cmds) {
170
+ const cmd = cmdMap.get(entry.id)
171
+ if (!cmd) continue
172
+
173
+ const displayId = entry.id.replaceAll(':', ' ')
174
+ const hint = entry.hint || ''
175
+ const desc = cmd.summary ?? (typeof cmd.description === 'string'
176
+ ? cmd.description.split('\n')[0]
177
+ : '')
178
+
179
+ // Left column (name + flags hint), right-padded to align descriptions
180
+ const rawLeft = ' ' + displayId + (hint ? ' ' + hint : '')
181
+ const pad = ' '.repeat(Math.max(2, 50 - rawLeft.length))
182
+
183
+ const leftPart = isColorEnabled
184
+ ? ' ' + chalk.hex(LIGHT_ORANGE).bold(displayId) +
185
+ (hint ? ' ' + chalk.dim(hint) : '')
186
+ : rawLeft
187
+
188
+ lines.push(leftPart + pad + chalk.dim(desc))
189
+ }
190
+
191
+ lines.push('')
192
+ }
193
+
194
+ // ── Flag globali ───────────────────────────────────────────────────────
195
+ lines.push(this.#sectionHeader('GLOBAL FLAGS'))
196
+ lines.push(this.#flagLine('-h, --help', 'Mostra aiuto per un comando'))
197
+ lines.push(this.#flagLine(' --json', 'Output in formato JSON strutturato'))
198
+ lines.push(this.#flagLine('-v, --version', 'Versione installata'))
199
+ lines.push('')
200
+
201
+ // ── Esempi ─────────────────────────────────────────────────────────────
202
+ lines.push(this.#sectionHeader('EXAMPLES'))
203
+
204
+ const maxCmdLen = Math.max(...EXAMPLES.map((e) => e.cmd.length))
205
+ for (const ex of EXAMPLES) {
206
+ const pad = ' '.repeat(maxCmdLen - ex.cmd.length + 4)
207
+ const sub = ex.cmd.replace(/^dvmi /, '')
208
+ const formatted = isColorEnabled
209
+ ? chalk.dim('$') + ' ' + chalk.hex(ORANGE).bold('dvmi') + ' ' +
210
+ chalk.white(sub) + pad + chalk.hex(DIM_GRAY)(ex.note)
211
+ : '$ ' + ex.cmd + pad + ex.note
212
+ lines.push(' ' + formatted)
213
+ }
214
+
215
+ lines.push('')
216
+ lines.push(
217
+ ' ' + chalk.dim('Approfondisci:') + ' ' +
218
+ chalk.hex(DIM_BLUE)('dvmi <COMANDO> --help') +
219
+ chalk.dim(' · ') +
220
+ chalk.hex(DIM_BLUE)('dvmi <TOPIC> --help') + '\n',
221
+ )
222
+
223
+ return lines.join('\n')
224
+ }
225
+
226
+ /**
227
+ * @param {string} title
228
+ * @returns {string}
229
+ */
230
+ #sectionHeader(title) {
231
+ return isColorEnabled ? chalk.hex(ORANGE).bold(title) : title
232
+ }
233
+
234
+ /**
235
+ * @param {string} flagStr
236
+ * @param {string} desc
237
+ * @returns {string}
238
+ */
239
+ #flagLine(flagStr, desc) {
240
+ const pad = ' '.repeat(Math.max(2, 18 - flagStr.length))
241
+ return isColorEnabled
242
+ ? ' ' + chalk.hex(DIM_BLUE).bold(flagStr) + pad + chalk.dim(desc)
243
+ : ' ' + flagStr + pad + desc
244
+ }
245
+
246
+ /**
247
+ * Colorize topic list with flat orange names (no gradient).
248
+ * @param {string} text
249
+ * @returns {string}
250
+ */
251
+ #flatColorizeTopics(text) {
252
+ return text
253
+ .split('\n')
254
+ .map((line) => {
255
+ const plain = strip(line)
256
+ if (!plain.trim()) return line
257
+
258
+ const rowMatch = plain.match(/^( )([a-z][\w-]*)(\s{2,})(.+)$/)
259
+ if (rowMatch) {
260
+ const [, indent, name, spaces, desc] = rowMatch
261
+ return indent + chalk.hex(LIGHT_ORANGE).bold(name) + spaces + chalk.white(desc)
262
+ }
263
+
264
+ const subMatch = plain.match(/^( )([a-z][\w -]*)(\s{2,})(.*)$/)
265
+ if (subMatch) {
266
+ const [, indent, name, spaces, desc] = subMatch
267
+ return indent + chalk.hex(LIGHT_ORANGE)(name) + spaces + chalk.dim(desc)
268
+ }
269
+
270
+ return line
271
+ })
272
+ .join('\n')
273
+ }
274
+
275
+ /**
276
+ * Colorize flag rows and command rows in individual command help pages.
277
+ * @param {string} text
278
+ * @returns {string}
279
+ */
280
+ #colorizeRows(text) {
281
+ return text
282
+ .split('\n')
283
+ .map((line) => {
284
+ const plain = strip(line)
285
+ if (!plain.trim()) return line
286
+
287
+ // Example lines: "$ dvmi …"
288
+ if (plain.includes('$ dvmi') || plain.trim().startsWith('$ dvmi')) {
289
+ return plain.replace(/\$ (dvmi\S*)/g, (_, cmd) =>
290
+ '$ ' + chalk.hex(ORANGE).bold(cmd),
291
+ )
292
+ }
293
+
294
+ // Flag rows: "--flag desc" or "-f, --flag desc"
295
+ const flagMatch = plain.match(/^(\s{2,})((?:-\w,\s*)?--[\w-]+)(\s+)(.*)$/)
296
+ if (flagMatch) {
297
+ const [, indent, flags, spaces, desc] = flagMatch
298
+ return indent + chalk.hex(DIM_BLUE).bold(flags) + spaces + chalk.dim(desc)
299
+ }
300
+
301
+ // Command/topic rows: " name description"
302
+ const rowMatch = plain.match(/^( )([a-z][\w:-]*)(\s{2,})(.+)$/)
303
+ if (rowMatch) {
304
+ const [, indent, name, spaces, desc] = rowMatch
305
+ return indent + chalk.hex(LIGHT_ORANGE).bold(name) + spaces + chalk.white(desc)
306
+ }
307
+
308
+ return line
309
+ })
310
+ .join('\n')
311
+ }
312
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Pre-command hook: trigger non-blocking version check.
3
+ */
4
+ export const init = async () => {
5
+ // Fire-and-forget version check — result used by postrun hook
6
+ import('../services/version-check.js')
7
+ .then(({ checkForUpdate }) => checkForUpdate())
8
+ .catch(() => null) // never block command execution
9
+ }
@@ -0,0 +1,18 @@
1
+ import chalk from 'chalk'
2
+
3
+ /**
4
+ * Post-command hook: display update notification if a newer version is available.
5
+ */
6
+ export const postrun = async () => {
7
+ try {
8
+ const { checkForUpdate } = await import('../services/version-check.js')
9
+ const { hasUpdate, current, latest } = await checkForUpdate()
10
+ if (hasUpdate && latest) {
11
+ process.stderr.write(
12
+ chalk.dim(`\nUpdate available: ${current} → ${chalk.green(latest)}. Run \`dvmi upgrade\`\n`),
13
+ )
14
+ }
15
+ } catch {
16
+ // Never interrupt user flow
17
+ }
18
+ }
package/src/index.js ADDED
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core'
@@ -0,0 +1,83 @@
1
+ import { exec } from './shell.js'
2
+ import { loadConfig } from './config.js'
3
+
4
+ /**
5
+ * @typedef {Object} AuthStatus
6
+ * @property {boolean} authenticated
7
+ * @property {string} [username]
8
+ * @property {string} [account]
9
+ * @property {string} [role]
10
+ * @property {string} [error]
11
+ */
12
+
13
+ /**
14
+ * Check GitHub authentication status.
15
+ * @returns {Promise<AuthStatus>}
16
+ */
17
+ export async function checkGitHubAuth() {
18
+ const result = await exec('gh', ['auth', 'status'])
19
+ if (result.exitCode !== 0) {
20
+ return { authenticated: false, error: result.stderr }
21
+ }
22
+ // Extract username from output like "Logged in to github.com as username"
23
+ const match = result.stderr.match(/Logged in to .+ as (\S+)/)
24
+ return { authenticated: true, username: match?.[1] ?? 'unknown' }
25
+ }
26
+
27
+ /**
28
+ * Log in to GitHub via SSO (opens browser).
29
+ * @returns {Promise<AuthStatus>}
30
+ */
31
+ export async function loginGitHub() {
32
+ const result = await exec('gh', ['auth', 'login', '--web'])
33
+ if (result.exitCode !== 0) {
34
+ return { authenticated: false, error: result.stderr }
35
+ }
36
+ return checkGitHubAuth()
37
+ }
38
+
39
+ /**
40
+ * Check AWS authentication via aws-vault.
41
+ * @returns {Promise<AuthStatus>}
42
+ */
43
+ export async function checkAWSAuth() {
44
+ const config = await loadConfig()
45
+ if (!config.awsProfile) return { authenticated: false, error: 'No AWS profile configured' }
46
+
47
+ const result = await exec('aws-vault', [
48
+ 'exec',
49
+ config.awsProfile,
50
+ '--',
51
+ 'aws',
52
+ 'sts',
53
+ 'get-caller-identity',
54
+ '--output',
55
+ 'json',
56
+ ])
57
+ if (result.exitCode !== 0) {
58
+ return { authenticated: false, error: result.stderr || 'Session expired' }
59
+ }
60
+ try {
61
+ const identity = JSON.parse(result.stdout)
62
+ return {
63
+ authenticated: true,
64
+ account: identity.Account,
65
+ role: identity.Arn?.split('/').at(-1),
66
+ }
67
+ } catch {
68
+ return { authenticated: false, error: 'Could not parse AWS identity' }
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Log in to AWS via aws-vault.
74
+ * @param {string} profile - aws-vault profile name
75
+ * @returns {Promise<AuthStatus>}
76
+ */
77
+ export async function loginAWS(profile) {
78
+ const result = await exec('aws-vault', ['login', profile])
79
+ if (result.exitCode !== 0) {
80
+ return { authenticated: false, error: result.stderr }
81
+ }
82
+ return checkAWSAuth()
83
+ }
@@ -0,0 +1,80 @@
1
+ import { CostExplorerClient, GetCostAndUsageCommand } from '@aws-sdk/client-cost-explorer'
2
+
3
+ /** @import { AWSCostEntry } from '../types.js' */
4
+
5
+ /**
6
+ * Get the date range for a cost period.
7
+ * @param {'last-month'|'last-week'|'mtd'} period
8
+ * @returns {{ start: string, end: string }}
9
+ */
10
+ function getPeriodDates(period) {
11
+ const now = new Date()
12
+ const fmt = (d) => d.toISOString().split('T')[0]
13
+
14
+ if (period === 'last-month') {
15
+ const start = new Date(now.getFullYear(), now.getMonth() - 1, 1)
16
+ const end = new Date(now.getFullYear(), now.getMonth(), 1)
17
+ return { start: fmt(start), end: fmt(end) }
18
+ }
19
+ if (period === 'last-week') {
20
+ const end = new Date(now)
21
+ end.setDate(now.getDate() - now.getDay())
22
+ const start = new Date(end)
23
+ start.setDate(end.getDate() - 7)
24
+ return { start: fmt(start), end: fmt(end) }
25
+ }
26
+ // mtd
27
+ const start = new Date(now.getFullYear(), now.getMonth(), 1)
28
+ return { start: fmt(start), end: fmt(now) }
29
+ }
30
+
31
+ /**
32
+ * Query AWS Cost Explorer for costs filtered by project tags.
33
+ * @param {string} serviceName - Tag value for filtering
34
+ * @param {Record<string, string>} tags - Project tags (key-value pairs)
35
+ * @param {'last-month'|'last-week'|'mtd'} [period]
36
+ * @returns {Promise<{ entries: AWSCostEntry[], period: { start: string, end: string } }>}
37
+ */
38
+ export async function getServiceCosts(serviceName, tags, period = 'last-month') {
39
+ // Cost Explorer always uses us-east-1
40
+ const client = new CostExplorerClient({ region: 'us-east-1' })
41
+ const { start, end } = getPeriodDates(period)
42
+
43
+ // Build tag filter from project tags
44
+ const tagEntries = Object.entries(tags)
45
+ const filter =
46
+ tagEntries.length === 1
47
+ ? { Tags: { Key: tagEntries[0][0], Values: [tagEntries[0][1]] } }
48
+ : {
49
+ And: tagEntries.map(([k, v]) => ({
50
+ Tags: { Key: k, Values: [v] },
51
+ })),
52
+ }
53
+
54
+ const command = new GetCostAndUsageCommand({
55
+ TimePeriod: { Start: start, End: end },
56
+ Granularity: 'MONTHLY',
57
+ Metrics: ['UnblendedCost'],
58
+ Filter: filter,
59
+ GroupBy: [{ Type: 'DIMENSION', Key: 'SERVICE' }],
60
+ })
61
+
62
+ const result = await client.send(command)
63
+ const entries = []
64
+
65
+ for (const timeResult of result.ResultsByTime ?? []) {
66
+ for (const group of timeResult.Groups ?? []) {
67
+ const amount = Number(group.Metrics?.UnblendedCost?.Amount ?? 0)
68
+ if (amount > 0) {
69
+ entries.push({
70
+ serviceName: group.Keys?.[0] ?? 'Unknown',
71
+ amount,
72
+ unit: group.Metrics?.UnblendedCost?.Unit ?? 'USD',
73
+ period: { start, end },
74
+ })
75
+ }
76
+ }
77
+ }
78
+
79
+ return { entries, period: { start, end } }
80
+ }