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,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
|
+
}
|