uva-cli 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 +177 -0
- package/bin.mjs +56 -0
- package/commands/branch.mjs +138 -0
- package/commands/commit.mjs +136 -0
- package/commands/init.mjs +160 -0
- package/commands/new-file.mjs +240 -0
- package/commands/push.mjs +31 -0
- package/commands/start.mjs +63 -0
- package/lib/banner.mjs +15 -0
- package/lib/colors.mjs +50 -0
- package/lib/config.mjs +34 -0
- package/lib/format.mjs +26 -0
- package/lib/git.mjs +67 -0
- package/lib/slugify.mjs +14 -0
- package/lib/types.mjs +45 -0
- package/package.json +47 -0
- package/templates/en/docs/adr.md +45 -0
- package/templates/en/docs/decision-log.md +26 -0
- package/templates/en/docs/meeting-notes.md +32 -0
- package/templates/en/docs/rfc.md +39 -0
- package/templates/en/docs/runbook.md +46 -0
- package/templates/en/git/pull-request.md +37 -0
- package/templates/pt-br/docs/adr.md +44 -0
- package/templates/pt-br/docs/decision-log.md +26 -0
- package/templates/pt-br/docs/meeting-notes.md +32 -0
- package/templates/pt-br/docs/rfc.md +39 -0
- package/templates/pt-br/docs/runbook.md +46 -0
- package/templates/pt-br/git/pull-request.md +37 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { select, text, confirm, isCancel, log } from '@clack/prompts'
|
|
2
|
+
import { bannerIntro, bannerOutro, bannerCancelled } from '../lib/banner.mjs'
|
|
3
|
+
import { getGitRoot } from '../lib/git.mjs'
|
|
4
|
+
import { slugify } from '../lib/slugify.mjs'
|
|
5
|
+
import { execSync } from 'child_process'
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
7
|
+
import { join, dirname } from 'path'
|
|
8
|
+
import { fileURLToPath } from 'url'
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
11
|
+
|
|
12
|
+
// ── Template registry ──────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const CATEGORIES = {
|
|
15
|
+
docs: {
|
|
16
|
+
label: 'Docs — documentation files',
|
|
17
|
+
types: [
|
|
18
|
+
{
|
|
19
|
+
value: 'adr',
|
|
20
|
+
label: 'ADR Architecture Decision Record',
|
|
21
|
+
folder: 'docs/decisions',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
value: 'decision-log',
|
|
25
|
+
label: 'Decision Log Lightweight decision entry',
|
|
26
|
+
folder: 'docs/decisions',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
value: 'meeting-notes',
|
|
30
|
+
label: 'Meeting Notes Agenda, notes, and action items',
|
|
31
|
+
folder: 'docs/meetings',
|
|
32
|
+
},
|
|
33
|
+
{ value: 'rfc', label: 'RFC Technical proposal / spec', folder: 'docs/rfcs' },
|
|
34
|
+
{
|
|
35
|
+
value: 'runbook',
|
|
36
|
+
label: 'Runbook Operational step-by-step guide',
|
|
37
|
+
folder: 'docs/runbooks',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
git: {
|
|
42
|
+
label: 'Git — GitHub workflow files',
|
|
43
|
+
types: [
|
|
44
|
+
{
|
|
45
|
+
value: 'pull-request',
|
|
46
|
+
label: 'PR Template GitHub pull request template',
|
|
47
|
+
folder: '.github',
|
|
48
|
+
fixedName: 'pull_request_template.md',
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const LANGUAGES = [
|
|
55
|
+
{ value: 'en', label: 'English' },
|
|
56
|
+
{ value: 'pt-br', label: 'Português (BR)' },
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
function getAuthor() {
|
|
62
|
+
try {
|
|
63
|
+
return execSync('git config user.name', { encoding: 'utf-8' }).trim() || 'unknown'
|
|
64
|
+
} catch {
|
|
65
|
+
return 'unknown'
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getDate() {
|
|
70
|
+
return new Date().toISOString().split('T')[0]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function applyTemplate(content, vars) {
|
|
74
|
+
return content
|
|
75
|
+
.replace(/\{\{title\}\}/g, vars.title)
|
|
76
|
+
.replace(/\{\{date\}\}/g, vars.date)
|
|
77
|
+
.replace(/\{\{author\}\}/g, vars.author)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Command ────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
// Called by commander as runNewFile(options, command)
|
|
83
|
+
export async function runNewFile(opts = {}) {
|
|
84
|
+
bannerIntro('new-file')
|
|
85
|
+
|
|
86
|
+
// ── Language ─────────────────────────────────────────────────────────────
|
|
87
|
+
let lang
|
|
88
|
+
if (opts.lang) {
|
|
89
|
+
const valid = LANGUAGES.map((l) => l.value)
|
|
90
|
+
if (!valid.includes(opts.lang)) {
|
|
91
|
+
log.error(`Invalid --lang "${opts.lang}". Valid values: ${valid.join(', ')}`)
|
|
92
|
+
process.exit(1)
|
|
93
|
+
}
|
|
94
|
+
lang = opts.lang
|
|
95
|
+
} else {
|
|
96
|
+
lang = await select({ message: 'Language', options: LANGUAGES })
|
|
97
|
+
if (isCancel(lang)) {
|
|
98
|
+
bannerCancelled()
|
|
99
|
+
process.exit(0)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Category ─────────────────────────────────────────────────────────────
|
|
104
|
+
let category
|
|
105
|
+
if (opts.category) {
|
|
106
|
+
const valid = Object.keys(CATEGORIES)
|
|
107
|
+
if (!valid.includes(opts.category)) {
|
|
108
|
+
log.error(`Invalid --category "${opts.category}". Valid values: ${valid.join(', ')}`)
|
|
109
|
+
process.exit(1)
|
|
110
|
+
}
|
|
111
|
+
category = opts.category
|
|
112
|
+
} else {
|
|
113
|
+
category = await select({
|
|
114
|
+
message: 'Category',
|
|
115
|
+
options: Object.entries(CATEGORIES).map(([value, { label }]) => ({ value, label })),
|
|
116
|
+
})
|
|
117
|
+
if (isCancel(category)) {
|
|
118
|
+
bannerCancelled()
|
|
119
|
+
process.exit(0)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Type ─────────────────────────────────────────────────────────────────
|
|
124
|
+
const types = CATEGORIES[category].types
|
|
125
|
+
let type
|
|
126
|
+
if (opts.type) {
|
|
127
|
+
const valid = types.map((t) => t.value)
|
|
128
|
+
if (!valid.includes(opts.type)) {
|
|
129
|
+
log.error(
|
|
130
|
+
`Invalid --type "${opts.type}" for category "${category}". Valid values: ${valid.join(', ')}`,
|
|
131
|
+
)
|
|
132
|
+
process.exit(1)
|
|
133
|
+
}
|
|
134
|
+
type = opts.type
|
|
135
|
+
} else {
|
|
136
|
+
type = await select({
|
|
137
|
+
message: 'File type',
|
|
138
|
+
options: types.map((t) => ({ value: t.value, label: t.label })),
|
|
139
|
+
})
|
|
140
|
+
if (isCancel(type)) {
|
|
141
|
+
bannerCancelled()
|
|
142
|
+
process.exit(0)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const def = types.find((t) => t.value === type)
|
|
147
|
+
const templatePath = join(__dirname, '../templates', lang, category, `${type}.md`)
|
|
148
|
+
|
|
149
|
+
if (!existsSync(templatePath)) {
|
|
150
|
+
log.error(`Template not found: templates/${lang}/${category}/${type}.md`)
|
|
151
|
+
process.exit(1)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const author = getAuthor()
|
|
155
|
+
const date = getDate()
|
|
156
|
+
|
|
157
|
+
// ── Git templates: fixed output path, no name needed ─────────────────────
|
|
158
|
+
if (def.fixedName) {
|
|
159
|
+
const root = getGitRoot()
|
|
160
|
+
const absFolder = join(root, def.folder)
|
|
161
|
+
const absFile = join(absFolder, def.fixedName)
|
|
162
|
+
const relPath = join(def.folder, def.fixedName)
|
|
163
|
+
|
|
164
|
+
if (existsSync(absFile)) {
|
|
165
|
+
const overwrite = await confirm({ message: `"${relPath}" already exists. Overwrite?` })
|
|
166
|
+
if (isCancel(overwrite) || !overwrite) {
|
|
167
|
+
bannerCancelled()
|
|
168
|
+
process.exit(0)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const content = applyTemplate(readFileSync(templatePath, { encoding: 'utf-8' }), {
|
|
173
|
+
title: '',
|
|
174
|
+
date,
|
|
175
|
+
author,
|
|
176
|
+
})
|
|
177
|
+
if (!existsSync(absFolder)) mkdirSync(absFolder, { recursive: true })
|
|
178
|
+
writeFileSync(absFile, content, { encoding: 'utf-8' })
|
|
179
|
+
bannerOutro(`Created: ${relPath}`)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Docs templates: need a title and output folder ────────────────────────
|
|
184
|
+
let rawName
|
|
185
|
+
if (opts.name) {
|
|
186
|
+
rawName = opts.name
|
|
187
|
+
} else {
|
|
188
|
+
const input = await text({
|
|
189
|
+
message: 'Title',
|
|
190
|
+
placeholder: 'Switch auth library to Lucia',
|
|
191
|
+
validate: (v) => (v.trim() ? undefined : 'Title cannot be empty.'),
|
|
192
|
+
})
|
|
193
|
+
if (isCancel(input)) {
|
|
194
|
+
bannerCancelled()
|
|
195
|
+
process.exit(0)
|
|
196
|
+
}
|
|
197
|
+
rawName = input.trim()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const fileName = `${slugify(rawName)}.md`
|
|
201
|
+
|
|
202
|
+
let outputFolder
|
|
203
|
+
if (opts.folder) {
|
|
204
|
+
outputFolder = opts.folder
|
|
205
|
+
} else {
|
|
206
|
+
const input = await text({
|
|
207
|
+
message: 'Output folder',
|
|
208
|
+
initialValue: def.folder,
|
|
209
|
+
validate: (v) => (v.trim() ? undefined : 'Folder cannot be empty.'),
|
|
210
|
+
})
|
|
211
|
+
if (isCancel(input)) {
|
|
212
|
+
bannerCancelled()
|
|
213
|
+
process.exit(0)
|
|
214
|
+
}
|
|
215
|
+
outputFolder = input.trim()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const root = getGitRoot()
|
|
219
|
+
const absFolder = join(root, outputFolder)
|
|
220
|
+
const absFile = join(absFolder, fileName)
|
|
221
|
+
const relPath = join(outputFolder, fileName)
|
|
222
|
+
|
|
223
|
+
if (existsSync(absFile)) {
|
|
224
|
+
const overwrite = await confirm({ message: `"${relPath}" already exists. Overwrite?` })
|
|
225
|
+
if (isCancel(overwrite) || !overwrite) {
|
|
226
|
+
bannerCancelled()
|
|
227
|
+
process.exit(0)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const content = applyTemplate(readFileSync(templatePath, { encoding: 'utf-8' }), {
|
|
232
|
+
title: rawName,
|
|
233
|
+
date,
|
|
234
|
+
author,
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
if (!existsSync(absFolder)) mkdirSync(absFolder, { recursive: true })
|
|
238
|
+
writeFileSync(absFile, content, { encoding: 'utf-8' })
|
|
239
|
+
bannerOutro(`Created: ${relPath}`)
|
|
240
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { confirm, isCancel, log } from '@clack/prompts'
|
|
2
|
+
import { bannerIntro, bannerOutro, bannerCancelled } from '../lib/banner.mjs'
|
|
3
|
+
import { getCurrentBranch } from '../lib/git.mjs'
|
|
4
|
+
import { spawnSync } from 'child_process'
|
|
5
|
+
|
|
6
|
+
// Called by commander as runPush(options, command)
|
|
7
|
+
export async function runPush(opts = {}) {
|
|
8
|
+
bannerIntro('push')
|
|
9
|
+
|
|
10
|
+
const branch = getCurrentBranch()
|
|
11
|
+
|
|
12
|
+
if (!opts.yes) {
|
|
13
|
+
const confirmed = await confirm({
|
|
14
|
+
message: `Push current branch: ${branch}?`,
|
|
15
|
+
})
|
|
16
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
17
|
+
bannerCancelled()
|
|
18
|
+
process.exit(0)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const result = spawnSync('git', ['push', 'origin', branch], {
|
|
23
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
24
|
+
})
|
|
25
|
+
if (result.status !== 0) {
|
|
26
|
+
log.error('Push failed — see the output above.')
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
bannerOutro(`Pushed: origin/${branch}`)
|
|
31
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { select, isCancel, intro, outro } from '@clack/prompts'
|
|
2
|
+
import pc from 'picocolors'
|
|
3
|
+
import { banner as brandBanner, uva, folha } from '../lib/colors.mjs'
|
|
4
|
+
import { runInit } from './init.mjs'
|
|
5
|
+
import { runCommit } from './commit.mjs'
|
|
6
|
+
import { runBranch } from './branch.mjs'
|
|
7
|
+
import { runNewFile } from './new-file.mjs'
|
|
8
|
+
import { runPush } from './push.mjs'
|
|
9
|
+
|
|
10
|
+
export async function runStart() {
|
|
11
|
+
intro(brandBanner('UVA CLI'))
|
|
12
|
+
|
|
13
|
+
console.log('')
|
|
14
|
+
console.log(uva(' uva-cli') + pc.dim(' — Git workflow automation'))
|
|
15
|
+
console.log('')
|
|
16
|
+
console.log(pc.dim(' Guides your team through branch creation and commits'))
|
|
17
|
+
console.log(pc.dim(' following whatever conventions your project defines.'))
|
|
18
|
+
console.log('')
|
|
19
|
+
|
|
20
|
+
const action = await select({
|
|
21
|
+
message: 'What do you want to do?',
|
|
22
|
+
options: [
|
|
23
|
+
{
|
|
24
|
+
value: 'init',
|
|
25
|
+
label: folha('uva init') + ' Set up UVA CLI for this project',
|
|
26
|
+
hint: 'configure commit and branch patterns',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
value: 'commit',
|
|
30
|
+
label: uva('uva commit') + ' Create an interactive commit',
|
|
31
|
+
hint: 'select files, type, ticket and message',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
value: 'branch',
|
|
35
|
+
label: uva('uva branch') + ' Create a new branch',
|
|
36
|
+
hint: 'checks out source and pulls automatically',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
value: 'new-file',
|
|
40
|
+
label: uva('uva new-file') + ' Scaffold a file from a template',
|
|
41
|
+
hint: 'docs, frontend (React), or backend (Express)',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
value: 'push',
|
|
45
|
+
label: uva('uva push') + ' Push the current branch to origin',
|
|
46
|
+
hint: 'confirms the branch and runs git push',
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
if (isCancel(action)) {
|
|
52
|
+
outro(pc.dim('Operation cancelled.'))
|
|
53
|
+
process.exit(0)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log('')
|
|
57
|
+
|
|
58
|
+
if (action === 'init') return runInit()
|
|
59
|
+
if (action === 'commit') return runCommit()
|
|
60
|
+
if (action === 'branch') return runBranch()
|
|
61
|
+
if (action === 'new-file') return runNewFile()
|
|
62
|
+
if (action === 'push') return runPush()
|
|
63
|
+
}
|
package/lib/banner.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { intro, outro } from '@clack/prompts'
|
|
2
|
+
import pc from 'picocolors'
|
|
3
|
+
import { banner as brandBanner, folha } from './colors.mjs'
|
|
4
|
+
|
|
5
|
+
export function bannerIntro(command) {
|
|
6
|
+
intro(brandBanner('UVA CLI') + ' ' + pc.dim(command))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function bannerOutro(message) {
|
|
10
|
+
outro(folha('+ ' + message))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function bannerCancelled() {
|
|
14
|
+
outro(pc.dim('Operation cancelled.'))
|
|
15
|
+
}
|
package/lib/colors.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brand colors — ANSI true color (24-bit).
|
|
3
|
+
* Falls back to basic ANSI if the terminal reports no true-color support.
|
|
4
|
+
*
|
|
5
|
+
* Dark-mode palette used for terminal output:
|
|
6
|
+
* uva #A78BFA (light purple — readable on dark bg)
|
|
7
|
+
* folha #34D399 (light green)
|
|
8
|
+
* bg #6D28D9 (deep purple — banner background)
|
|
9
|
+
* tinta #1C1B22 (near-black ink)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const TRUE_COLOR =
|
|
13
|
+
process.env.COLORTERM === 'truecolor' ||
|
|
14
|
+
process.env.COLORTERM === '24bit' ||
|
|
15
|
+
process.env.TERM_PROGRAM === 'iTerm.app' ||
|
|
16
|
+
process.env.TERM_PROGRAM === 'vscode'
|
|
17
|
+
|
|
18
|
+
function hexRgb(hex) {
|
|
19
|
+
const n = parseInt(hex.replace('#', ''), 16)
|
|
20
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Apply foreground and/or background hex color to text. */
|
|
24
|
+
function paint(text, { fg, bg } = {}) {
|
|
25
|
+
if (!TRUE_COLOR) return text
|
|
26
|
+
let codes = ''
|
|
27
|
+
if (bg) {
|
|
28
|
+
const [r, g, b] = hexRgb(bg)
|
|
29
|
+
codes += `\x1b[48;2;${r};${g};${b}m`
|
|
30
|
+
}
|
|
31
|
+
if (fg) {
|
|
32
|
+
const [r, g, b] = hexRgb(fg)
|
|
33
|
+
codes += `\x1b[38;2;${r};${g};${b}m`
|
|
34
|
+
}
|
|
35
|
+
return `${codes}${text}\x1b[0m`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Brand helpers ──────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/** Light purple text — for general UVA highlights. */
|
|
41
|
+
export const uva = (text) => paint(text, { fg: '#A78BFA' })
|
|
42
|
+
|
|
43
|
+
/** Light green text — for success messages and outros. */
|
|
44
|
+
export const folha = (text) => paint(text, { fg: '#34D399' })
|
|
45
|
+
|
|
46
|
+
/** Deep-purple background + light-purple text — for the intro banner pill. */
|
|
47
|
+
export const banner = (text) => paint(` ${text} `, { fg: '#A78BFA', bg: '#6D28D9' })
|
|
48
|
+
|
|
49
|
+
/** Light-purple dim text — for secondary info (falls back to the raw string). */
|
|
50
|
+
export const dim = (text) => paint(text, { fg: '#6D4FA8' })
|
package/lib/config.mjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import { log } from '@clack/prompts'
|
|
4
|
+
import { getGitRoot } from './git.mjs'
|
|
5
|
+
|
|
6
|
+
function getConfigPath() {
|
|
7
|
+
const root = getGitRoot()
|
|
8
|
+
return join(root, 'uva.config.json')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function loadConfig() {
|
|
12
|
+
const path = getConfigPath()
|
|
13
|
+
if (!existsSync(path)) return null
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(path, { encoding: 'utf-8' }))
|
|
16
|
+
} catch {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function saveConfig(config) {
|
|
22
|
+
const path = getConfigPath()
|
|
23
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + '\n', { encoding: 'utf-8' })
|
|
24
|
+
return path
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function requireConfig() {
|
|
28
|
+
const config = loadConfig()
|
|
29
|
+
if (!config) {
|
|
30
|
+
log.error('No configuration found. Run `uva init` to set up your project.')
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
return config
|
|
34
|
+
}
|
package/lib/format.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { slugify } from './slugify.mjs'
|
|
2
|
+
|
|
3
|
+
export function buildCommitMessage(format, { type, ticket, message }) {
|
|
4
|
+
switch (format) {
|
|
5
|
+
case 'conventional-ticket':
|
|
6
|
+
return `${type}(${ticket}): ${message}`
|
|
7
|
+
case 'ticket-conventional':
|
|
8
|
+
return `[${ticket}] ${type}: ${message}`
|
|
9
|
+
case 'conventional':
|
|
10
|
+
default:
|
|
11
|
+
return `${type}: ${message}`
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildBranchName(format, { type, ticket, name }) {
|
|
16
|
+
const slug = slugify(name)
|
|
17
|
+
switch (format) {
|
|
18
|
+
case 'type-ticket-name':
|
|
19
|
+
return `${type}/${ticket}_${slug}`
|
|
20
|
+
case 'ticket-type-name':
|
|
21
|
+
return `${ticket}/${type}/${slug}`
|
|
22
|
+
case 'type-name':
|
|
23
|
+
default:
|
|
24
|
+
return `${type}/${slug}`
|
|
25
|
+
}
|
|
26
|
+
}
|
package/lib/git.mjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { execSync, spawnSync } from 'child_process'
|
|
2
|
+
|
|
3
|
+
function exec(cmd) {
|
|
4
|
+
return execSync(cmd, { encoding: 'utf-8', stdio: 'pipe' }).trim()
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getCurrentBranch() {
|
|
8
|
+
return exec('git rev-parse --abbrev-ref HEAD')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getGitRoot() {
|
|
12
|
+
return exec('git rev-parse --show-toplevel')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getChangedFiles() {
|
|
16
|
+
const output = exec('git status --porcelain')
|
|
17
|
+
if (!output) return []
|
|
18
|
+
return output
|
|
19
|
+
.split('\n')
|
|
20
|
+
.filter((line) => line.trim())
|
|
21
|
+
.map((line) => {
|
|
22
|
+
// format: XY<space>path (3 chars) or X<space>path (2 chars when Y is space)
|
|
23
|
+
const pathStart = line[2] === ' ' ? 3 : 2
|
|
24
|
+
return {
|
|
25
|
+
status: line.slice(0, pathStart - 1).trim(),
|
|
26
|
+
path: line.slice(pathStart).trim(),
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function hasUncommittedChanges() {
|
|
32
|
+
return getChangedFiles().length > 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function checkout(branch) {
|
|
36
|
+
exec(`git checkout ${branch}`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function pull() {
|
|
40
|
+
exec('git pull')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createBranch(name) {
|
|
44
|
+
exec(`git checkout -b ${name}`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function addFiles(paths) {
|
|
48
|
+
// spawnSync with an array of args avoids any shell quoting issues
|
|
49
|
+
const result = spawnSync('git', ['add', '--', ...paths], {
|
|
50
|
+
encoding: 'utf-8',
|
|
51
|
+
stdio: 'pipe',
|
|
52
|
+
})
|
|
53
|
+
if (result.status !== 0) {
|
|
54
|
+
throw new Error(result.stderr || 'git add falhou')
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function commit(message) {
|
|
59
|
+
// pipe stdin to avoid conflicts with the interactive terminal of clack
|
|
60
|
+
// inherit stdout/stderr so that lefthook/commitlint appear normally
|
|
61
|
+
const result = spawnSync('git', ['commit', '-m', message], {
|
|
62
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
63
|
+
})
|
|
64
|
+
if (result.status !== 0) {
|
|
65
|
+
throw new Error('git commit falhou — veja a saída acima')
|
|
66
|
+
}
|
|
67
|
+
}
|
package/lib/slugify.mjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts free text to kebab-case without accents.
|
|
3
|
+
* Example: "Add Login Screen" -> "add-login-screen"
|
|
4
|
+
*/
|
|
5
|
+
export function slugify(text) {
|
|
6
|
+
return text
|
|
7
|
+
.normalize('NFD')
|
|
8
|
+
.replace(/[\u0300-\u036f]/g, '') // remove diacritics
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/\s+/g, '-')
|
|
11
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
12
|
+
.replace(/-+/g, '-')
|
|
13
|
+
.replace(/^-|-$/g, '')
|
|
14
|
+
}
|
package/lib/types.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export const COMMIT_TYPES = [
|
|
2
|
+
{ value: 'feat', label: 'feat · New feature' },
|
|
3
|
+
{ value: 'fix', label: 'fix · Bug fix' },
|
|
4
|
+
{ value: 'docs', label: 'docs · Documentation change' },
|
|
5
|
+
{ value: 'style', label: 'style · Formatting, no logic change' },
|
|
6
|
+
{ value: 'refactor', label: 'refactor · Refactor without behavior change' },
|
|
7
|
+
{ value: 'test', label: 'test · Add or fix tests' },
|
|
8
|
+
{ value: 'chore', label: 'chore · Build tasks, configs, dependencies' },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
export const COMMIT_FORMATS = [
|
|
12
|
+
{
|
|
13
|
+
value: 'conventional-ticket',
|
|
14
|
+
label: 'feat(PROJ-42): add login screen',
|
|
15
|
+
hint: 'Conventional Commits with ticket',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
value: 'conventional',
|
|
19
|
+
label: 'feat: add login screen',
|
|
20
|
+
hint: 'Conventional Commits, no ticket',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
value: 'ticket-conventional',
|
|
24
|
+
label: '[PROJ-42] feat: add login screen',
|
|
25
|
+
hint: 'Ticket first, then conventional type',
|
|
26
|
+
},
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
export const BRANCH_FORMATS = [
|
|
30
|
+
{
|
|
31
|
+
value: 'type-ticket-name',
|
|
32
|
+
label: 'feat/PROJ-42_add-login-screen',
|
|
33
|
+
hint: 'type/ticket_name',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
value: 'type-name',
|
|
37
|
+
label: 'feat/add-login-screen',
|
|
38
|
+
hint: 'type/name (no ticket in branch)',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
value: 'ticket-type-name',
|
|
42
|
+
label: 'PROJ-42/feat/add-login-screen',
|
|
43
|
+
hint: 'ticket/type/name',
|
|
44
|
+
},
|
|
45
|
+
]
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "uva-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "UVA CLI — Interactive Git workflow automation for teams",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"uva": "./bin.mjs"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18.0.0"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin.mjs",
|
|
14
|
+
"commands/",
|
|
15
|
+
"lib/",
|
|
16
|
+
"templates/"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"cli",
|
|
20
|
+
"git",
|
|
21
|
+
"conventional-commits",
|
|
22
|
+
"workflow",
|
|
23
|
+
"automation",
|
|
24
|
+
"interactive",
|
|
25
|
+
"branching"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "node --test tests/slugify.test.mjs tests/format.test.mjs",
|
|
30
|
+
"lint": "eslint .",
|
|
31
|
+
"format": "prettier --write .",
|
|
32
|
+
"format:check": "prettier --check .",
|
|
33
|
+
"prepare": "lefthook install",
|
|
34
|
+
"prepublishOnly": "npm run format:check && npm run lint && npm test"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@clack/prompts": "^0.9.0",
|
|
38
|
+
"commander": "^12.0.0",
|
|
39
|
+
"picocolors": "^1.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@eslint/js": "^10.0.1",
|
|
43
|
+
"eslint": "^10.10.0",
|
|
44
|
+
"lefthook": "^2.1.12",
|
|
45
|
+
"prettier": "^3.9.6"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# ADR: {{title}}
|
|
2
|
+
|
|
3
|
+
**Date:** {{date}}
|
|
4
|
+
**Author:** {{author}}
|
|
5
|
+
**Status:** Proposed
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
<!-- What is the situation? What forces are at play — technical, product, team, or otherwise?
|
|
12
|
+
Include enough background for someone unfamiliar with the problem to understand it. -->
|
|
13
|
+
|
|
14
|
+
## Decision
|
|
15
|
+
|
|
16
|
+
<!-- State the decision clearly and directly. -->
|
|
17
|
+
|
|
18
|
+
## Rationale
|
|
19
|
+
|
|
20
|
+
<!-- Why this option over the others? What trade-offs were accepted? -->
|
|
21
|
+
|
|
22
|
+
## Consequences
|
|
23
|
+
|
|
24
|
+
### Positive
|
|
25
|
+
|
|
26
|
+
-
|
|
27
|
+
|
|
28
|
+
### Negative
|
|
29
|
+
|
|
30
|
+
-
|
|
31
|
+
|
|
32
|
+
### Risks
|
|
33
|
+
|
|
34
|
+
-
|
|
35
|
+
|
|
36
|
+
## Alternatives Considered
|
|
37
|
+
|
|
38
|
+
| Alternative | Why it was not chosen |
|
|
39
|
+
| :---------- | :-------------------- |
|
|
40
|
+
| | |
|
|
41
|
+
| | |
|
|
42
|
+
|
|
43
|
+
## References
|
|
44
|
+
|
|
45
|
+
-
|