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
package/package.json
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "devvami",
|
|
3
|
+
"description": "DevEx CLI for developers and teams — manage repos, PRs, pipelines, tasks, and costs from the terminal",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"author": "",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"dvmi": "./bin/run.js"
|
|
9
|
+
},
|
|
10
|
+
"packageManager": "pnpm@10.32.1",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=24.0.0",
|
|
13
|
+
"pnpm": ">=10"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"files": [
|
|
17
|
+
"./bin",
|
|
18
|
+
"./src",
|
|
19
|
+
"./oclif.manifest.json"
|
|
20
|
+
],
|
|
21
|
+
"main": "src/index.js",
|
|
22
|
+
"oclif": {
|
|
23
|
+
"bin": "dvmi",
|
|
24
|
+
"dirname": "dvmi",
|
|
25
|
+
"commands": "./src/commands",
|
|
26
|
+
"helpClass": "./src/help.js",
|
|
27
|
+
"plugins": [
|
|
28
|
+
"@oclif/plugin-help",
|
|
29
|
+
"@oclif/plugin-autocomplete"
|
|
30
|
+
],
|
|
31
|
+
"hooks": {
|
|
32
|
+
"init": "./src/hooks/init.js",
|
|
33
|
+
"postrun": "./src/hooks/postrun.js"
|
|
34
|
+
},
|
|
35
|
+
"topicSeparator": " ",
|
|
36
|
+
"theme": {
|
|
37
|
+
"sectionHeader": "#FF6B2B",
|
|
38
|
+
"sectionDescription": "#FFFFFF",
|
|
39
|
+
"bin": "#FF9A5C",
|
|
40
|
+
"command": "#FF9A5C",
|
|
41
|
+
"commandSummary": "#CCCCCC",
|
|
42
|
+
"topic": "#FF9A5C",
|
|
43
|
+
"version": "#4A9EFF",
|
|
44
|
+
"dollarSign": "#FF6B2B",
|
|
45
|
+
"flag": "#4A9EFF",
|
|
46
|
+
"flagSeparator": "#888888",
|
|
47
|
+
"flagDefaultValue": "#888888",
|
|
48
|
+
"flagOptions": "#888888",
|
|
49
|
+
"flagRequired": "#FF6B2B",
|
|
50
|
+
"alias": "#888888"
|
|
51
|
+
},
|
|
52
|
+
"topics": {
|
|
53
|
+
"auth": {
|
|
54
|
+
"description": "🔑 GitHub and AWS authentication"
|
|
55
|
+
},
|
|
56
|
+
"create": {
|
|
57
|
+
"description": "🏗️ Create projects from templates"
|
|
58
|
+
},
|
|
59
|
+
"branch": {
|
|
60
|
+
"description": "🌿 Branch management with naming conventions"
|
|
61
|
+
},
|
|
62
|
+
"pr": {
|
|
63
|
+
"description": "🔀 Pull Request workflow"
|
|
64
|
+
},
|
|
65
|
+
"repo": {
|
|
66
|
+
"description": "📁 GitHub repository management"
|
|
67
|
+
},
|
|
68
|
+
"pipeline": {
|
|
69
|
+
"description": "⚙️ GitHub Actions monitoring"
|
|
70
|
+
},
|
|
71
|
+
"tasks": {
|
|
72
|
+
"description": "✅ Task management"
|
|
73
|
+
},
|
|
74
|
+
"costs": {
|
|
75
|
+
"description": "💰 AWS cost analysis"
|
|
76
|
+
},
|
|
77
|
+
"docs": {
|
|
78
|
+
"description": "📖 Repository documentation"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"dependencies": {
|
|
83
|
+
"@aws-sdk/client-cost-explorer": "^3",
|
|
84
|
+
"@inquirer/prompts": "^7",
|
|
85
|
+
"@oclif/core": "^4",
|
|
86
|
+
"@oclif/plugin-autocomplete": "^3",
|
|
87
|
+
"@oclif/plugin-help": "^6",
|
|
88
|
+
"chalk": "^5",
|
|
89
|
+
"execa": "^9",
|
|
90
|
+
"figlet": "^1",
|
|
91
|
+
"js-yaml": "^4.1.1",
|
|
92
|
+
"keytar": "^7",
|
|
93
|
+
"marked": "^9.1.6",
|
|
94
|
+
"octokit": "^4",
|
|
95
|
+
"open": "^10",
|
|
96
|
+
"ora": "^8",
|
|
97
|
+
"pako": "^2.1.0"
|
|
98
|
+
},
|
|
99
|
+
"devDependencies": {
|
|
100
|
+
"@commitlint/cli": "^19",
|
|
101
|
+
"@commitlint/config-conventional": "^19",
|
|
102
|
+
"@commitlint/types": "^19",
|
|
103
|
+
"@eslint/compat": "^1",
|
|
104
|
+
"@semantic-release/changelog": "^6",
|
|
105
|
+
"@semantic-release/git": "^10",
|
|
106
|
+
"@semantic-release/github": "^10",
|
|
107
|
+
"@vitest/coverage-v8": "^3",
|
|
108
|
+
"commitizen": "^4",
|
|
109
|
+
"conventional-changelog-conventionalcommits": "^8",
|
|
110
|
+
"cz-git": "^1",
|
|
111
|
+
"eslint": "^9",
|
|
112
|
+
"eslint-config-prettier": "^10",
|
|
113
|
+
"eslint-plugin-jsdoc": "^50",
|
|
114
|
+
"lefthook": "^1",
|
|
115
|
+
"memfs": "^4",
|
|
116
|
+
"msw": "^2",
|
|
117
|
+
"oclif": "^4",
|
|
118
|
+
"prettier": "^3",
|
|
119
|
+
"semantic-release": "^24",
|
|
120
|
+
"shx": "^0.4.0",
|
|
121
|
+
"strip-ansi": "^7",
|
|
122
|
+
"vitest": "^3"
|
|
123
|
+
},
|
|
124
|
+
"scripts": {
|
|
125
|
+
"commit": "git-cz",
|
|
126
|
+
"release": "semantic-release",
|
|
127
|
+
"release:dry-run": "semantic-release --dry-run",
|
|
128
|
+
"lint": "eslint src/ tests/",
|
|
129
|
+
"lint:fix": "eslint src/ tests/ --fix",
|
|
130
|
+
"format": "prettier --write src/ tests/",
|
|
131
|
+
"test": "vitest run",
|
|
132
|
+
"test:unit": "vitest run --project unit",
|
|
133
|
+
"test:services": "vitest run --project services",
|
|
134
|
+
"test:integration": "vitest run --project integration",
|
|
135
|
+
"test:watch": "vitest",
|
|
136
|
+
"test:coverage": "vitest run --coverage",
|
|
137
|
+
"prepack": "oclif manifest",
|
|
138
|
+
"postpack": "shx rm -f oclif.manifest.json",
|
|
139
|
+
"prepare": "lefthook install"
|
|
140
|
+
},
|
|
141
|
+
"config": {
|
|
142
|
+
"commitizen": {
|
|
143
|
+
"path": "cz-git"
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"repository": {
|
|
147
|
+
"type": "git",
|
|
148
|
+
"url": "https://github.com/savez/devvami"
|
|
149
|
+
},
|
|
150
|
+
"publishConfig": {
|
|
151
|
+
"access": "public",
|
|
152
|
+
"registry": "https://registry.npmjs.org"
|
|
153
|
+
},
|
|
154
|
+
"pnpm": {
|
|
155
|
+
"onlyBuiltDependencies": [
|
|
156
|
+
"esbuild",
|
|
157
|
+
"keytar",
|
|
158
|
+
"msw"
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import ora from 'ora'
|
|
4
|
+
import { checkGitHubAuth, loginGitHub, checkAWSAuth, loginAWS } from '../../services/auth.js'
|
|
5
|
+
import { loadConfig } from '../../services/config.js'
|
|
6
|
+
|
|
7
|
+
export default class AuthLogin extends Command {
|
|
8
|
+
static description = 'Autenticazione centralizzata GitHub + AWS'
|
|
9
|
+
|
|
10
|
+
static examples = [
|
|
11
|
+
'<%= config.bin %> auth login',
|
|
12
|
+
'<%= config.bin %> auth login --github',
|
|
13
|
+
'<%= config.bin %> auth login --aws',
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
static enableJsonFlag = true
|
|
17
|
+
|
|
18
|
+
static flags = {
|
|
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
|
+
}
|
|
23
|
+
|
|
24
|
+
async run() {
|
|
25
|
+
const { flags } = await this.parse(AuthLogin)
|
|
26
|
+
const isJson = flags.json
|
|
27
|
+
const doGitHub = !flags.aws || flags.github
|
|
28
|
+
const doAWS = !flags.github || flags.aws
|
|
29
|
+
|
|
30
|
+
const result = { github: null, aws: null }
|
|
31
|
+
|
|
32
|
+
// GitHub auth
|
|
33
|
+
if (doGitHub) {
|
|
34
|
+
const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking GitHub auth...') }).start()
|
|
35
|
+
let ghStatus = await checkGitHubAuth()
|
|
36
|
+
|
|
37
|
+
if (ghStatus.authenticated) {
|
|
38
|
+
spinner?.succeed(`GitHub: already authenticated as @${ghStatus.username}`)
|
|
39
|
+
result.github = { status: 'ok', username: ghStatus.username, org: '' }
|
|
40
|
+
} else {
|
|
41
|
+
if (spinner) spinner.text = 'Logging in to GitHub...'
|
|
42
|
+
ghStatus = await loginGitHub()
|
|
43
|
+
if (ghStatus.authenticated) {
|
|
44
|
+
spinner?.succeed(`GitHub: authenticated as @${ghStatus.username}`)
|
|
45
|
+
result.github = { status: 'ok', username: ghStatus.username, org: '' }
|
|
46
|
+
} else {
|
|
47
|
+
spinner?.fail('GitHub authentication failed')
|
|
48
|
+
result.github = { status: 'error', error: ghStatus.error }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// AWS auth
|
|
54
|
+
if (doAWS) {
|
|
55
|
+
const config = await loadConfig()
|
|
56
|
+
const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking AWS auth...') }).start()
|
|
57
|
+
let awsStatus = await checkAWSAuth()
|
|
58
|
+
|
|
59
|
+
if (awsStatus.authenticated) {
|
|
60
|
+
spinner?.succeed(`AWS: session active for account ${awsStatus.account}`)
|
|
61
|
+
result.aws = { status: 'ok', account: awsStatus.account, role: awsStatus.role }
|
|
62
|
+
} else {
|
|
63
|
+
if (spinner) spinner.text = 'Logging in to AWS via aws-vault...'
|
|
64
|
+
awsStatus = await loginAWS(config.awsProfile || 'default')
|
|
65
|
+
if (awsStatus.authenticated) {
|
|
66
|
+
spinner?.succeed(`AWS: logged in to account ${awsStatus.account}`)
|
|
67
|
+
result.aws = { status: 'ok', account: awsStatus.account, role: awsStatus.role }
|
|
68
|
+
} else {
|
|
69
|
+
spinner?.fail('AWS authentication failed')
|
|
70
|
+
result.aws = { status: 'error', error: awsStatus.error }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (isJson) return result
|
|
76
|
+
|
|
77
|
+
this.log('\n' + chalk.green('Authentication complete'))
|
|
78
|
+
if (result.github) {
|
|
79
|
+
const icon = result.github.status === 'ok' ? chalk.green('✓') : chalk.red('✗')
|
|
80
|
+
this.log(` ${icon} GitHub: @${result.github.username ?? 'error'}`)
|
|
81
|
+
}
|
|
82
|
+
if (result.aws) {
|
|
83
|
+
const icon = result.aws.status === 'ok' ? chalk.green('✓') : chalk.red('✗')
|
|
84
|
+
this.log(` ${icon} AWS: account ${result.aws.account ?? 'error'}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core'
|
|
2
|
+
import { writeFile } from 'node:fs/promises'
|
|
3
|
+
import { exec } from '../services/shell.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse a conventional commit message.
|
|
7
|
+
* @param {string} message
|
|
8
|
+
* @returns {{ type: string, scope: string, description: string }|null}
|
|
9
|
+
*/
|
|
10
|
+
function parseConventionalCommit(message) {
|
|
11
|
+
const match = message.match(/^(\w+)(?:\(([^)]+)\))?!?: (.+)/)
|
|
12
|
+
if (!match) return null
|
|
13
|
+
return { type: match[1], scope: match[2] ?? '', description: match[3] }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default class Changelog extends Command {
|
|
17
|
+
static description = 'Genera changelog da Conventional Commits'
|
|
18
|
+
|
|
19
|
+
static examples = [
|
|
20
|
+
'<%= config.bin %> changelog',
|
|
21
|
+
'<%= config.bin %> changelog --from v1.0.0',
|
|
22
|
+
'<%= config.bin %> changelog --output CHANGELOG.md',
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
static enableJsonFlag = true
|
|
26
|
+
|
|
27
|
+
static flags = {
|
|
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
|
+
}
|
|
32
|
+
|
|
33
|
+
async run() {
|
|
34
|
+
const { flags } = await this.parse(Changelog)
|
|
35
|
+
const isJson = flags.json
|
|
36
|
+
|
|
37
|
+
// Determine from ref
|
|
38
|
+
let from = flags.from
|
|
39
|
+
if (!from) {
|
|
40
|
+
const tagResult = await exec('git', ['describe', '--tags', '--abbrev=0'])
|
|
41
|
+
from = tagResult.exitCode === 0 ? tagResult.stdout : ''
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Get commits
|
|
45
|
+
const range = from ? `${from}..${flags.to}` : flags.to
|
|
46
|
+
const logResult = await exec('git', ['log', range, '--format=%s|%H'])
|
|
47
|
+
if (logResult.exitCode !== 0) {
|
|
48
|
+
this.error('Failed to read git log. Are you in a git repository?')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const lines = logResult.stdout.split('\n').filter(Boolean)
|
|
52
|
+
|
|
53
|
+
/** @type {Record<string, Array<{ message: string, hash: string }>>} */
|
|
54
|
+
const sections = { feat: [], fix: [], chore: [], docs: [], refactor: [], test: [], other: [] }
|
|
55
|
+
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
const [message, hash] = line.split('|')
|
|
58
|
+
const parsed = parseConventionalCommit(message)
|
|
59
|
+
const type = parsed?.type ?? 'other'
|
|
60
|
+
const entry = { message: message.trim(), hash: hash?.slice(0, 7) ?? '' }
|
|
61
|
+
if (type in sections) {
|
|
62
|
+
sections[type].push(entry)
|
|
63
|
+
} else {
|
|
64
|
+
sections.other.push(entry)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (isJson) return { from: from || 'beginning', to: flags.to, sections }
|
|
69
|
+
|
|
70
|
+
// Build markdown
|
|
71
|
+
const title = `## [Unreleased]${from ? ` (since ${from})` : ''}`
|
|
72
|
+
const parts = [title, '']
|
|
73
|
+
|
|
74
|
+
const sectionTitles = {
|
|
75
|
+
feat: '### Features',
|
|
76
|
+
fix: '### Bug Fixes',
|
|
77
|
+
chore: '### Chores',
|
|
78
|
+
docs: '### Documentation',
|
|
79
|
+
refactor: '### Refactoring',
|
|
80
|
+
test: '### Tests',
|
|
81
|
+
other: '### Other',
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const [type, entries] of Object.entries(sections)) {
|
|
85
|
+
if (entries.length === 0) continue
|
|
86
|
+
parts.push(sectionTitles[type])
|
|
87
|
+
for (const e of entries) parts.push(`- ${e.message}`)
|
|
88
|
+
parts.push('')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const output = parts.join('\n')
|
|
92
|
+
|
|
93
|
+
if (flags.output) {
|
|
94
|
+
await writeFile(flags.output, output, 'utf8')
|
|
95
|
+
this.log(`Changelog written to ${flags.output}`)
|
|
96
|
+
} else {
|
|
97
|
+
this.log(output)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { from: from || 'beginning', to: flags.to, sections }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Command, Args, Flags } from '@oclif/core'
|
|
2
|
+
import ora from 'ora'
|
|
3
|
+
import { getServiceCosts } from '../../services/aws-costs.js'
|
|
4
|
+
import { loadConfig } from '../../services/config.js'
|
|
5
|
+
import { formatCostTable, calculateTotal } from '../../formatters/cost.js'
|
|
6
|
+
|
|
7
|
+
export default class CostsGet extends Command {
|
|
8
|
+
static description = 'Stima costi AWS per un servizio (via Cost Explorer API)'
|
|
9
|
+
|
|
10
|
+
static examples = [
|
|
11
|
+
'<%= config.bin %> costs get my-service',
|
|
12
|
+
'<%= config.bin %> costs get my-api --period mtd',
|
|
13
|
+
'<%= config.bin %> costs get my-service --json',
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
static enableJsonFlag = true
|
|
17
|
+
|
|
18
|
+
static args = {
|
|
19
|
+
service: Args.string({ description: 'Nome del servizio', required: true }),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static flags = {
|
|
23
|
+
period: Flags.string({
|
|
24
|
+
description: 'Periodo: last-month, last-week, mtd',
|
|
25
|
+
default: 'last-month',
|
|
26
|
+
options: ['last-month', 'last-week', 'mtd'],
|
|
27
|
+
}),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async run() {
|
|
31
|
+
const { args, flags } = await this.parse(CostsGet)
|
|
32
|
+
const isJson = flags.json
|
|
33
|
+
|
|
34
|
+
const spinner = isJson ? null : ora(`Fetching costs for ${args.service}...`).start()
|
|
35
|
+
|
|
36
|
+
// Get project tags from config
|
|
37
|
+
const config = await loadConfig()
|
|
38
|
+
const tags = config.projectTags ?? { project: args.service }
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const { entries, period } = await getServiceCosts(args.service, tags, /** @type {any} */ (flags.period))
|
|
42
|
+
spinner?.stop()
|
|
43
|
+
|
|
44
|
+
const total = calculateTotal(entries)
|
|
45
|
+
const result = {
|
|
46
|
+
service: args.service,
|
|
47
|
+
period,
|
|
48
|
+
items: entries,
|
|
49
|
+
total: { amount: total, unit: 'USD' },
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (isJson) return result
|
|
53
|
+
|
|
54
|
+
if (entries.length === 0) {
|
|
55
|
+
this.log(`No costs found for service "${args.service}".`)
|
|
56
|
+
this.log('Check service name and tagging convention.')
|
|
57
|
+
return result
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.log(formatCostTable(entries, args.service))
|
|
61
|
+
return result
|
|
62
|
+
} catch (err) {
|
|
63
|
+
spinner?.stop()
|
|
64
|
+
if (String(err).includes('AccessDenied') || String(err).includes('UnauthorizedAccess')) {
|
|
65
|
+
this.error('Missing IAM permission: ce:GetCostAndUsage\nContact your AWS admin to grant Cost Explorer access.')
|
|
66
|
+
}
|
|
67
|
+
if (String(err).includes('CredentialsProviderError') || String(err).includes('No credentials')) {
|
|
68
|
+
this.error('No AWS credentials. Use: aws-vault exec <profile> -- dvmi costs get ' + args.service)
|
|
69
|
+
}
|
|
70
|
+
throw err
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { Command, Args, Flags } from '@oclif/core'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import ora from 'ora'
|
|
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
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} lang
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function langColor(lang) {
|
|
16
|
+
const map = {
|
|
17
|
+
javascript: chalk.yellow,
|
|
18
|
+
typescript: chalk.blue,
|
|
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
|
+
}
|
|
29
|
+
const fn = map[lang.toLowerCase()]
|
|
30
|
+
return fn ? fn(lang) : chalk.dim(lang)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default class CreateRepo extends Command {
|
|
34
|
+
static description = 'Crea nuovo progetto da template GitHub o lista i template disponibili'
|
|
35
|
+
|
|
36
|
+
static examples = [
|
|
37
|
+
'<%= config.bin %> create repo --list',
|
|
38
|
+
'<%= config.bin %> create repo --list --search "lambda"',
|
|
39
|
+
'<%= config.bin %> create repo template-lambda',
|
|
40
|
+
'<%= config.bin %> create repo template-lambda --name my-service --dry-run',
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
static enableJsonFlag = true
|
|
44
|
+
|
|
45
|
+
static args = {
|
|
46
|
+
template: Args.string({ description: 'Nome del template', required: false }),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static flags = {
|
|
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
|
+
}
|
|
58
|
+
|
|
59
|
+
async run() {
|
|
60
|
+
const { args, flags } = await this.parse(CreateRepo)
|
|
61
|
+
const isJson = flags.json
|
|
62
|
+
const isDryRun = flags['dry-run']
|
|
63
|
+
const config = await loadConfig()
|
|
64
|
+
|
|
65
|
+
if (!config.org) {
|
|
66
|
+
this.error('GitHub org not configured. Run `dvmi init` to set up your environment.')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --list mode
|
|
70
|
+
if (flags.list || !args.template) {
|
|
71
|
+
const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching templates...') }).start()
|
|
72
|
+
const templates = await listTemplates(config.org)
|
|
73
|
+
spinner?.stop()
|
|
74
|
+
|
|
75
|
+
// Search filter
|
|
76
|
+
const searchQuery = flags.search?.toLowerCase()
|
|
77
|
+
const filtered = searchQuery
|
|
78
|
+
? templates.filter((t) =>
|
|
79
|
+
t.name.toLowerCase().includes(searchQuery) ||
|
|
80
|
+
t.description.toLowerCase().includes(searchQuery),
|
|
81
|
+
)
|
|
82
|
+
: templates
|
|
83
|
+
|
|
84
|
+
if (isJson) return { templates: filtered }
|
|
85
|
+
|
|
86
|
+
if (templates.length === 0) {
|
|
87
|
+
this.log(chalk.yellow('No templates found in the organization.'))
|
|
88
|
+
this.log(chalk.dim('Templates are GitHub repos marked as "Template repository".'))
|
|
89
|
+
return { templates: [] }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (filtered.length === 0) {
|
|
93
|
+
this.log(chalk.dim(`No templates matching "${flags.search}".`))
|
|
94
|
+
return { templates: [] }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const filterInfo = flags.search
|
|
98
|
+
? chalk.dim(' — search: ') + chalk.white(`"${flags.search}"`)
|
|
99
|
+
: ''
|
|
100
|
+
|
|
101
|
+
this.log(
|
|
102
|
+
chalk.bold('\nAvailable templates') +
|
|
103
|
+
filterInfo +
|
|
104
|
+
chalk.dim(` (${filtered.length}${filtered.length < templates.length ? `/${templates.length}` : ''})`) +
|
|
105
|
+
'\n',
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
this.log(renderTable(filtered, [
|
|
109
|
+
{ header: 'Name', key: 'name', width: 35 },
|
|
110
|
+
{ header: 'Language', key: 'language', width: 14, format: (v) => v || '—', colorize: (v) => v === '—' ? chalk.dim(v) : langColor(v) },
|
|
111
|
+
{ header: 'Description', key: 'description', width: 60, format: (v) => String(v || '—') },
|
|
112
|
+
]))
|
|
113
|
+
|
|
114
|
+
this.log('')
|
|
115
|
+
return { templates: filtered }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Create mode
|
|
119
|
+
const templates = await listTemplates(config.org)
|
|
120
|
+
const template = templates.find((t) => t.name === args.template)
|
|
121
|
+
if (!template) {
|
|
122
|
+
const names = templates.map((t) => t.name).join(', ')
|
|
123
|
+
this.error(`Template "${args.template}" not found. Available: ${names}`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Get repo name
|
|
127
|
+
let repoName = flags.name
|
|
128
|
+
if (!repoName && !isJson) {
|
|
129
|
+
repoName = await input({ message: 'Repository name:' })
|
|
130
|
+
} else if (!repoName) {
|
|
131
|
+
this.error('--name is required in non-interactive mode')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const validation = validateRepoName(repoName)
|
|
135
|
+
if (!validation.valid) {
|
|
136
|
+
this.error(`${validation.error}${validation.suggestion ? `\nSuggestion: ${validation.suggestion}` : ''}`)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const isPrivate = !flags.public
|
|
140
|
+
|
|
141
|
+
if (!isJson && !isDryRun) {
|
|
142
|
+
const ok = await confirm({
|
|
143
|
+
message: `Create ${isPrivate ? 'private' : 'public'} repo "${config.org}/${repoName}" from "${args.template}"?`,
|
|
144
|
+
})
|
|
145
|
+
if (!ok) { this.log('Aborted.'); return }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (isDryRun) {
|
|
149
|
+
const preview = {
|
|
150
|
+
repository: { name: repoName, org: config.org, template: args.template, private: isPrivate },
|
|
151
|
+
postScaffolding: { branchProtection: 'would configure', dependabot: 'would enable', codeowners: 'would create' },
|
|
152
|
+
}
|
|
153
|
+
if (isJson) return preview
|
|
154
|
+
this.log(chalk.bold('\nDry run preview:'))
|
|
155
|
+
this.log(JSON.stringify(preview, null, 2))
|
|
156
|
+
return preview
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Create repo
|
|
160
|
+
const spinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Creating repository...') }).start()
|
|
161
|
+
const repo = await createFromTemplate({
|
|
162
|
+
templateOwner: config.org,
|
|
163
|
+
templateRepo: args.template,
|
|
164
|
+
name: repoName,
|
|
165
|
+
org: config.org,
|
|
166
|
+
description: flags.description,
|
|
167
|
+
isPrivate,
|
|
168
|
+
})
|
|
169
|
+
spinner?.succeed(`Repository created: ${repo.htmlUrl}`)
|
|
170
|
+
|
|
171
|
+
// Post-scaffolding
|
|
172
|
+
const bpSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Configuring branch protection...') }).start()
|
|
173
|
+
await setBranchProtection(config.org, repoName).catch(() => null)
|
|
174
|
+
bpSpinner?.succeed('Branch protection configured')
|
|
175
|
+
|
|
176
|
+
const depSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Enabling Dependabot...') }).start()
|
|
177
|
+
await enableDependabot(config.org, repoName).catch(() => null)
|
|
178
|
+
depSpinner?.succeed('Dependabot enabled')
|
|
179
|
+
|
|
180
|
+
// Clone
|
|
181
|
+
const cloneSpinner = isJson ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Cloning repository...') }).start()
|
|
182
|
+
await exec('gh', ['repo', 'clone', `${config.org}/${repoName}`])
|
|
183
|
+
cloneSpinner?.succeed(`Cloned to ./${repoName}`)
|
|
184
|
+
|
|
185
|
+
const result = {
|
|
186
|
+
repository: { name: repoName, url: repo.htmlUrl, localPath: `./${repoName}` },
|
|
187
|
+
postScaffolding: { branchProtection: 'ok', dependabot: 'ok', codeowners: 'ok' },
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!isJson) {
|
|
191
|
+
this.log('\n' + chalk.green('✓') + ` cd ${repoName} to start working`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return result
|
|
195
|
+
}
|
|
196
|
+
}
|