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,51 @@
1
+ import { Command } from '@oclif/core'
2
+ import chalk from 'chalk'
3
+ import ora from 'ora'
4
+ import { listMyPRs } from '../../services/github.js'
5
+ import { loadConfig } from '../../services/config.js'
6
+ import { renderTable, colorStatus } from '../../formatters/table.js'
7
+
8
+ export default class PRReview extends Command {
9
+ static description = 'Lista PR assegnate a te per la code review'
10
+
11
+ static examples = [
12
+ '<%= config.bin %> pr review',
13
+ '<%= config.bin %> pr review --json',
14
+ ]
15
+
16
+ static enableJsonFlag = true
17
+
18
+ async run() {
19
+ const { flags } = await this.parse(PRReview)
20
+ const isJson = flags.json
21
+ const config = await loadConfig()
22
+
23
+ if (!config.org) {
24
+ this.error("GitHub org non configurata. Esegui `dvmi init` per configurare l'ambiente.")
25
+ }
26
+
27
+ const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Caricamento PR in review...') }).start()
28
+ const { reviewing } = await listMyPRs(config.org)
29
+ spinner?.stop()
30
+
31
+ if (isJson) return { reviewing }
32
+
33
+ if (reviewing.length === 0) {
34
+ this.log(chalk.dim('Nessuna PR assegnata per review.'))
35
+ return { reviewing }
36
+ }
37
+
38
+ this.log(chalk.bold(`\nPR ASSEGNATE PER REVIEW (${reviewing.length}):`))
39
+ this.log(
40
+ renderTable(reviewing, [
41
+ { header: '#', key: 'number', width: 6 },
42
+ { header: 'Titolo', key: 'title', width: 45 },
43
+ { header: 'Autore', key: 'author', width: 20 },
44
+ { header: 'Branch', key: 'headBranch', width: 30 },
45
+ { header: 'CI', key: 'ciStatus', width: 10, format: (v) => colorStatus(String(v)) },
46
+ ]),
47
+ )
48
+
49
+ return { reviewing }
50
+ }
51
+ }
@@ -0,0 +1,70 @@
1
+ import { Command, Flags } from '@oclif/core'
2
+ import chalk from 'chalk'
3
+ import ora from 'ora'
4
+ import { listMyPRs } from '../../services/github.js'
5
+ import { loadConfig } from '../../services/config.js'
6
+ import { renderTable, colorStatus } from '../../formatters/table.js'
7
+
8
+ export default class PRStatus extends Command {
9
+ static description = 'Stato delle tue PR aperte (come autore e come reviewer)'
10
+
11
+ static examples = [
12
+ '<%= config.bin %> pr status',
13
+ '<%= config.bin %> pr status --author',
14
+ '<%= config.bin %> pr status --json',
15
+ ]
16
+
17
+ static enableJsonFlag = true
18
+
19
+ static flags = {
20
+ author: Flags.boolean({ description: 'Solo PR dove sei autore', default: false }),
21
+ reviewer: Flags.boolean({ description: 'Solo PR dove sei reviewer', default: false }),
22
+ }
23
+
24
+ async run() {
25
+ const { flags } = await this.parse(PRStatus)
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
+ const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching PRs...') }).start()
34
+ const { authored, reviewing } = await listMyPRs(config.org)
35
+ spinner?.stop()
36
+
37
+ const showAuthored = !flags.reviewer || flags.author
38
+ const showReviewing = !flags.author || flags.reviewer
39
+
40
+ if (isJson) {
41
+ return {
42
+ authored: showAuthored ? authored : [],
43
+ reviewing: showReviewing ? reviewing : [],
44
+ }
45
+ }
46
+
47
+ if (showAuthored && authored.length > 0) {
48
+ this.log(chalk.bold('\nYOUR PRS:'))
49
+ this.log(renderTable(authored, [
50
+ { header: 'Repo', key: 'headBranch', width: 30, format: (v) => String(v).split('/')[0] },
51
+ { header: 'Title', key: 'title', width: 40 },
52
+ { header: 'CI', key: 'ciStatus', width: 10, format: (v) => colorStatus(String(v)) },
53
+ { header: 'Review', key: 'reviewStatus', width: 20, format: (v) => colorStatus(String(v)) },
54
+ ]))
55
+ } else if (showAuthored) {
56
+ this.log(chalk.dim('No authored PRs found.'))
57
+ }
58
+
59
+ if (showReviewing && reviewing.length > 0) {
60
+ this.log(chalk.bold('\nREVIEW REQUESTED:'))
61
+ this.log(renderTable(reviewing, [
62
+ { header: 'Title', key: 'title', width: 40 },
63
+ { header: 'Author', key: 'author', width: 20 },
64
+ { header: 'CI', key: 'ciStatus', width: 10, format: (v) => colorStatus(String(v)) },
65
+ ]))
66
+ }
67
+
68
+ return { authored, reviewing }
69
+ }
70
+ }
@@ -0,0 +1,113 @@
1
+ import { Command, Flags } from '@oclif/core'
2
+ import ora from 'ora'
3
+ import chalk from 'chalk'
4
+ import { listRepos } from '../../services/github.js'
5
+ import { loadConfig } from '../../services/config.js'
6
+ import { renderTable } from '../../formatters/table.js'
7
+
8
+ /**
9
+ * @param {string} lang
10
+ * @returns {string}
11
+ */
12
+ function langColor(lang) {
13
+ const map = {
14
+ javascript: chalk.yellow,
15
+ typescript: chalk.blue,
16
+ python: chalk.green,
17
+ java: chalk.red,
18
+ go: chalk.cyan,
19
+ ruby: chalk.magenta,
20
+ rust: chalk.hex('#CE422B'),
21
+ kotlin: chalk.hex('#7F52FF'),
22
+ swift: chalk.hex('#F05138'),
23
+ php: chalk.hex('#777BB4'),
24
+ shell: chalk.greenBright,
25
+ }
26
+ const fn = map[lang.toLowerCase()]
27
+ return fn ? fn(lang) : chalk.dim(lang)
28
+ }
29
+
30
+ export default class RepoList extends Command {
31
+ static description = 'Lista repository dell\'organizzazione'
32
+
33
+ static examples = [
34
+ '<%= config.bin %> repo list',
35
+ '<%= config.bin %> repo list --language typescript',
36
+ '<%= config.bin %> repo list --search "lambda"',
37
+ '<%= config.bin %> repo list --topic microservice --search "api"',
38
+ '<%= config.bin %> repo list --json',
39
+ ]
40
+
41
+ static enableJsonFlag = true
42
+
43
+ static flags = {
44
+ language: Flags.string({ description: 'Filtra per linguaggio' }),
45
+ topic: Flags.string({ description: 'Filtra per topic' }),
46
+ search: Flags.string({ char: 's', description: 'Cerca in nome e descrizione (case-insensitive)' }),
47
+ }
48
+
49
+ async run() {
50
+ const { flags } = await this.parse(RepoList)
51
+ const isJson = flags.json
52
+ const config = await loadConfig()
53
+
54
+ if (!config.org) {
55
+ this.error('GitHub org not configured. Run `dvmi init` to set up your environment.')
56
+ }
57
+
58
+ const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching repositories...') }).start()
59
+ const repos = await listRepos(config.org, {
60
+ language: flags.language,
61
+ topic: flags.topic,
62
+ })
63
+ spinner?.stop()
64
+
65
+ // Search filter (name + description)
66
+ const searchQuery = flags.search?.toLowerCase()
67
+ const filtered = searchQuery
68
+ ? repos.filter((r) =>
69
+ r.name.toLowerCase().includes(searchQuery) ||
70
+ r.description.toLowerCase().includes(searchQuery),
71
+ )
72
+ : repos
73
+
74
+ if (isJson) return { repositories: filtered, total: filtered.length }
75
+
76
+ if (repos.length === 0) {
77
+ this.log(chalk.yellow('No repositories found matching your filters.'))
78
+ return { repositories: [], total: 0 }
79
+ }
80
+
81
+ if (filtered.length === 0) {
82
+ this.log(chalk.dim(`No repositories matching "${flags.search}".`))
83
+ return { repositories: [], total: 0 }
84
+ }
85
+
86
+ // Build filter info line
87
+ const filterInfo = [
88
+ flags.language && chalk.dim(`language: ${chalk.white(flags.language)}`),
89
+ flags.topic && chalk.dim(`topic: ${chalk.white(flags.topic)}`),
90
+ flags.search && chalk.dim(`search: ${chalk.white(`"${flags.search}"`)}`),
91
+ ].filter(Boolean).join(chalk.dim(' · '))
92
+
93
+ this.log(
94
+ chalk.bold(`\nRepositories in ${config.org}`) +
95
+ (filterInfo ? chalk.dim(' — ') + filterInfo : '') +
96
+ chalk.dim(` (${filtered.length}${filtered.length < repos.length ? `/${repos.length}` : ''})`) +
97
+ '\n',
98
+ )
99
+
100
+ this.log(renderTable(filtered, [
101
+ { header: 'Name', key: 'name', width: 40 },
102
+ { header: 'Language', key: 'language', width: 14, format: (v) => v || '—', colorize: (v) => v === '—' ? chalk.dim(v) : langColor(v) },
103
+ { header: 'Last push', key: 'pushedAt', width: 12, format: (v) => {
104
+ const d = new Date(String(v))
105
+ return isNaN(d.getTime()) ? '—' : d.toLocaleDateString()
106
+ }},
107
+ { header: 'Description', key: 'description', width: 60, format: (v) => String(v || '—') },
108
+ ]))
109
+
110
+ this.log('')
111
+ return { repositories: filtered, total: filtered.length }
112
+ }
113
+ }
@@ -0,0 +1,62 @@
1
+ import { Command, Args, Flags } from '@oclif/core'
2
+ import chalk from 'chalk'
3
+ import ora from 'ora'
4
+ import { searchCode } from '../services/github.js'
5
+ import { loadConfig } from '../services/config.js'
6
+ import { renderTable } from '../formatters/table.js'
7
+
8
+ export default class Search extends Command {
9
+ static description = 'Cerca codice nei repository dell\'organizzazione'
10
+
11
+ static examples = [
12
+ '<%= config.bin %> search "getUserById"',
13
+ '<%= config.bin %> search "TODO" --language typescript',
14
+ '<%= config.bin %> search "config" --repo my-service --json',
15
+ ]
16
+
17
+ static enableJsonFlag = true
18
+
19
+ static args = {
20
+ term: Args.string({ description: 'Termine di ricerca', required: true }),
21
+ }
22
+
23
+ static flags = {
24
+ language: Flags.string({ description: 'Filtra per linguaggio' }),
25
+ repo: Flags.string({ description: 'Cerca in un repo specifico' }),
26
+ limit: Flags.integer({ description: 'Max risultati', default: 20 }),
27
+ }
28
+
29
+ async run() {
30
+ const { args, flags } = await this.parse(Search)
31
+ const isJson = flags.json
32
+ const config = await loadConfig()
33
+
34
+ if (!config.org) {
35
+ this.error('GitHub org not configured. Run `dvmi init` to set up your environment.')
36
+ }
37
+
38
+ const spinner = isJson ? null : ora(`Searching for "${args.term}"...`).start()
39
+ const results = await searchCode(config.org, args.term, {
40
+ language: flags.language,
41
+ repo: flags.repo,
42
+ limit: flags.limit,
43
+ })
44
+ spinner?.stop()
45
+
46
+ if (isJson) return { results, total: results.length }
47
+
48
+ if (results.length === 0) {
49
+ this.log(chalk.yellow(`No results found for "${args.term}" in the organization.`))
50
+ return { results: [], total: 0 }
51
+ }
52
+
53
+ this.log(chalk.bold(`\n${results.length} result(s) for "${args.term}":\n`))
54
+ this.log(renderTable(results, [
55
+ { header: 'Repo', key: 'repo', width: 25 },
56
+ { header: 'File', key: 'file', width: 45 },
57
+ { header: 'Match', key: 'match' },
58
+ ]))
59
+
60
+ return { results, total: results.length }
61
+ }
62
+ }
@@ -0,0 +1,131 @@
1
+ import { Command, Flags } from '@oclif/core'
2
+ import chalk from 'chalk'
3
+ import ora from 'ora'
4
+ import { getTasks, getTasksByList, isAuthenticated } from '../../services/clickup.js'
5
+ import { loadConfig } from '../../services/config.js'
6
+ import { renderTable } from '../../formatters/table.js'
7
+
8
+ export default class TasksAssigned extends Command {
9
+ static description = 'Task ClickUp assegnati a te (alias di tasks list)'
10
+
11
+ static examples = [
12
+ '<%= config.bin %> tasks assigned',
13
+ '<%= config.bin %> tasks assigned --status in_progress',
14
+ '<%= config.bin %> tasks assigned --search "bug fix"',
15
+ '<%= config.bin %> tasks assigned --list-id 12345',
16
+ '<%= config.bin %> tasks assigned --json',
17
+ ]
18
+
19
+ static enableJsonFlag = true
20
+
21
+ static flags = {
22
+ status: Flags.string({ description: 'Filtra per status (open, in_progress, done)' }),
23
+ search: Flags.string({
24
+ char: 's',
25
+ description: 'Cerca nel titolo del task (case-insensitive)',
26
+ }),
27
+ 'list-id': Flags.string({
28
+ description: "ID della lista ClickUp (visibile nell'URL della lista)",
29
+ }),
30
+ }
31
+
32
+ async run() {
33
+ const { flags } = await this.parse(TasksAssigned)
34
+ const isJson = flags.json
35
+ const config = await loadConfig()
36
+
37
+ // Check auth
38
+ if (!(await isAuthenticated())) {
39
+ this.error('ClickUp not authenticated. Run `dvmi init` to configure ClickUp.')
40
+ }
41
+
42
+ // Ensure team ID is configured
43
+ const teamId = config.clickup?.teamId
44
+ if (!teamId && !flags['list-id']) {
45
+ this.error('ClickUp team ID not configured. Run `dvmi init` to configure ClickUp.')
46
+ }
47
+
48
+ const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching tasks...') }).start()
49
+
50
+ /** @param {number} count */
51
+ const onProgress = (count) => {
52
+ if (spinner) spinner.text = chalk.hex('#FF6B2B')(`Fetching tasks... (${count})`)
53
+ }
54
+
55
+ let tasks
56
+ if (flags['list-id']) {
57
+ tasks = await getTasksByList(flags['list-id'], { status: flags.status }, onProgress).catch((err) => {
58
+ spinner?.stop()
59
+ this.error(err.message)
60
+ })
61
+ } else {
62
+ tasks = await getTasks(/** @type {string} */ (teamId), { status: flags.status }, onProgress)
63
+ }
64
+ spinner?.stop()
65
+
66
+ // Apply search filter
67
+ const searchQuery = flags.search?.toLowerCase()
68
+ const filtered = searchQuery
69
+ ? tasks.filter((t) => t.name.toLowerCase().includes(searchQuery))
70
+ : tasks
71
+
72
+ if (isJson) return { tasks: filtered }
73
+
74
+ if (tasks.length === 0) {
75
+ this.log(chalk.dim('No tasks assigned to you.'))
76
+ return { tasks: [] }
77
+ }
78
+
79
+ if (filtered.length === 0) {
80
+ this.log(chalk.dim('No tasks matching filters.'))
81
+ return { tasks: [] }
82
+ }
83
+
84
+ // Priority label + color
85
+ const priorityLabel = (p) => ['', 'URGENT', 'HIGH', 'NORMAL', 'LOW'][p] ?? String(p)
86
+ const priorityColor = (label) => {
87
+ if (label === 'URGENT') return chalk.red.bold(label)
88
+ if (label === 'HIGH') return chalk.yellow(label)
89
+ if (label === 'NORMAL') return chalk.white(label)
90
+ if (label === 'LOW') return chalk.dim(label)
91
+ return label
92
+ }
93
+
94
+ // Status color
95
+ const statusColor = (status) => {
96
+ const s = status.toLowerCase()
97
+ if (s.includes('done') || s.includes('complet') || s.includes('closed')) return chalk.green(status)
98
+ if (s.includes('progress') || s.includes('active') || s.includes('open')) return chalk.cyan(status)
99
+ if (s.includes('block') || s.includes('review') || s.includes('wait')) return chalk.yellow(status)
100
+ return chalk.dim(status)
101
+ }
102
+
103
+ // Summary header
104
+ const filterInfo = [
105
+ flags.status && chalk.dim(`status: ${chalk.white(flags.status)}`),
106
+ flags.search && chalk.dim(`search: ${chalk.white(`"${flags.search}"`)}`),
107
+ flags['list-id'] && chalk.dim(`list-id: ${chalk.white(flags['list-id'])}`),
108
+ ].filter(Boolean).join(chalk.dim(' · '))
109
+
110
+ this.log(
111
+ chalk.bold('\nYour assigned tasks') +
112
+ (filterInfo ? chalk.dim(' — ') + filterInfo : '') +
113
+ chalk.dim(` (${filtered.length}${filtered.length < tasks.length ? `/${tasks.length}` : ''})`) +
114
+ '\n',
115
+ )
116
+
117
+ this.log(renderTable(filtered, [
118
+ { header: 'ID', key: 'id', width: 10 },
119
+ { header: 'Link', key: 'url', width: 42, format: (v) => v ?? '—' },
120
+ { header: 'Priority', key: 'priority', width: 8, format: (v) => priorityLabel(Number(v)), colorize: priorityColor },
121
+ { header: 'Status', key: 'status', width: 15, colorize: statusColor },
122
+ { header: 'Due', key: 'dueDate', width: 12, format: (v) => v ?? '—' },
123
+ { header: 'Lista', key: 'listName', width: 20, format: (v) => v ?? '—' },
124
+ { header: 'Cartella', key: 'folderName', width: 20, format: (v) => v ?? '—' },
125
+ { header: 'Description', key: 'name', width: 55 },
126
+ ]))
127
+
128
+ this.log('')
129
+ return { tasks: filtered }
130
+ }
131
+ }
@@ -0,0 +1,133 @@
1
+ import { Command, Flags } from '@oclif/core'
2
+ import chalk from 'chalk'
3
+ import ora from 'ora'
4
+ import { getTasks, getTasksByList, isAuthenticated } from '../../services/clickup.js'
5
+ import { loadConfig } from '../../services/config.js'
6
+ import { renderTable } from '../../formatters/table.js'
7
+
8
+ export default class TasksList extends Command {
9
+ static description = 'Task ClickUp assegnati a te'
10
+
11
+ static examples = [
12
+ '<%= config.bin %> tasks list',
13
+ '<%= config.bin %> tasks list --status in_progress',
14
+ '<%= config.bin %> tasks list --search "bug fix"',
15
+ '<%= config.bin %> tasks list --status open --search "login"',
16
+ '<%= config.bin %> tasks list --list-id 12345',
17
+ '<%= config.bin %> tasks list --list-id 12345 --status "in progress"',
18
+ '<%= config.bin %> tasks list --json',
19
+ ]
20
+
21
+ static enableJsonFlag = true
22
+
23
+ static flags = {
24
+ status: Flags.string({ description: 'Filtra per status (open, in_progress, done)' }),
25
+ search: Flags.string({
26
+ char: 's',
27
+ description: 'Cerca nel titolo del task (case-insensitive)',
28
+ }),
29
+ 'list-id': Flags.string({
30
+ description: "ID della lista ClickUp (visibile nell'URL della lista)",
31
+ }),
32
+ }
33
+
34
+ async run() {
35
+ const { flags } = await this.parse(TasksList)
36
+ const isJson = flags.json
37
+ const config = await loadConfig()
38
+
39
+ // Check auth
40
+ if (!(await isAuthenticated())) {
41
+ this.error('ClickUp not authenticated. Run `dvmi init` to configure ClickUp.')
42
+ }
43
+
44
+ // Ensure team ID is configured
45
+ const teamId = config.clickup?.teamId
46
+ if (!teamId && !flags['list-id']) {
47
+ this.error('ClickUp team ID not configured. Run `dvmi init` to configure ClickUp.')
48
+ }
49
+
50
+ const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching tasks...') }).start()
51
+
52
+ /** @param {number} count */
53
+ const onProgress = (count) => {
54
+ if (spinner) spinner.text = chalk.hex('#FF6B2B')(`Fetching tasks... (${count})`)
55
+ }
56
+
57
+ let tasks
58
+ if (flags['list-id']) {
59
+ tasks = await getTasksByList(flags['list-id'], { status: flags.status }, onProgress).catch((err) => {
60
+ spinner?.stop()
61
+ this.error(err.message)
62
+ })
63
+ } else {
64
+ tasks = await getTasks(/** @type {string} */ (teamId), { status: flags.status }, onProgress)
65
+ }
66
+ spinner?.stop()
67
+
68
+ // Apply search filter
69
+ const searchQuery = flags.search?.toLowerCase()
70
+ const filtered = searchQuery
71
+ ? tasks.filter((t) => t.name.toLowerCase().includes(searchQuery))
72
+ : tasks
73
+
74
+ if (isJson) return { tasks: filtered }
75
+
76
+ if (tasks.length === 0) {
77
+ this.log(chalk.dim('No tasks assigned to you.'))
78
+ return { tasks: [] }
79
+ }
80
+
81
+ if (filtered.length === 0) {
82
+ this.log(chalk.dim(`No tasks matching filters.`))
83
+ return { tasks: [] }
84
+ }
85
+
86
+ // Priority label + color
87
+ const priorityLabel = (p) => ['', 'URGENT', 'HIGH', 'NORMAL', 'LOW'][p] ?? String(p)
88
+ const priorityColor = (label) => {
89
+ if (label === 'URGENT') return chalk.red.bold(label)
90
+ if (label === 'HIGH') return chalk.yellow(label)
91
+ if (label === 'NORMAL') return chalk.white(label)
92
+ if (label === 'LOW') return chalk.dim(label)
93
+ return label
94
+ }
95
+
96
+ // Status color
97
+ const statusColor = (status) => {
98
+ const s = status.toLowerCase()
99
+ if (s.includes('done') || s.includes('complet') || s.includes('closed')) return chalk.green(status)
100
+ if (s.includes('progress') || s.includes('active') || s.includes('open')) return chalk.cyan(status)
101
+ if (s.includes('block') || s.includes('review') || s.includes('wait')) return chalk.yellow(status)
102
+ return chalk.dim(status)
103
+ }
104
+
105
+ // Summary header
106
+ const filterInfo = [
107
+ flags.status && chalk.dim(`status: ${chalk.white(flags.status)}`),
108
+ flags.search && chalk.dim(`search: ${chalk.white(`"${flags.search}"`)}`),
109
+ flags['list-id'] && chalk.dim(`list-id: ${chalk.white(flags['list-id'])}`),
110
+ ].filter(Boolean).join(chalk.dim(' · '))
111
+
112
+ this.log(
113
+ chalk.bold('\nYour tasks') +
114
+ (filterInfo ? chalk.dim(' — ') + filterInfo : '') +
115
+ chalk.dim(` (${filtered.length}${filtered.length < tasks.length ? `/${tasks.length}` : ''})`) +
116
+ '\n',
117
+ )
118
+
119
+ this.log(renderTable(filtered, [
120
+ { header: 'ID', key: 'id', width: 10 },
121
+ { header: 'Link', key: 'url', width: 42, format: (v) => v ?? '—' },
122
+ { header: 'Priority', key: 'priority', width: 8, format: (v) => priorityLabel(Number(v)), colorize: priorityColor },
123
+ { header: 'Status', key: 'status', width: 15, colorize: statusColor },
124
+ { header: 'Due', key: 'dueDate', width: 12, format: (v) => v ?? '—' },
125
+ { header: 'Lista', key: 'listName', width: 20, format: (v) => v ?? '—' },
126
+ { header: 'Cartella', key: 'folderName', width: 20, format: (v) => v ?? '—' },
127
+ { header: 'Description', key: 'name', width: 55 },
128
+ ]))
129
+
130
+ this.log('')
131
+ return { tasks: filtered }
132
+ }
133
+ }
@@ -0,0 +1,73 @@
1
+ import { Command } from '@oclif/core'
2
+ import chalk from 'chalk'
3
+ import ora from 'ora'
4
+ import { getTasksToday, isAuthenticated } from '../../services/clickup.js'
5
+ import { loadConfig } from '../../services/config.js'
6
+ import { renderTable } from '../../formatters/table.js'
7
+
8
+ /**
9
+ * Return today's date as a local YYYY-MM-DD string.
10
+ * @returns {string}
11
+ */
12
+ function localTodayString() {
13
+ const d = new Date()
14
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
15
+ }
16
+
17
+ export default class TasksToday extends Command {
18
+ static description = 'Task in lavorazione oggi: data odierna nel range [startDate, dueDate]. Include task scaduti non conclusi.'
19
+
20
+ static examples = [
21
+ '<%= config.bin %> tasks today',
22
+ '<%= config.bin %> tasks today --json',
23
+ ]
24
+
25
+ static enableJsonFlag = true
26
+
27
+ async run() {
28
+ const { flags } = await this.parse(TasksToday)
29
+ const isJson = flags.json
30
+ const config = await loadConfig()
31
+
32
+ if (!(await isAuthenticated())) {
33
+ this.error('ClickUp not authenticated. Run `dvmi init` to configure ClickUp.')
34
+ }
35
+
36
+ const teamId = config.clickup?.teamId
37
+ if (!teamId) this.error('ClickUp team ID not configured. Run `dvmi init` to configure ClickUp.')
38
+
39
+ const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching today\'s tasks...') }).start()
40
+ const tasks = await getTasksToday(teamId)
41
+ spinner?.stop()
42
+
43
+ if (isJson) return { tasks }
44
+
45
+ if (tasks.length === 0) {
46
+ this.log(chalk.dim('No tasks for today.'))
47
+ this.log(chalk.dim('Check `dvmi tasks list` for all assigned tasks.'))
48
+ return { tasks: [] }
49
+ }
50
+
51
+ const today = localTodayString()
52
+
53
+ this.log(chalk.bold('\nToday\'s tasks:\n'))
54
+ this.log(renderTable(tasks, [
55
+ { header: 'Title', key: 'name', width: 45 },
56
+ { header: 'Status', key: 'status', width: 15 },
57
+ {
58
+ header: 'Due',
59
+ key: 'dueDate',
60
+ width: 12,
61
+ format: (v) => v ?? '—',
62
+ colorize: (v) => {
63
+ if (!v) return chalk.dim('—')
64
+ if (v < today) return chalk.red.bold(v)
65
+ return v
66
+ },
67
+ },
68
+ { header: 'Link', key: 'url', format: (v) => v ?? '—' },
69
+ ]))
70
+
71
+ return { tasks }
72
+ }
73
+ }
@@ -0,0 +1,52 @@
1
+ import { Command } from '@oclif/core'
2
+ import chalk from 'chalk'
3
+ import ora from 'ora'
4
+ import { checkForUpdate } from '../services/version-check.js'
5
+ import { exec } from '../services/shell.js'
6
+
7
+ export default class Upgrade extends Command {
8
+ static description = 'Aggiorna la CLI all\'ultima versione disponibile'
9
+
10
+ static examples = [
11
+ '<%= config.bin %> upgrade',
12
+ '<%= config.bin %> upgrade --json',
13
+ ]
14
+
15
+ static enableJsonFlag = true
16
+
17
+ async run() {
18
+ const { flags } = await this.parse(Upgrade)
19
+ const isJson = flags.json
20
+
21
+ const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking for updates...') }).start()
22
+ const { hasUpdate, current, latest } = await checkForUpdate({ force: true })
23
+ spinner?.stop()
24
+
25
+ if (!hasUpdate) {
26
+ const msg = `You're already on the latest version (${current})`
27
+ if (isJson) return { currentVersion: current, latestVersion: latest, updated: false }
28
+ this.log(chalk.green('✓') + ' ' + msg)
29
+ return
30
+ }
31
+
32
+ if (!isJson) {
33
+ this.log(`Updating from ${chalk.yellow(current)} to ${chalk.green(latest)}`)
34
+ }
35
+
36
+ const updateSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Installing update...') }).start()
37
+ // Non passare --registry globale: verrebbe usato anche per le dipendenze.
38
+ // ~/.npmrc ha già devvami:registry per il solo scope corretto.
39
+ const result = await exec('npm', ['install', '-g', `devvami@${latest}`])
40
+ if (result.exitCode !== 0) {
41
+ updateSpinner?.fail('Update failed')
42
+ this.error(`Update failed: ${result.stderr}`)
43
+ }
44
+ updateSpinner?.succeed(`Updated to ${latest}`)
45
+
46
+ const response = { currentVersion: current, latestVersion: latest, updated: true }
47
+ if (isJson) return response
48
+
49
+ this.log(chalk.green('✓') + ` Successfully updated to ${latest}`)
50
+ return response
51
+ }
52
+ }