devvami 1.4.1 → 1.5.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/README.md +7 -0
- package/oclif.manifest.json +41 -1
- package/package.json +2 -1
- package/src/commands/auth/login.js +20 -16
- package/src/commands/changelog.js +12 -12
- package/src/commands/costs/get.js +14 -24
- package/src/commands/costs/trend.js +13 -24
- package/src/commands/create/repo.js +72 -54
- package/src/commands/docs/list.js +29 -25
- package/src/commands/docs/projects.js +58 -24
- package/src/commands/docs/read.js +56 -39
- package/src/commands/docs/search.js +37 -25
- package/src/commands/doctor.js +37 -35
- package/src/commands/dotfiles/add.js +51 -39
- package/src/commands/dotfiles/setup.js +62 -33
- package/src/commands/dotfiles/status.js +18 -18
- package/src/commands/dotfiles/sync.js +62 -46
- package/src/commands/init.js +143 -132
- package/src/commands/logs/index.js +10 -16
- package/src/commands/open.js +12 -12
- package/src/commands/pipeline/logs.js +8 -11
- package/src/commands/pipeline/rerun.js +21 -16
- package/src/commands/pipeline/status.js +28 -24
- package/src/commands/pr/create.js +40 -27
- package/src/commands/pr/detail.js +9 -7
- package/src/commands/pr/review.js +18 -19
- package/src/commands/pr/status.js +27 -21
- package/src/commands/prompts/browse.js +15 -15
- package/src/commands/prompts/download.js +15 -16
- package/src/commands/prompts/install-speckit.js +11 -12
- package/src/commands/prompts/list.js +12 -12
- package/src/commands/prompts/run.js +16 -19
- package/src/commands/repo/list.js +57 -41
- package/src/commands/search.js +20 -18
- package/src/commands/security/setup.js +38 -34
- package/src/commands/sync-config-ai/index.js +143 -0
- package/src/commands/tasks/assigned.js +43 -33
- package/src/commands/tasks/list.js +43 -33
- package/src/commands/tasks/today.js +32 -30
- package/src/commands/upgrade.js +18 -17
- package/src/commands/vuln/detail.js +8 -8
- package/src/commands/vuln/scan.js +95 -21
- package/src/commands/vuln/search.js +23 -18
- package/src/commands/welcome.js +2 -2
- package/src/commands/whoami.js +19 -23
- package/src/formatters/ai-config.js +127 -0
- package/src/formatters/charts.js +6 -23
- package/src/formatters/cost.js +1 -7
- package/src/formatters/dotfiles.js +48 -19
- package/src/formatters/markdown.js +11 -6
- package/src/formatters/openapi.js +7 -9
- package/src/formatters/prompts.js +69 -78
- package/src/formatters/security.js +2 -2
- package/src/formatters/status.js +1 -1
- package/src/formatters/table.js +1 -3
- package/src/formatters/vuln.js +33 -20
- package/src/help.js +162 -164
- package/src/hooks/init.js +1 -3
- package/src/hooks/postrun.js +5 -7
- package/src/index.js +1 -1
- package/src/services/ai-config-store.js +318 -0
- package/src/services/ai-env-deployer.js +444 -0
- package/src/services/ai-env-scanner.js +242 -0
- package/src/services/audit-detector.js +2 -2
- package/src/services/audit-runner.js +40 -31
- package/src/services/auth.js +9 -9
- package/src/services/awesome-copilot.js +7 -4
- package/src/services/aws-costs.js +22 -22
- package/src/services/clickup.js +26 -26
- package/src/services/cloudwatch-logs.js +5 -9
- package/src/services/config.js +13 -13
- package/src/services/docs.js +19 -20
- package/src/services/dotfiles.js +149 -51
- package/src/services/github.js +22 -24
- package/src/services/nvd.js +21 -31
- package/src/services/platform.js +2 -2
- package/src/services/prompts.js +23 -35
- package/src/services/security.js +135 -61
- package/src/services/shell.js +4 -4
- package/src/services/skills-sh.js +3 -9
- package/src/services/speckit.js +4 -7
- package/src/services/version-check.js +10 -10
- package/src/types.js +85 -0
- package/src/utils/aws-vault.js +18 -41
- package/src/utils/banner.js +5 -7
- package/src/utils/errors.js +42 -46
- package/src/utils/frontmatter.js +4 -4
- package/src/utils/gradient.js +18 -16
- package/src/utils/open-browser.js +3 -3
- package/src/utils/tui/form.js +1006 -0
- package/src/utils/tui/modal.js +15 -14
- package/src/utils/tui/navigable-table.js +25 -17
- package/src/utils/tui/tab-tui.js +800 -0
- package/src/utils/typewriter.js +3 -3
- package/src/utils/welcome.js +18 -21
- package/src/validators/repo-name.js +2 -2
package/README.md
CHANGED
|
@@ -284,6 +284,13 @@ Built with:
|
|
|
284
284
|
- [ora](https://github.com/sindresorhus/ora) — Spinners
|
|
285
285
|
- [keytar](https://github.com/atom/node-keytar) — Secure credentials
|
|
286
286
|
- [@inquirer/prompts](https://github.com/SBoudrias/Inquirer.js) — Interactive prompts
|
|
287
|
+
- [execa](https://github.com/sindresorhus/execa) — Process execution
|
|
288
|
+
- [js-yaml](https://github.com/nodeca/js-yaml) — YAML parsing
|
|
289
|
+
- [marked](https://github.com/markedjs/marked) — Markdown parsing
|
|
290
|
+
- [figlet](https://github.com/patorjk/figlet.js) — ASCII art banners
|
|
291
|
+
- [open](https://github.com/sindresorhus/open) — Browser/app launcher
|
|
292
|
+
- [@aws-sdk](https://github.com/aws/aws-sdk-js-v3) — AWS services integration
|
|
293
|
+
- [pako](https://github.com/nodeca/pako) — Compression utilities
|
|
287
294
|
|
|
288
295
|
---
|
|
289
296
|
|
package/oclif.manifest.json
CHANGED
|
@@ -1742,6 +1742,46 @@
|
|
|
1742
1742
|
"list.js"
|
|
1743
1743
|
]
|
|
1744
1744
|
},
|
|
1745
|
+
"sync-config-ai": {
|
|
1746
|
+
"aliases": [],
|
|
1747
|
+
"args": {},
|
|
1748
|
+
"description": "Manage AI coding tool configurations across environments via TUI",
|
|
1749
|
+
"examples": [
|
|
1750
|
+
"<%= config.bin %> sync-config-ai",
|
|
1751
|
+
"<%= config.bin %> sync-config-ai --json"
|
|
1752
|
+
],
|
|
1753
|
+
"flags": {
|
|
1754
|
+
"json": {
|
|
1755
|
+
"description": "Format output as json.",
|
|
1756
|
+
"helpGroup": "GLOBAL",
|
|
1757
|
+
"name": "json",
|
|
1758
|
+
"allowNo": false,
|
|
1759
|
+
"type": "boolean"
|
|
1760
|
+
},
|
|
1761
|
+
"help": {
|
|
1762
|
+
"char": "h",
|
|
1763
|
+
"description": "Show CLI help.",
|
|
1764
|
+
"name": "help",
|
|
1765
|
+
"allowNo": false,
|
|
1766
|
+
"type": "boolean"
|
|
1767
|
+
}
|
|
1768
|
+
},
|
|
1769
|
+
"hasDynamicHelp": false,
|
|
1770
|
+
"hiddenAliases": [],
|
|
1771
|
+
"id": "sync-config-ai",
|
|
1772
|
+
"pluginAlias": "devvami",
|
|
1773
|
+
"pluginName": "devvami",
|
|
1774
|
+
"pluginType": "core",
|
|
1775
|
+
"strict": true,
|
|
1776
|
+
"enableJsonFlag": true,
|
|
1777
|
+
"isESM": true,
|
|
1778
|
+
"relativePath": [
|
|
1779
|
+
"src",
|
|
1780
|
+
"commands",
|
|
1781
|
+
"sync-config-ai",
|
|
1782
|
+
"index.js"
|
|
1783
|
+
]
|
|
1784
|
+
},
|
|
1745
1785
|
"security:setup": {
|
|
1746
1786
|
"aliases": [],
|
|
1747
1787
|
"args": {},
|
|
@@ -2120,5 +2160,5 @@
|
|
|
2120
2160
|
]
|
|
2121
2161
|
}
|
|
2122
2162
|
},
|
|
2123
|
-
"version": "1.
|
|
2163
|
+
"version": "1.5.0"
|
|
2124
2164
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devvami",
|
|
3
3
|
"description": "DevEx CLI for developers and teams — manage repos, PRs, pipelines, tasks, and costs from the terminal",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.5.0",
|
|
5
5
|
"author": "",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -139,6 +139,7 @@
|
|
|
139
139
|
"test:integration": "vitest run --project integration",
|
|
140
140
|
"test:watch": "vitest",
|
|
141
141
|
"test:coverage": "vitest run --coverage",
|
|
142
|
+
"pretest": "oclif manifest",
|
|
142
143
|
"prepack": "oclif manifest",
|
|
143
144
|
"postpack": "shx rm -f oclif.manifest.json",
|
|
144
145
|
"prepare": "lefthook install"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Command, Flags} from '@oclif/core'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import ora from 'ora'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import {checkGitHubAuth, loginGitHub, checkAWSAuth, loginAWS} from '../../services/auth.js'
|
|
5
|
+
import {loadConfig} from '../../services/config.js'
|
|
6
6
|
|
|
7
7
|
export default class AuthLogin extends Command {
|
|
8
8
|
static description = 'Autenticazione centralizzata GitHub + AWS'
|
|
@@ -16,36 +16,38 @@ export default class AuthLogin extends Command {
|
|
|
16
16
|
static enableJsonFlag = true
|
|
17
17
|
|
|
18
18
|
static flags = {
|
|
19
|
-
github: Flags.boolean({
|
|
20
|
-
aws: Flags.boolean({
|
|
21
|
-
verbose: Flags.boolean({
|
|
19
|
+
github: Flags.boolean({description: 'Solo autenticazione GitHub', default: false}),
|
|
20
|
+
aws: Flags.boolean({description: 'Solo autenticazione AWS', default: false}),
|
|
21
|
+
verbose: Flags.boolean({description: 'Output dettagliato', default: false}),
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
async run() {
|
|
25
|
-
const {
|
|
25
|
+
const {flags} = await this.parse(AuthLogin)
|
|
26
26
|
const isJson = flags.json
|
|
27
27
|
const doGitHub = !flags.aws || flags.github
|
|
28
28
|
const doAWS = !flags.github || flags.aws
|
|
29
29
|
|
|
30
|
-
const result = {
|
|
30
|
+
const result = {github: null, aws: null}
|
|
31
31
|
|
|
32
32
|
// GitHub auth
|
|
33
33
|
if (doGitHub) {
|
|
34
|
-
const spinner = isJson
|
|
34
|
+
const spinner = isJson
|
|
35
|
+
? null
|
|
36
|
+
: ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking GitHub auth...')}).start()
|
|
35
37
|
let ghStatus = await checkGitHubAuth()
|
|
36
38
|
|
|
37
39
|
if (ghStatus.authenticated) {
|
|
38
40
|
spinner?.succeed(`GitHub: already authenticated as @${ghStatus.username}`)
|
|
39
|
-
result.github = {
|
|
41
|
+
result.github = {status: 'ok', username: ghStatus.username, org: ''}
|
|
40
42
|
} else {
|
|
41
43
|
if (spinner) spinner.text = 'Logging in to GitHub...'
|
|
42
44
|
ghStatus = await loginGitHub()
|
|
43
45
|
if (ghStatus.authenticated) {
|
|
44
46
|
spinner?.succeed(`GitHub: authenticated as @${ghStatus.username}`)
|
|
45
|
-
result.github = {
|
|
47
|
+
result.github = {status: 'ok', username: ghStatus.username, org: ''}
|
|
46
48
|
} else {
|
|
47
49
|
spinner?.fail('GitHub authentication failed')
|
|
48
|
-
result.github = {
|
|
50
|
+
result.github = {status: 'error', error: ghStatus.error}
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
}
|
|
@@ -53,21 +55,23 @@ export default class AuthLogin extends Command {
|
|
|
53
55
|
// AWS auth
|
|
54
56
|
if (doAWS) {
|
|
55
57
|
const config = await loadConfig()
|
|
56
|
-
const spinner = isJson
|
|
58
|
+
const spinner = isJson
|
|
59
|
+
? null
|
|
60
|
+
: ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking AWS auth...')}).start()
|
|
57
61
|
let awsStatus = await checkAWSAuth()
|
|
58
62
|
|
|
59
63
|
if (awsStatus.authenticated) {
|
|
60
64
|
spinner?.succeed(`AWS: session active for account ${awsStatus.account}`)
|
|
61
|
-
result.aws = {
|
|
65
|
+
result.aws = {status: 'ok', account: awsStatus.account, role: awsStatus.role}
|
|
62
66
|
} else {
|
|
63
67
|
if (spinner) spinner.text = 'Logging in to AWS via aws-vault...'
|
|
64
68
|
awsStatus = await loginAWS(config.awsProfile || 'default')
|
|
65
69
|
if (awsStatus.authenticated) {
|
|
66
70
|
spinner?.succeed(`AWS: logged in to account ${awsStatus.account}`)
|
|
67
|
-
result.aws = {
|
|
71
|
+
result.aws = {status: 'ok', account: awsStatus.account, role: awsStatus.role}
|
|
68
72
|
} else {
|
|
69
73
|
spinner?.fail('AWS authentication failed')
|
|
70
|
-
result.aws = {
|
|
74
|
+
result.aws = {status: 'error', error: awsStatus.error}
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import {Command, Flags} from '@oclif/core'
|
|
2
|
+
import {writeFile} from 'node:fs/promises'
|
|
3
|
+
import {exec} from '../services/shell.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Parse a conventional commit message.
|
|
@@ -10,7 +10,7 @@ import { exec } from '../services/shell.js'
|
|
|
10
10
|
function parseConventionalCommit(message) {
|
|
11
11
|
const match = message.match(/^(\w+)(?:\(([^)]+)\))?!?: (.+)/)
|
|
12
12
|
if (!match) return null
|
|
13
|
-
return {
|
|
13
|
+
return {type: match[1], scope: match[2] ?? '', description: match[3]}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export default class Changelog extends Command {
|
|
@@ -25,13 +25,13 @@ export default class Changelog extends Command {
|
|
|
25
25
|
static enableJsonFlag = true
|
|
26
26
|
|
|
27
27
|
static flags = {
|
|
28
|
-
from: Flags.string({
|
|
29
|
-
to: Flags.string({
|
|
30
|
-
output: Flags.string({
|
|
28
|
+
from: Flags.string({description: 'Tag o commit di partenza (default: ultimo tag)'}),
|
|
29
|
+
to: Flags.string({description: 'Commit finale (default: HEAD)', default: 'HEAD'}),
|
|
30
|
+
output: Flags.string({description: 'Scrivi su file (default: stdout)'}),
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
async run() {
|
|
34
|
-
const {
|
|
34
|
+
const {flags} = await this.parse(Changelog)
|
|
35
35
|
const isJson = flags.json
|
|
36
36
|
|
|
37
37
|
// Determine from ref
|
|
@@ -51,13 +51,13 @@ export default class Changelog extends Command {
|
|
|
51
51
|
const lines = logResult.stdout.split('\n').filter(Boolean)
|
|
52
52
|
|
|
53
53
|
/** @type {Record<string, Array<{ message: string, hash: string }>>} */
|
|
54
|
-
const sections = {
|
|
54
|
+
const sections = {feat: [], fix: [], chore: [], docs: [], refactor: [], test: [], other: []}
|
|
55
55
|
|
|
56
56
|
for (const line of lines) {
|
|
57
57
|
const [message, hash] = line.split('|')
|
|
58
58
|
const parsed = parseConventionalCommit(message)
|
|
59
59
|
const type = parsed?.type ?? 'other'
|
|
60
|
-
const entry = {
|
|
60
|
+
const entry = {message: message.trim(), hash: hash?.slice(0, 7) ?? ''}
|
|
61
61
|
if (type in sections) {
|
|
62
62
|
sections[type].push(entry)
|
|
63
63
|
} else {
|
|
@@ -65,7 +65,7 @@ export default class Changelog extends Command {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
if (isJson) return {
|
|
68
|
+
if (isJson) return {from: from || 'beginning', to: flags.to, sections}
|
|
69
69
|
|
|
70
70
|
// Build markdown
|
|
71
71
|
const title = `## [Unreleased]${from ? ` (since ${from})` : ''}`
|
|
@@ -97,6 +97,6 @@ export default class Changelog extends Command {
|
|
|
97
97
|
this.log(output)
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
return {
|
|
100
|
+
return {from: from || 'beginning', to: flags.to, sections}
|
|
101
101
|
}
|
|
102
102
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {Command, Args, Flags} from '@oclif/core'
|
|
2
|
+
import {input} from '@inquirer/prompts'
|
|
3
3
|
import ora from 'ora'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import {getServiceCosts} from '../../services/aws-costs.js'
|
|
5
|
+
import {loadConfig} from '../../services/config.js'
|
|
6
|
+
import {formatCostTable, calculateTotal} from '../../formatters/cost.js'
|
|
7
|
+
import {DvmiError} from '../../utils/errors.js'
|
|
8
8
|
import {
|
|
9
9
|
awsVaultPrefix,
|
|
10
10
|
isAwsVaultSession,
|
|
@@ -28,7 +28,7 @@ export default class CostsGet extends Command {
|
|
|
28
28
|
static enableJsonFlag = true
|
|
29
29
|
|
|
30
30
|
static args = {
|
|
31
|
-
service: Args.string({
|
|
31
|
+
service: Args.string({description: 'Service name (used to derive tag filter from config)', required: false}),
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
static flags = {
|
|
@@ -48,18 +48,14 @@ export default class CostsGet extends Command {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
async run() {
|
|
51
|
-
const {
|
|
51
|
+
const {args, flags} = await this.parse(CostsGet)
|
|
52
52
|
const isJson = flags.json
|
|
53
53
|
const isInteractive = !isJson && process.stdout.isTTY && process.env.CI !== 'true'
|
|
54
54
|
const groupBy = /** @type {'service'|'tag'|'both'} */ (flags['group-by'])
|
|
55
55
|
|
|
56
56
|
const config = await loadConfig()
|
|
57
57
|
|
|
58
|
-
if (
|
|
59
|
-
isInteractive &&
|
|
60
|
-
!isAwsVaultSession() &&
|
|
61
|
-
process.env.DVMI_AWS_VAULT_REEXEC !== '1'
|
|
62
|
-
) {
|
|
58
|
+
if (isInteractive && !isAwsVaultSession() && process.env.DVMI_AWS_VAULT_REEXEC !== '1') {
|
|
63
59
|
const profile = await input({
|
|
64
60
|
message: 'AWS profile (aws-vault):',
|
|
65
61
|
default: config.awsProfile || process.env.AWS_VAULT || 'default',
|
|
@@ -91,19 +87,16 @@ export default class CostsGet extends Command {
|
|
|
91
87
|
|
|
92
88
|
// Validate: tag key required when grouping by tag or both
|
|
93
89
|
if ((groupBy === 'tag' || groupBy === 'both') && !tagKey) {
|
|
94
|
-
throw new DvmiError(
|
|
95
|
-
'No tag key available.',
|
|
96
|
-
'Pass --tag-key or configure projectTags in dvmi config.',
|
|
97
|
-
)
|
|
90
|
+
throw new DvmiError('No tag key available.', 'Pass --tag-key or configure projectTags in dvmi config.')
|
|
98
91
|
}
|
|
99
92
|
|
|
100
93
|
const serviceArg = args.service ?? 'all'
|
|
101
|
-
const tags = config.projectTags ?? (args.service ? {
|
|
94
|
+
const tags = config.projectTags ?? (args.service ? {project: args.service} : {})
|
|
102
95
|
|
|
103
96
|
const spinner = isJson ? null : ora(`Fetching costs...`).start()
|
|
104
97
|
|
|
105
98
|
try {
|
|
106
|
-
const {
|
|
99
|
+
const {entries, period} = await getServiceCosts(
|
|
107
100
|
serviceArg,
|
|
108
101
|
tags,
|
|
109
102
|
/** @type {any} */ (flags.period),
|
|
@@ -119,7 +112,7 @@ export default class CostsGet extends Command {
|
|
|
119
112
|
tagKey: tagKey ?? null,
|
|
120
113
|
period,
|
|
121
114
|
items: entries,
|
|
122
|
-
total: {
|
|
115
|
+
total: {amount: total, unit: 'USD'},
|
|
123
116
|
}
|
|
124
117
|
|
|
125
118
|
if (isJson) return result
|
|
@@ -156,10 +149,7 @@ export default class CostsGet extends Command {
|
|
|
156
149
|
}
|
|
157
150
|
|
|
158
151
|
const prefix = awsVaultPrefix(config)
|
|
159
|
-
this.error(
|
|
160
|
-
`No AWS credentials. Use: ${prefix}dvmi costs get` +
|
|
161
|
-
(args.service ? ` ${args.service}` : ''),
|
|
162
|
-
)
|
|
152
|
+
this.error(`No AWS credentials. Use: ${prefix}dvmi costs get` + (args.service ? ` ${args.service}` : ''))
|
|
163
153
|
}
|
|
164
154
|
throw err
|
|
165
155
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {Command, Flags} from '@oclif/core'
|
|
2
|
+
import {input} from '@inquirer/prompts'
|
|
3
3
|
import ora from 'ora'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import {getTrendCosts, getTwoMonthPeriod} from '../../services/aws-costs.js'
|
|
5
|
+
import {loadConfig} from '../../services/config.js'
|
|
6
|
+
import {barChart, lineChart} from '../../formatters/charts.js'
|
|
7
|
+
import {DvmiError} from '../../utils/errors.js'
|
|
8
8
|
import {
|
|
9
9
|
awsVaultPrefix,
|
|
10
10
|
isAwsVaultSession,
|
|
@@ -42,18 +42,14 @@ export default class CostsTrend extends Command {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
async run() {
|
|
45
|
-
const {
|
|
45
|
+
const {flags} = await this.parse(CostsTrend)
|
|
46
46
|
const isJson = flags.json
|
|
47
47
|
const isInteractive = !isJson && process.stdout.isTTY && process.env.CI !== 'true'
|
|
48
48
|
const groupBy = /** @type {'service'|'tag'|'both'} */ (flags['group-by'])
|
|
49
49
|
|
|
50
50
|
const config = await loadConfig()
|
|
51
51
|
|
|
52
|
-
if (
|
|
53
|
-
isInteractive &&
|
|
54
|
-
!isAwsVaultSession() &&
|
|
55
|
-
process.env.DVMI_AWS_VAULT_REEXEC !== '1'
|
|
56
|
-
) {
|
|
52
|
+
if (isInteractive && !isAwsVaultSession() && process.env.DVMI_AWS_VAULT_REEXEC !== '1') {
|
|
57
53
|
const profile = await input({
|
|
58
54
|
message: 'AWS profile (aws-vault):',
|
|
59
55
|
default: config.awsProfile || process.env.AWS_VAULT || 'default',
|
|
@@ -83,10 +79,7 @@ export default class CostsTrend extends Command {
|
|
|
83
79
|
const tagKey = flags['tag-key'] ?? configTagKey
|
|
84
80
|
|
|
85
81
|
if ((groupBy === 'tag' || groupBy === 'both') && !tagKey) {
|
|
86
|
-
throw new DvmiError(
|
|
87
|
-
'No tag key available.',
|
|
88
|
-
'Pass --tag-key or configure projectTags in dvmi config.',
|
|
89
|
-
)
|
|
82
|
+
throw new DvmiError('No tag key available.', 'Pass --tag-key or configure projectTags in dvmi config.')
|
|
90
83
|
}
|
|
91
84
|
|
|
92
85
|
const spinner = isJson ? null : ora('Fetching cost trend data...').start()
|
|
@@ -95,13 +88,13 @@ export default class CostsTrend extends Command {
|
|
|
95
88
|
const trendSeries = await getTrendCosts(groupBy, tagKey)
|
|
96
89
|
spinner?.stop()
|
|
97
90
|
|
|
98
|
-
const {
|
|
91
|
+
const {start, end} = getTwoMonthPeriod()
|
|
99
92
|
|
|
100
93
|
if (isJson) {
|
|
101
94
|
return {
|
|
102
95
|
groupBy,
|
|
103
96
|
tagKey: tagKey ?? null,
|
|
104
|
-
period: {
|
|
97
|
+
period: {start, end},
|
|
105
98
|
series: trendSeries,
|
|
106
99
|
}
|
|
107
100
|
}
|
|
@@ -113,9 +106,7 @@ export default class CostsTrend extends Command {
|
|
|
113
106
|
|
|
114
107
|
// Convert CostTrendSeries[] → ChartSeries[]
|
|
115
108
|
// All series must share the same label (date) axis — use the union of all dates
|
|
116
|
-
const allDates = Array.from(
|
|
117
|
-
new Set(trendSeries.flatMap((s) => s.points.map((p) => p.date))),
|
|
118
|
-
).sort()
|
|
109
|
+
const allDates = Array.from(new Set(trendSeries.flatMap((s) => s.points.map((p) => p.date)))).sort()
|
|
119
110
|
|
|
120
111
|
/** @type {import('../../formatters/charts.js').ChartSeries[]} */
|
|
121
112
|
const chartSeries = trendSeries.map((s) => {
|
|
@@ -128,9 +119,7 @@ export default class CostsTrend extends Command {
|
|
|
128
119
|
})
|
|
129
120
|
|
|
130
121
|
const title = `AWS Cost Trend — last 2 months (${start} → ${end})`
|
|
131
|
-
const rendered = flags.line
|
|
132
|
-
? lineChart(chartSeries, { title })
|
|
133
|
-
: barChart(chartSeries, { title })
|
|
122
|
+
const rendered = flags.line ? lineChart(chartSeries, {title}) : barChart(chartSeries, {title})
|
|
134
123
|
|
|
135
124
|
this.log(rendered)
|
|
136
125
|
} catch (err) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Command, Args, Flags} from '@oclif/core'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import ora from 'ora'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
4
|
+
import {input, confirm} from '@inquirer/prompts'
|
|
5
|
+
import {listTemplates, createFromTemplate, setBranchProtection, enableDependabot} from '../../services/github.js'
|
|
6
|
+
import {loadConfig} from '../../services/config.js'
|
|
7
|
+
import {validateRepoName} from '../../validators/repo-name.js'
|
|
8
|
+
import {renderTable} from '../../formatters/table.js'
|
|
9
|
+
import {exec} from '../../services/shell.js'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* @param {string} lang
|
|
@@ -16,15 +16,15 @@ function langColor(lang) {
|
|
|
16
16
|
const map = {
|
|
17
17
|
javascript: chalk.yellow,
|
|
18
18
|
typescript: chalk.blue,
|
|
19
|
-
python:
|
|
20
|
-
java:
|
|
21
|
-
go:
|
|
22
|
-
ruby:
|
|
23
|
-
rust:
|
|
24
|
-
kotlin:
|
|
25
|
-
swift:
|
|
26
|
-
php:
|
|
27
|
-
shell:
|
|
19
|
+
python: chalk.green,
|
|
20
|
+
java: chalk.red,
|
|
21
|
+
go: chalk.cyan,
|
|
22
|
+
ruby: chalk.magenta,
|
|
23
|
+
rust: chalk.hex('#CE422B'),
|
|
24
|
+
kotlin: chalk.hex('#7F52FF'),
|
|
25
|
+
swift: chalk.hex('#F05138'),
|
|
26
|
+
php: chalk.hex('#777BB4'),
|
|
27
|
+
shell: chalk.greenBright,
|
|
28
28
|
}
|
|
29
29
|
const fn = map[lang.toLowerCase()]
|
|
30
30
|
return fn ? fn(lang) : chalk.dim(lang)
|
|
@@ -43,21 +43,21 @@ export default class CreateRepo extends Command {
|
|
|
43
43
|
static enableJsonFlag = true
|
|
44
44
|
|
|
45
45
|
static args = {
|
|
46
|
-
template: Args.string({
|
|
46
|
+
template: Args.string({description: 'Nome del template', required: false}),
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
static flags = {
|
|
50
|
-
list:
|
|
51
|
-
search:
|
|
52
|
-
name:
|
|
53
|
-
description: Flags.string({
|
|
54
|
-
private:
|
|
55
|
-
public:
|
|
56
|
-
'dry-run':
|
|
50
|
+
list: Flags.boolean({description: 'Lista template disponibili', default: false}),
|
|
51
|
+
search: Flags.string({char: 's', description: 'Cerca in nome e descrizione dei template (case-insensitive)'}),
|
|
52
|
+
name: Flags.string({description: 'Nome del nuovo repository'}),
|
|
53
|
+
description: Flags.string({description: 'Descrizione del repository', default: ''}),
|
|
54
|
+
private: Flags.boolean({description: 'Repository privato (default)', default: true}),
|
|
55
|
+
public: Flags.boolean({description: 'Repository pubblico', default: false}),
|
|
56
|
+
'dry-run': Flags.boolean({description: 'Preview senza eseguire', default: false}),
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
async run() {
|
|
60
|
-
const {
|
|
60
|
+
const {args, flags} = await this.parse(CreateRepo)
|
|
61
61
|
const isJson = flags.json
|
|
62
62
|
const isDryRun = flags['dry-run']
|
|
63
63
|
const config = await loadConfig()
|
|
@@ -68,51 +68,58 @@ export default class CreateRepo extends Command {
|
|
|
68
68
|
|
|
69
69
|
// --list mode
|
|
70
70
|
if (flags.list || !args.template) {
|
|
71
|
-
const spinner = isJson
|
|
71
|
+
const spinner = isJson
|
|
72
|
+
? null
|
|
73
|
+
: ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching templates...')}).start()
|
|
72
74
|
const templates = await listTemplates(config.org)
|
|
73
75
|
spinner?.stop()
|
|
74
76
|
|
|
75
77
|
// Search filter
|
|
76
78
|
const searchQuery = flags.search?.toLowerCase()
|
|
77
79
|
const filtered = searchQuery
|
|
78
|
-
? templates.filter(
|
|
79
|
-
t.name.toLowerCase().includes(searchQuery) ||
|
|
80
|
-
t.description.toLowerCase().includes(searchQuery),
|
|
80
|
+
? templates.filter(
|
|
81
|
+
(t) => t.name.toLowerCase().includes(searchQuery) || t.description.toLowerCase().includes(searchQuery),
|
|
81
82
|
)
|
|
82
83
|
: templates
|
|
83
84
|
|
|
84
|
-
if (isJson) return {
|
|
85
|
+
if (isJson) return {templates: filtered}
|
|
85
86
|
|
|
86
87
|
if (templates.length === 0) {
|
|
87
88
|
this.log(chalk.yellow('No templates found in the organization.'))
|
|
88
89
|
this.log(chalk.dim('Templates are GitHub repos marked as "Template repository".'))
|
|
89
|
-
return {
|
|
90
|
+
return {templates: []}
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
if (filtered.length === 0) {
|
|
93
94
|
this.log(chalk.dim(`No templates matching "${flags.search}".`))
|
|
94
|
-
return {
|
|
95
|
+
return {templates: []}
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
const filterInfo = flags.search
|
|
98
|
-
? chalk.dim(' — search: ') + chalk.white(`"${flags.search}"`)
|
|
99
|
-
: ''
|
|
98
|
+
const filterInfo = flags.search ? chalk.dim(' — search: ') + chalk.white(`"${flags.search}"`) : ''
|
|
100
99
|
|
|
101
100
|
this.log(
|
|
102
101
|
chalk.bold('\nAvailable templates') +
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
filterInfo +
|
|
103
|
+
chalk.dim(` (${filtered.length}${filtered.length < templates.length ? `/${templates.length}` : ''})`) +
|
|
104
|
+
'\n',
|
|
106
105
|
)
|
|
107
106
|
|
|
108
|
-
this.log(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
this.log(
|
|
108
|
+
renderTable(filtered, [
|
|
109
|
+
{header: 'Name', key: 'name', width: 35},
|
|
110
|
+
{
|
|
111
|
+
header: 'Language',
|
|
112
|
+
key: 'language',
|
|
113
|
+
width: 14,
|
|
114
|
+
format: (v) => v || '—',
|
|
115
|
+
colorize: (v) => (v === '—' ? chalk.dim(v) : langColor(v)),
|
|
116
|
+
},
|
|
117
|
+
{header: 'Description', key: 'description', width: 60, format: (v) => String(v || '—')},
|
|
118
|
+
]),
|
|
119
|
+
)
|
|
113
120
|
|
|
114
121
|
this.log('')
|
|
115
|
-
return {
|
|
122
|
+
return {templates: filtered}
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
// Create mode
|
|
@@ -126,7 +133,7 @@ export default class CreateRepo extends Command {
|
|
|
126
133
|
// Get repo name
|
|
127
134
|
let repoName = flags.name
|
|
128
135
|
if (!repoName && !isJson) {
|
|
129
|
-
repoName = await input({
|
|
136
|
+
repoName = await input({message: 'Repository name:'})
|
|
130
137
|
} else if (!repoName) {
|
|
131
138
|
this.error('--name is required in non-interactive mode')
|
|
132
139
|
}
|
|
@@ -142,13 +149,16 @@ export default class CreateRepo extends Command {
|
|
|
142
149
|
const ok = await confirm({
|
|
143
150
|
message: `Create ${isPrivate ? 'private' : 'public'} repo "${config.org}/${repoName}" from "${args.template}"?`,
|
|
144
151
|
})
|
|
145
|
-
if (!ok) {
|
|
152
|
+
if (!ok) {
|
|
153
|
+
this.log('Aborted.')
|
|
154
|
+
return
|
|
155
|
+
}
|
|
146
156
|
}
|
|
147
157
|
|
|
148
158
|
if (isDryRun) {
|
|
149
159
|
const preview = {
|
|
150
|
-
repository: {
|
|
151
|
-
postScaffolding: {
|
|
160
|
+
repository: {name: repoName, org: config.org, template: args.template, private: isPrivate},
|
|
161
|
+
postScaffolding: {branchProtection: 'would configure', dependabot: 'would enable', codeowners: 'would create'},
|
|
152
162
|
}
|
|
153
163
|
if (isJson) return preview
|
|
154
164
|
this.log(chalk.bold('\nDry run preview:'))
|
|
@@ -157,7 +167,9 @@ export default class CreateRepo extends Command {
|
|
|
157
167
|
}
|
|
158
168
|
|
|
159
169
|
// Create repo
|
|
160
|
-
const spinner = isJson
|
|
170
|
+
const spinner = isJson
|
|
171
|
+
? null
|
|
172
|
+
: ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Creating repository...')}).start()
|
|
161
173
|
const repo = await createFromTemplate({
|
|
162
174
|
templateOwner: config.org,
|
|
163
175
|
templateRepo: args.template,
|
|
@@ -169,22 +181,28 @@ export default class CreateRepo extends Command {
|
|
|
169
181
|
spinner?.succeed(`Repository created: ${repo.htmlUrl}`)
|
|
170
182
|
|
|
171
183
|
// Post-scaffolding
|
|
172
|
-
const bpSpinner = isJson
|
|
184
|
+
const bpSpinner = isJson
|
|
185
|
+
? null
|
|
186
|
+
: ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Configuring branch protection...')}).start()
|
|
173
187
|
await setBranchProtection(config.org, repoName).catch(() => null)
|
|
174
188
|
bpSpinner?.succeed('Branch protection configured')
|
|
175
189
|
|
|
176
|
-
const depSpinner = isJson
|
|
190
|
+
const depSpinner = isJson
|
|
191
|
+
? null
|
|
192
|
+
: ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Enabling Dependabot...')}).start()
|
|
177
193
|
await enableDependabot(config.org, repoName).catch(() => null)
|
|
178
194
|
depSpinner?.succeed('Dependabot enabled')
|
|
179
195
|
|
|
180
196
|
// Clone
|
|
181
|
-
const cloneSpinner = isJson
|
|
197
|
+
const cloneSpinner = isJson
|
|
198
|
+
? null
|
|
199
|
+
: ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Cloning repository...')}).start()
|
|
182
200
|
await exec('gh', ['repo', 'clone', `${config.org}/${repoName}`])
|
|
183
201
|
cloneSpinner?.succeed(`Cloned to ./${repoName}`)
|
|
184
202
|
|
|
185
203
|
const result = {
|
|
186
|
-
repository: {
|
|
187
|
-
postScaffolding: {
|
|
204
|
+
repository: {name: repoName, url: repo.htmlUrl, localPath: `./${repoName}`},
|
|
205
|
+
postScaffolding: {branchProtection: 'ok', dependabot: 'ok', codeowners: 'ok'},
|
|
188
206
|
}
|
|
189
207
|
|
|
190
208
|
if (!isJson) {
|