@mihairo/cmt 1.1.6 → 1.3.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 +4 -2
- package/cmt +414 -103
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="logo.png" alt="cmt" width="720" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
> Conventional Commits CLI — zero dependencies,
|
|
5
|
+
> Conventional Commits CLI — zero dependencies, 1300 lines of bash.
|
|
4
6
|
|
|
5
7
|
[](https://npmjs.com/package/@mihairo/cmt)
|
|
6
8
|
[](https://conventionalcommits.org)
|
package/cmt
CHANGED
|
@@ -6,33 +6,67 @@
|
|
|
6
6
|
# =============================================================================
|
|
7
7
|
set -euo pipefail
|
|
8
8
|
|
|
9
|
-
CMT_VERSION="1.
|
|
9
|
+
CMT_VERSION="1.3.0" # x-release-please-version
|
|
10
10
|
CMT_CONFIG_FILE=".cmt.json"
|
|
11
11
|
CMT_SCHEMA_URL="https://raw.githubusercontent.com/mihai-ro/cmt/main/schema/cmt.schema.json"
|
|
12
12
|
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
13
|
+
# Detect if we have a functional TTY for colored output
|
|
14
|
+
_has_color_tty() {
|
|
15
|
+
# Check for NO_COLOR
|
|
16
|
+
if [ -n "${NO_COLOR:-}" ]; then
|
|
17
|
+
return 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Check if stdout is a terminal
|
|
21
|
+
if [ -t 1 ]; then
|
|
22
|
+
return 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Check if we can write to /dev/tty
|
|
26
|
+
if [ -c /dev/tty ] && [ -w /dev/tty ]; then
|
|
27
|
+
return 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
return 1
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# COLORS AND OUTPUT
|
|
34
|
+
# Initialize colors based on terminal capability
|
|
35
|
+
_init_colors() {
|
|
36
|
+
if ! _has_color_tty; then
|
|
37
|
+
RESET='' BOLD='' DIM=''
|
|
38
|
+
GREEN='' YELLOW='' RED='' BLUE='' MUTED='' CYAN=''
|
|
39
|
+
return
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Check for dumb terminal
|
|
43
|
+
case "${TERM:-dumb}" in
|
|
44
|
+
dumb|unknown)
|
|
45
|
+
# Still try if we have a tty
|
|
46
|
+
if ! _has_color_tty; then
|
|
47
|
+
RESET='' BOLD='' DIM=''
|
|
48
|
+
GREEN='' YELLOW='' RED='' BLUE='' MUTED='' CYAN=''
|
|
49
|
+
return
|
|
50
|
+
fi
|
|
51
|
+
;;
|
|
52
|
+
esac
|
|
53
|
+
|
|
54
|
+
RESET=$'\033[0m'
|
|
55
|
+
BOLD=$'\033[1m'
|
|
56
|
+
DIM=$'\033[2m'
|
|
57
|
+
ACCENT=$'\033[38;2;86;182;194m' # cyan #56b6c2
|
|
58
|
+
ACCENT_BOLD=$'\033[1;38;2;86;182;194m'
|
|
59
|
+
GREEN=$'\033[38;2;152;195;121m' # green #98c379 — success
|
|
60
|
+
YELLOW=$'\033[38;2;229;192;123m' # yellow #e5c07b — warning / patch
|
|
61
|
+
RED=$'\033[38;2;224;108;117m' # red #e06c75 — error / breaking
|
|
62
|
+
BLUE=$'\033[38;2;97;175;239m' # blue #61afef — info
|
|
63
|
+
MUTED=$'\033[38;2;92;99;112m' # comment #5c6370 — secondary text
|
|
64
|
+
CYAN="$ACCENT"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_init_colors
|
|
68
|
+
|
|
69
|
+
# output helpers
|
|
36
70
|
info() { printf " ${BLUE}·${RESET} %s\n" "$*"; }
|
|
37
71
|
success() { printf " ${GREEN}✔${RESET} %s\n" "$*"; }
|
|
38
72
|
warn() { printf " ${YELLOW}⚑${RESET} ${YELLOW}%s${RESET}\n" "$*" >&2; }
|
|
@@ -101,9 +135,9 @@ load_config() {
|
|
|
101
135
|
if [[ "$line" =~ \[.*\] ]]; then
|
|
102
136
|
# fully inline: "scopes": ["auth","api"]
|
|
103
137
|
local _s="${line#*[}"; _s="${_s%%]*}"
|
|
104
|
-
while [[ "$_s" =~
|
|
138
|
+
while [[ "$_s" =~ \"([^\"]+)\" ]]; do
|
|
105
139
|
CUSTOM_SCOPES+=("${BASH_REMATCH[1]}")
|
|
106
|
-
_s="${_s
|
|
140
|
+
_s="${_s#*\"[^\"]*\"}"
|
|
107
141
|
done
|
|
108
142
|
elif [[ "$line" == *"["* ]]; then
|
|
109
143
|
collecting_arr="scopes"; array_buf=""
|
|
@@ -115,9 +149,9 @@ load_config() {
|
|
|
115
149
|
if [[ "$line" =~ \[.*\] ]]; then
|
|
116
150
|
local _s="${line#*[}"; _s="${_s%%]*}"
|
|
117
151
|
RULE_ALLOW_BREAKING=()
|
|
118
|
-
while [[ "$_s" =~
|
|
152
|
+
while [[ "$_s" =~ \"([^\"]+)\" ]]; do
|
|
119
153
|
RULE_ALLOW_BREAKING+=("${BASH_REMATCH[1]}")
|
|
120
|
-
_s="${_s
|
|
154
|
+
_s="${_s#*\"[^\"]*\"}"
|
|
121
155
|
done
|
|
122
156
|
[[ ${#RULE_ALLOW_BREAKING[@]} -eq 0 ]] && RULE_ALLOW_BREAKING=("feat" "fix")
|
|
123
157
|
elif [[ "$line" == *"["* ]]; then
|
|
@@ -131,7 +165,6 @@ load_config() {
|
|
|
131
165
|
section="rules"; continue
|
|
132
166
|
fi
|
|
133
167
|
|
|
134
|
-
# ── multi-line array collector ─────────────────────────────────────────
|
|
135
168
|
if [[ -n "$collecting_arr" ]]; then
|
|
136
169
|
if [[ "$line" == *"]"* ]]; then
|
|
137
170
|
# closing bracket — flush accumulated buffer
|
|
@@ -139,15 +172,15 @@ load_config() {
|
|
|
139
172
|
local _s="$array_buf"
|
|
140
173
|
case "$collecting_arr" in
|
|
141
174
|
scopes)
|
|
142
|
-
while [[ "$_s" =~
|
|
175
|
+
while [[ "$_s" =~ \"([^\"]+)\" ]]; do
|
|
143
176
|
CUSTOM_SCOPES+=("${BASH_REMATCH[1]}")
|
|
144
|
-
_s="${_s
|
|
177
|
+
_s="${_s#*\"[^\"]*\"}"
|
|
145
178
|
done
|
|
146
179
|
;;
|
|
147
180
|
allowBreakingChanges)
|
|
148
|
-
while [[ "$_s" =~
|
|
181
|
+
while [[ "$_s" =~ \"([^\"]+)\" ]]; do
|
|
149
182
|
RULE_ALLOW_BREAKING+=("${BASH_REMATCH[1]}")
|
|
150
|
-
_s="${_s
|
|
183
|
+
_s="${_s#*\"[^\"]*\"}"
|
|
151
184
|
done
|
|
152
185
|
[[ ${#RULE_ALLOW_BREAKING[@]} -eq 0 ]] && RULE_ALLOW_BREAKING=("feat" "fix")
|
|
153
186
|
;;
|
|
@@ -159,7 +192,6 @@ load_config() {
|
|
|
159
192
|
continue
|
|
160
193
|
fi
|
|
161
194
|
|
|
162
|
-
# ── customTypes object items ───────────────────────────────────────────
|
|
163
195
|
if [[ "$section" == "customTypes" ]]; then
|
|
164
196
|
if [[ "$line" =~ ^[[:space:]]*\] ]]; then
|
|
165
197
|
section=""; in_obj=0; continue
|
|
@@ -201,13 +233,12 @@ load_config() {
|
|
|
201
233
|
fi
|
|
202
234
|
fi
|
|
203
235
|
|
|
204
|
-
# ── rules scalar fields ────────────────────────────────────────────────
|
|
205
236
|
if [[ "$section" == "rules" ]]; then
|
|
206
237
|
if [[ "$line" =~ ^[[:space:]]*\} ]]; then
|
|
207
238
|
section=""; continue
|
|
208
239
|
fi
|
|
209
240
|
# match: "key": true|false|number — no subprocess needed
|
|
210
|
-
if [[ "$line" =~
|
|
241
|
+
if [[ "$line" =~ \"([^\"]+)\"[[:space:]]*:[[:space:]]*(true|false|[0-9]+) ]]; then
|
|
211
242
|
case "${BASH_REMATCH[1]}" in
|
|
212
243
|
maxHeaderLength) RULE_MAX_HEADER="${BASH_REMATCH[2]}" ;;
|
|
213
244
|
requireScope) [[ "${BASH_REMATCH[2]}" == "true" ]] && RULE_REQUIRE_SCOPE=1 ;;
|
|
@@ -221,13 +252,15 @@ load_config() {
|
|
|
221
252
|
}
|
|
222
253
|
|
|
223
254
|
# type selection UI
|
|
224
|
-
# arrow-key picker
|
|
255
|
+
# arrow-key picker with incremental search
|
|
225
256
|
# Usage: _pick <result_var> <prompt> <item1> <item2> ...
|
|
226
|
-
#
|
|
257
|
+
# Sets result_var to chosen index (0-based, original item list). Renders on /dev/tty.
|
|
227
258
|
_pick() {
|
|
228
259
|
local _var="$1" _prompt="$2"; shift 2
|
|
229
260
|
local -a _items=("$@")
|
|
230
261
|
local _n=${#_items[@]} _cur=0 _top=0 _view=7 _k1 _k2 _k3
|
|
262
|
+
local _filter="" _nv=0
|
|
263
|
+
local -a _visible=() _plain=()
|
|
231
264
|
|
|
232
265
|
exec 9<>/dev/tty
|
|
233
266
|
|
|
@@ -237,11 +270,52 @@ _pick() {
|
|
|
237
270
|
|
|
238
271
|
trap "trap - EXIT INT TERM; stty '$_old' <&9 2>/dev/null; printf '\033[?25h' >&9 2>/dev/null; exec 9>&- 2>/dev/null" EXIT INT TERM
|
|
239
272
|
|
|
273
|
+
# Pre-strip ANSI codes for search matching (bash 3.2 compatible char scan)
|
|
274
|
+
_strip_ansi_plain() {
|
|
275
|
+
local _s="$1" _out="" _i=0 _len _c
|
|
276
|
+
_len=${#_s}
|
|
277
|
+
while (( _i < _len )); do
|
|
278
|
+
_c="${_s:$_i:1}"
|
|
279
|
+
if [[ "$_c" == $'\033' && "${_s:$((_i+1)):1}" == "[" ]]; then
|
|
280
|
+
(( _i += 2 ))
|
|
281
|
+
while (( _i < _len )); do
|
|
282
|
+
_c="${_s:$_i:1}"
|
|
283
|
+
(( _i++ ))
|
|
284
|
+
[[ "$_c" =~ [a-zA-Z] ]] && break
|
|
285
|
+
done
|
|
286
|
+
else
|
|
287
|
+
_out+="$_c"
|
|
288
|
+
(( _i++ ))
|
|
289
|
+
fi
|
|
290
|
+
done
|
|
291
|
+
printf '%s' "$_out"
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
local _pi
|
|
295
|
+
for (( _pi=0; _pi<_n; _pi++ )); do
|
|
296
|
+
_plain[$_pi]=$(_strip_ansi_plain "${_items[$_pi]}")
|
|
297
|
+
done
|
|
298
|
+
|
|
299
|
+
# Rebuild _visible indices based on current _filter
|
|
300
|
+
_rebuild_visible() {
|
|
301
|
+
_visible=()
|
|
302
|
+
local _i
|
|
303
|
+
for (( _i=0; _i<_n; _i++ )); do
|
|
304
|
+
if [[ -z "$_filter" || "${_plain[$_i]}" == *"$_filter"* ]]; then
|
|
305
|
+
_visible+=("$_i")
|
|
306
|
+
fi
|
|
307
|
+
done
|
|
308
|
+
_nv=${#_visible[@]}
|
|
309
|
+
(( _cur >= _nv && _nv > 0 )) && _cur=$(( _nv - 1 ))
|
|
310
|
+
[[ $_nv -eq 0 ]] && _cur=0
|
|
311
|
+
_top=0
|
|
312
|
+
}
|
|
313
|
+
|
|
240
314
|
_pick_move_up() {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
_top=$((
|
|
315
|
+
[[ $_nv -eq 0 ]] && return
|
|
316
|
+
_cur=$(( (_cur - 1 + _nv) % _nv ))
|
|
317
|
+
if [[ $_cur -eq $(( _nv - 1 )) ]]; then
|
|
318
|
+
_top=$(( _nv - _view ))
|
|
245
319
|
[[ $_top -lt 0 ]] && _top=0 || true
|
|
246
320
|
elif [[ $_cur -lt $_top ]]; then
|
|
247
321
|
_top=$(( _cur ))
|
|
@@ -249,36 +323,49 @@ _pick() {
|
|
|
249
323
|
}
|
|
250
324
|
|
|
251
325
|
_pick_move_down() {
|
|
252
|
-
|
|
326
|
+
[[ $_nv -eq 0 ]] && return
|
|
327
|
+
_cur=$(( (_cur + 1) % _nv ))
|
|
253
328
|
if [[ $_cur -ge $(( _top + _view )) ]]; then
|
|
254
329
|
_top=$(( _cur - _view + 1 ))
|
|
255
330
|
elif [[ $_cur -eq 0 ]]; then
|
|
256
|
-
# wrapped from bottom to top
|
|
257
331
|
_top=0
|
|
258
332
|
fi
|
|
259
333
|
}
|
|
260
334
|
|
|
261
335
|
# draw the visible window of _view items starting at _top
|
|
262
336
|
_draw() {
|
|
263
|
-
local _i _end
|
|
337
|
+
local _i _end _vi
|
|
338
|
+
|
|
339
|
+
# search line
|
|
340
|
+
if [[ -n "$_filter" ]]; then
|
|
341
|
+
printf " ${ACCENT}/ %s${RESET}\033[K\n" "$_filter" >&9
|
|
342
|
+
else
|
|
343
|
+
printf " ${MUTED}/ type to filter${RESET}\033[K\n" >&9
|
|
344
|
+
fi
|
|
345
|
+
|
|
264
346
|
_end=$(( _top + _view ))
|
|
265
|
-
[[ $_end -gt
|
|
347
|
+
[[ $_end -gt _nv ]] && _end=$_nv
|
|
266
348
|
|
|
267
|
-
# scroll hint above
|
|
349
|
+
# scroll hint above
|
|
268
350
|
if [[ $_top -gt 0 ]]; then
|
|
269
351
|
printf " ${MUTED}↑ %d more${RESET}\033[K\n" "$_top" >&9
|
|
270
352
|
fi
|
|
271
353
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
354
|
+
if [[ $_nv -eq 0 ]]; then
|
|
355
|
+
printf " ${MUTED}no matches${RESET}\033[K\n" >&9
|
|
356
|
+
else
|
|
357
|
+
for (( _i=_top; _i<_end; _i++ )); do
|
|
358
|
+
_vi=${_visible[$_i]}
|
|
359
|
+
if (( _i == _cur )); then
|
|
360
|
+
printf " ${ACCENT_BOLD}❯${RESET} %s${RESET}\033[K\n" "${_items[$_vi]}" >&9
|
|
361
|
+
else
|
|
362
|
+
printf " ${MUTED}·${RESET} ${MUTED}%s${RESET}\033[K\n" "${_items[$_vi]}" >&9
|
|
363
|
+
fi
|
|
364
|
+
done
|
|
365
|
+
fi
|
|
279
366
|
|
|
280
367
|
# scroll hint below
|
|
281
|
-
local _remaining=$((
|
|
368
|
+
local _remaining=$(( _nv - _end ))
|
|
282
369
|
if [[ $_remaining -gt 0 ]]; then
|
|
283
370
|
printf " ${MUTED}↓ %d more${RESET}\033[K\n" "$_remaining" >&9
|
|
284
371
|
else
|
|
@@ -286,6 +373,8 @@ _pick() {
|
|
|
286
373
|
fi
|
|
287
374
|
}
|
|
288
375
|
|
|
376
|
+
_rebuild_visible
|
|
377
|
+
|
|
289
378
|
printf "\033[?25l" >&9
|
|
290
379
|
printf "\n ${ACCENT_BOLD}%s${RESET}\n\n" "$_prompt" >&9
|
|
291
380
|
printf "\033[s" >&9
|
|
@@ -302,27 +391,49 @@ _pick() {
|
|
|
302
391
|
A) _pick_move_up ;;
|
|
303
392
|
B) _pick_move_down ;;
|
|
304
393
|
esac
|
|
394
|
+
elif [[ -z "$_k2" ]]; then
|
|
395
|
+
# lone Escape: clear filter
|
|
396
|
+
_filter=""; _rebuild_visible
|
|
305
397
|
fi
|
|
306
398
|
;;
|
|
307
399
|
$'\r'|$'\n'|'') break ;;
|
|
400
|
+
$'\177'|$'\010')
|
|
401
|
+
# backspace — remove last filter char
|
|
402
|
+
_filter="${_filter%?}"; _rebuild_visible ;;
|
|
308
403
|
k) _pick_move_up ;;
|
|
309
404
|
j) _pick_move_down ;;
|
|
310
|
-
|
|
405
|
+
$'\003') stty "$_old" <&9 2>/dev/null; trap - EXIT INT TERM; exec 9>&-; exit 1 ;;
|
|
406
|
+
q)
|
|
407
|
+
if [[ -z "$_filter" ]]; then
|
|
408
|
+
stty "$_old" <&9 2>/dev/null; trap - EXIT INT TERM; exec 9>&-; exit 1
|
|
409
|
+
else
|
|
410
|
+
_filter+="q"; _rebuild_visible
|
|
411
|
+
fi
|
|
412
|
+
;;
|
|
413
|
+
[[:print:]])
|
|
414
|
+
# any other printable char goes into the search filter
|
|
415
|
+
_filter+="$_k1"; _rebuild_visible ;;
|
|
311
416
|
esac
|
|
312
417
|
printf "\033[u\033[J" >&9
|
|
313
418
|
_draw
|
|
314
419
|
done
|
|
315
420
|
|
|
421
|
+
# nothing matched — fall back to first item
|
|
422
|
+
[[ $_nv -eq 0 ]] && { printf -v "$_var" '%d' 0; } && {
|
|
423
|
+
stty "$_old" <&9 2>/dev/null; trap - EXIT INT TERM; printf "\033[?25h" >&9; exec 9>&-; return; }
|
|
424
|
+
|
|
425
|
+
local _selected_orig=${_visible[$_cur]}
|
|
426
|
+
|
|
316
427
|
# collapse to just the selected line
|
|
317
428
|
printf "\033[u\033[J" >&9
|
|
318
|
-
printf " ${ACCENT_BOLD}❯${RESET} ${BOLD}%s${RESET}\n" "${_items[$
|
|
429
|
+
printf " ${ACCENT_BOLD}❯${RESET} ${BOLD}%s${RESET}\n" "${_items[$_selected_orig]}" >&9
|
|
319
430
|
|
|
320
431
|
stty "$_old" <&9 2>/dev/null
|
|
321
432
|
trap - EXIT INT TERM
|
|
322
433
|
printf "\033[?25h" >&9
|
|
323
434
|
exec 9>&-
|
|
324
435
|
|
|
325
|
-
printf -v "$_var" '%d' "$
|
|
436
|
+
printf -v "$_var" '%d' "$_selected_orig"
|
|
326
437
|
}
|
|
327
438
|
|
|
328
439
|
|
|
@@ -412,8 +523,8 @@ input_description() {
|
|
|
412
523
|
read -r desc </dev/tty
|
|
413
524
|
if [[ -z "$desc" ]]; then
|
|
414
525
|
warn "Description is required"
|
|
415
|
-
elif (( ${#desc} >
|
|
416
|
-
warn "Description is ${#desc} chars — keep it under
|
|
526
|
+
elif (( ${#desc} > RULE_MAX_HEADER )); then
|
|
527
|
+
warn "Description is ${#desc} chars — keep it under ${RULE_MAX_HEADER} for readability"
|
|
417
528
|
printf " ${YELLOW}use anyway?${RESET} ${MUTED}[y/N]${RESET} ${ACCENT}›${RESET} " >/dev/tty
|
|
418
529
|
read -r confirm </dev/tty
|
|
419
530
|
[[ "$confirm" =~ ^[yY]$ ]] && { SELECTED_DESC="$desc"; return 0; }
|
|
@@ -424,13 +535,22 @@ input_description() {
|
|
|
424
535
|
done
|
|
425
536
|
}
|
|
426
537
|
|
|
427
|
-
# body input
|
|
538
|
+
# body input — multi-line; empty line to finish
|
|
428
539
|
input_body() {
|
|
429
|
-
printf "\n ${ACCENT_BOLD}body${RESET} ${MUTED}optional —
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
540
|
+
printf "\n ${ACCENT_BOLD}body${RESET} ${MUTED}optional — empty line to finish${RESET}\n" >/dev/tty
|
|
541
|
+
local -a _lines=() _l
|
|
542
|
+
while true; do
|
|
543
|
+
printf " ${ACCENT}›${RESET} " >/dev/tty
|
|
544
|
+
IFS= read -r _l </dev/tty
|
|
545
|
+
[[ -z "$_l" ]] && break
|
|
546
|
+
_lines+=("$_l")
|
|
547
|
+
done
|
|
548
|
+
if (( ${#_lines[@]} > 0 )); then
|
|
549
|
+
SELECTED_BODY="$(printf '%s\n' "${_lines[@]}")"
|
|
550
|
+
SELECTED_BODY="${SELECTED_BODY%$'\n'}" # trim trailing newline
|
|
551
|
+
else
|
|
552
|
+
SELECTED_BODY=""
|
|
553
|
+
fi
|
|
434
554
|
}
|
|
435
555
|
|
|
436
556
|
# breaking change
|
|
@@ -486,6 +606,25 @@ assemble_message() {
|
|
|
486
606
|
COMMIT_MESSAGE="$msg"
|
|
487
607
|
}
|
|
488
608
|
|
|
609
|
+
# word-wrap: prints wrapped lines of $1 at most $2 chars wide.
|
|
610
|
+
_wordwrap() {
|
|
611
|
+
local _text="$1" _width="$2" _line="" _word
|
|
612
|
+
local -a _words
|
|
613
|
+
read -ra _words <<< "$_text"
|
|
614
|
+
if [[ ${#_words[@]} -eq 0 ]]; then printf '\n'; return; fi
|
|
615
|
+
for _word in "${_words[@]}"; do
|
|
616
|
+
if [[ -z "$_line" ]]; then
|
|
617
|
+
_line="$_word"
|
|
618
|
+
elif (( ${#_line} + 1 + ${#_word} <= _width )); then
|
|
619
|
+
_line="$_line $_word"
|
|
620
|
+
else
|
|
621
|
+
printf '%s\n' "$_line"
|
|
622
|
+
_line="$_word"
|
|
623
|
+
fi
|
|
624
|
+
done
|
|
625
|
+
[[ -n "$_line" ]] && printf '%s\n' "$_line"
|
|
626
|
+
}
|
|
627
|
+
|
|
489
628
|
_print_commit_box() {
|
|
490
629
|
local msg="$1" _max=0 _line _W _content_w _bar _bottom _max_W
|
|
491
630
|
|
|
@@ -504,17 +643,10 @@ _print_commit_box() {
|
|
|
504
643
|
printf -v _bar '%*s' $(( _W - 19 )) ''; _bar=${_bar// /─}
|
|
505
644
|
printf "\n ${MUTED}╭─ commit message %s╮${RESET}\n" "$_bar" >/dev/tty
|
|
506
645
|
while IFS= read -r _line; do
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
for (i = 1; i <= NF; i++) {
|
|
512
|
-
if (line == "") { line = $i }
|
|
513
|
-
else if (length(line) + 1 + length($i) <= w) { line = line " " $i }
|
|
514
|
-
else { print line; line = $i }
|
|
515
|
-
}
|
|
516
|
-
if (line != "") print line
|
|
517
|
-
}')"
|
|
646
|
+
while IFS= read -r _wrapped; do
|
|
647
|
+
printf " ${MUTED}│${RESET} ${BOLD}%-${_content_w}s${RESET} ${MUTED}│${RESET}\n" "$_wrapped" >/dev/tty
|
|
648
|
+
done < <(_wordwrap "$_line" "$_content_w")
|
|
649
|
+
done <<< "$msg"
|
|
518
650
|
printf -v _bottom '%*s' $(( _W - 2 )) ''; _bottom=${_bottom// /─}
|
|
519
651
|
printf " ${MUTED}╰%s╯${RESET}\n" "$_bottom" >/dev/tty
|
|
520
652
|
}
|
|
@@ -552,10 +684,14 @@ confirm_and_commit() {
|
|
|
552
684
|
cmd_commit() {
|
|
553
685
|
# --write-only: used by prepare-commit-msg hook — run picker, print message to
|
|
554
686
|
# stdout, do NOT call git commit (git will do that after the hook returns).
|
|
555
|
-
|
|
556
|
-
|
|
687
|
+
# --dry-run: run the picker, print the assembled message, do NOT commit.
|
|
688
|
+
local write_only=0 dry_run=0
|
|
689
|
+
for _a in "$@"; do
|
|
690
|
+
[[ "$_a" == "--write-only" ]] && write_only=1
|
|
691
|
+
[[ "$_a" == "--dry-run" ]] && dry_run=1
|
|
692
|
+
done
|
|
557
693
|
|
|
558
|
-
if [[ $write_only -eq 0 ]]; then
|
|
694
|
+
if [[ $write_only -eq 0 && $dry_run -eq 0 ]]; then
|
|
559
695
|
git rev-parse --git-dir > /dev/null 2>&1 || die "Not inside a git repository"
|
|
560
696
|
if git diff --cached --quiet 2>/dev/null; then
|
|
561
697
|
warn "No staged changes detected. Did you forget \`git add\`?"
|
|
@@ -585,6 +721,13 @@ cmd_commit() {
|
|
|
585
721
|
return 0
|
|
586
722
|
fi
|
|
587
723
|
|
|
724
|
+
if [[ $dry_run -eq 1 ]]; then
|
|
725
|
+
_print_commit_box "$COMMIT_MESSAGE"
|
|
726
|
+
printf "\n"
|
|
727
|
+
printf '%s\n' "$COMMIT_MESSAGE"
|
|
728
|
+
return 0
|
|
729
|
+
fi
|
|
730
|
+
|
|
588
731
|
confirm_and_commit
|
|
589
732
|
}
|
|
590
733
|
|
|
@@ -702,9 +845,12 @@ _hook_snippet() {
|
|
|
702
845
|
cat << 'SNIPPET'
|
|
703
846
|
# >>> cmt — Conventional Commits CLI
|
|
704
847
|
[ -n "${2:-}" ] && exit 0
|
|
848
|
+
_cmt_bin=""
|
|
705
849
|
for _p in "./node_modules/.bin/cmt" "$HOME/.local/bin/cmt" "/usr/local/bin/cmt" "/opt/homebrew/bin/cmt"; do
|
|
706
|
-
[ -x "$_p" ] && { "$_p"
|
|
850
|
+
[ -x "$_p" ] && { _cmt_bin="$_p"; break; }
|
|
707
851
|
done
|
|
852
|
+
[ -z "$_cmt_bin" ] && _cmt_bin=$(command -v cmt 2>/dev/null)
|
|
853
|
+
[ -n "$_cmt_bin" ] && { "$_cmt_bin" commit --write-only > "$1"; exit 0; }
|
|
708
854
|
exit 0
|
|
709
855
|
# <<< cmt
|
|
710
856
|
SNIPPET
|
|
@@ -714,9 +860,12 @@ SNIPPET
|
|
|
714
860
|
_lint_snippet() {
|
|
715
861
|
cat << 'SNIPPET'
|
|
716
862
|
# >>> cmt-lint — Conventional Commits CLI
|
|
863
|
+
_cmt_bin=""
|
|
717
864
|
for _p in "./node_modules/.bin/cmt" "$HOME/.local/bin/cmt" "/usr/local/bin/cmt" "/opt/homebrew/bin/cmt"; do
|
|
718
|
-
[ -x "$_p" ] && { "$_p"
|
|
865
|
+
[ -x "$_p" ] && { _cmt_bin="$_p"; break; }
|
|
719
866
|
done
|
|
867
|
+
[ -z "$_cmt_bin" ] && _cmt_bin=$(command -v cmt 2>/dev/null)
|
|
868
|
+
[ -n "$_cmt_bin" ] && { "$_cmt_bin" lint "$1"; exit $?; }
|
|
720
869
|
exit 0
|
|
721
870
|
# <<< cmt-lint
|
|
722
871
|
SNIPPET
|
|
@@ -753,13 +902,30 @@ cmd_log() {
|
|
|
753
902
|
git rev-parse --git-dir > /dev/null 2>&1 || die "Not inside a git repository"
|
|
754
903
|
load_config
|
|
755
904
|
|
|
756
|
-
local limit=
|
|
757
|
-
|
|
905
|
+
local limit=20 filter_type="" filter_author=""
|
|
906
|
+
while [[ $# -gt 0 ]]; do
|
|
907
|
+
case "$1" in
|
|
908
|
+
--type=*) filter_type="${1#--type=}" ;;
|
|
909
|
+
--author=*) filter_author="${1#--author=}" ;;
|
|
910
|
+
[0-9]*) limit="$1" ;;
|
|
911
|
+
esac
|
|
912
|
+
shift
|
|
913
|
+
done
|
|
914
|
+
|
|
915
|
+
local _label="last ${limit} commits"
|
|
916
|
+
[[ -n "$filter_type" ]] && _label+=" ${MUTED}type=${filter_type}${RESET}"
|
|
917
|
+
[[ -n "$filter_author" ]] && _label+=" ${MUTED}author=${filter_author}${RESET}"
|
|
918
|
+
printf "\n ${ACCENT_BOLD}log${RESET} ${MUTED}%s${RESET}\n" "$_label"
|
|
758
919
|
|
|
759
920
|
local _p_log_type='^([a-z][a-z0-9_-]*)[(:!]'
|
|
760
921
|
while IFS='|' read -r hash short_hash date author subject; do
|
|
761
922
|
local type="" emoji color
|
|
762
923
|
[[ "$subject" =~ $_p_log_type ]] && type="${BASH_REMATCH[1]}"
|
|
924
|
+
|
|
925
|
+
# apply filters
|
|
926
|
+
[[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue
|
|
927
|
+
[[ -n "$filter_author" && "$author" != *"$filter_author"* ]] && continue
|
|
928
|
+
|
|
763
929
|
emoji="·"; color="$RESET"
|
|
764
930
|
|
|
765
931
|
if [[ -n "$type" ]]; then
|
|
@@ -918,6 +1084,129 @@ cmd_uninstall() {
|
|
|
918
1084
|
}
|
|
919
1085
|
|
|
920
1086
|
|
|
1087
|
+
# cmd: amend — interactively rebuild or raw-edit the last commit message
|
|
1088
|
+
cmd_amend() {
|
|
1089
|
+
git rev-parse --git-dir > /dev/null 2>&1 || die "Not inside a git repository"
|
|
1090
|
+
git log -1 > /dev/null 2>&1 || die "No commits yet"
|
|
1091
|
+
|
|
1092
|
+
local last_msg
|
|
1093
|
+
last_msg=$(git log -1 --format=%B)
|
|
1094
|
+
_print_commit_box "$last_msg"
|
|
1095
|
+
|
|
1096
|
+
printf "\n ${ACCENT_BOLD}amend${RESET} ${MUTED}[Y=rebuild / e=edit raw / N=abort]${RESET} ${ACCENT}›${RESET} " >/dev/tty
|
|
1097
|
+
read -r _ans </dev/tty
|
|
1098
|
+
|
|
1099
|
+
case "$_ans" in
|
|
1100
|
+
e|E)
|
|
1101
|
+
local tmpfile; tmpfile=$(mktemp /tmp/cmt-amend-XXXXXX)
|
|
1102
|
+
printf '%s' "$last_msg" > "$tmpfile"
|
|
1103
|
+
"${EDITOR:-vi}" "$tmpfile"
|
|
1104
|
+
COMMIT_MESSAGE=$(< "$tmpfile"); rm -f "$tmpfile"
|
|
1105
|
+
git commit --amend --no-edit -m "$COMMIT_MESSAGE"
|
|
1106
|
+
printf "\n"; success "${BOLD}amended${RESET}"
|
|
1107
|
+
;;
|
|
1108
|
+
""|y|Y)
|
|
1109
|
+
load_config
|
|
1110
|
+
SELECTED_TYPE="" SELECTED_SCOPE="" SELECTED_DESC=""
|
|
1111
|
+
SELECTED_BODY="" SELECTED_FOOTER="" IS_BREAKING=0 BREAKING_DESC=""
|
|
1112
|
+
select_type; select_scope; input_description
|
|
1113
|
+
input_body; input_breaking; input_footer
|
|
1114
|
+
assemble_message
|
|
1115
|
+
_print_commit_box "$COMMIT_MESSAGE"
|
|
1116
|
+
printf "\n ${ACCENT_BOLD}commit --amend?${RESET} ${MUTED}[Y/n]${RESET} ${ACCENT}›${RESET} " >/dev/tty
|
|
1117
|
+
read -r _c </dev/tty
|
|
1118
|
+
[[ "$_c" =~ ^[nN]$ ]] && { warn "Amend aborted."; exit 0; }
|
|
1119
|
+
git commit --amend --no-edit -m "$COMMIT_MESSAGE"
|
|
1120
|
+
printf "\n"; success "${BOLD}amended${RESET}"
|
|
1121
|
+
;;
|
|
1122
|
+
*) warn "Amend aborted."; exit 0 ;;
|
|
1123
|
+
esac
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
# cmd: status — show staged changes before committing
|
|
1127
|
+
cmd_status() {
|
|
1128
|
+
git rev-parse --git-dir > /dev/null 2>&1 || die "Not inside a git repository"
|
|
1129
|
+
printf "\n ${ACCENT_BOLD}staged${RESET}\n\n"
|
|
1130
|
+
local staged=0
|
|
1131
|
+
while IFS= read -r _line; do
|
|
1132
|
+
local _x="${_line:0:1}" _f="${_line:3}"
|
|
1133
|
+
case "$_x" in
|
|
1134
|
+
A) printf " ${GREEN}+${RESET} %-40s ${MUTED}added${RESET}\n" "$_f"; (( staged++ )) ;;
|
|
1135
|
+
M) printf " ${BLUE}~${RESET} %-40s ${MUTED}modified${RESET}\n" "$_f"; (( staged++ )) ;;
|
|
1136
|
+
D) printf " ${RED}-${RESET} %-40s ${MUTED}deleted${RESET}\n" "$_f"; (( staged++ )) ;;
|
|
1137
|
+
R) printf " ${YELLOW}»${RESET} %-40s ${MUTED}renamed${RESET}\n" "$_f"; (( staged++ )) ;;
|
|
1138
|
+
C) printf " ${YELLOW}»${RESET} %-40s ${MUTED}copied${RESET}\n" "$_f"; (( staged++ )) ;;
|
|
1139
|
+
esac
|
|
1140
|
+
done < <(git status --porcelain 2>/dev/null)
|
|
1141
|
+
[[ $staged -eq 0 ]] && warn "Nothing staged. Run: git add <file>"
|
|
1142
|
+
printf "\n"
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
# cmd: completions — print shell completion scripts
|
|
1146
|
+
cmd_completions() {
|
|
1147
|
+
local _shell="${1:-bash}"
|
|
1148
|
+
case "$_shell" in
|
|
1149
|
+
bash) _completions_bash ;;
|
|
1150
|
+
zsh) _completions_zsh ;;
|
|
1151
|
+
fish) _completions_fish ;;
|
|
1152
|
+
*) die "Unknown shell: $_shell (use: bash, zsh, or fish)" ;;
|
|
1153
|
+
esac
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
_completions_bash() {
|
|
1157
|
+
cat << 'EOF'
|
|
1158
|
+
# cmt bash completions — add to ~/.bashrc: eval "$(cmt completions bash)"
|
|
1159
|
+
_cmt_completions() {
|
|
1160
|
+
local cur="${COMP_WORDS[COMP_CWORD]}"
|
|
1161
|
+
local cmds="commit lint init log types status amend uninstall completions version help"
|
|
1162
|
+
COMPREPLY=( $(compgen -W "$cmds" -- "$cur") )
|
|
1163
|
+
}
|
|
1164
|
+
complete -F _cmt_completions cmt
|
|
1165
|
+
EOF
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
_completions_zsh() {
|
|
1169
|
+
cat << 'EOF'
|
|
1170
|
+
# cmt zsh completions — add to ~/.zshrc: eval "$(cmt completions zsh)"
|
|
1171
|
+
_cmt() {
|
|
1172
|
+
local -a cmds
|
|
1173
|
+
cmds=(
|
|
1174
|
+
'commit:Interactive commit builder'
|
|
1175
|
+
'lint:Lint a commit message file or stdin'
|
|
1176
|
+
'init:Set up repo with hook and config'
|
|
1177
|
+
'log:Pretty log of recent commits'
|
|
1178
|
+
'types:List available commit types'
|
|
1179
|
+
'status:Show staged changes'
|
|
1180
|
+
'amend:Amend the last commit'
|
|
1181
|
+
'uninstall:Remove cmt-managed hooks'
|
|
1182
|
+
'completions:Print shell completion scripts'
|
|
1183
|
+
'version:Print version'
|
|
1184
|
+
'help:Show help'
|
|
1185
|
+
)
|
|
1186
|
+
_describe 'command' cmds
|
|
1187
|
+
}
|
|
1188
|
+
compdef _cmt cmt
|
|
1189
|
+
EOF
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
_completions_fish() {
|
|
1193
|
+
cat << 'EOF'
|
|
1194
|
+
# cmt fish completions — add to config.fish: cmt completions fish | source
|
|
1195
|
+
complete -c cmt -f
|
|
1196
|
+
complete -c cmt -n '__fish_use_subcommand' -a commit -d 'Interactive commit builder'
|
|
1197
|
+
complete -c cmt -n '__fish_use_subcommand' -a lint -d 'Lint a commit message'
|
|
1198
|
+
complete -c cmt -n '__fish_use_subcommand' -a init -d 'Set up repo with hook and config'
|
|
1199
|
+
complete -c cmt -n '__fish_use_subcommand' -a log -d 'Pretty log of recent commits'
|
|
1200
|
+
complete -c cmt -n '__fish_use_subcommand' -a types -d 'List available commit types'
|
|
1201
|
+
complete -c cmt -n '__fish_use_subcommand' -a status -d 'Show staged changes'
|
|
1202
|
+
complete -c cmt -n '__fish_use_subcommand' -a amend -d 'Amend the last commit'
|
|
1203
|
+
complete -c cmt -n '__fish_use_subcommand' -a uninstall -d 'Remove cmt-managed hooks'
|
|
1204
|
+
complete -c cmt -n '__fish_use_subcommand' -a completions -d 'Print shell completion scripts'
|
|
1205
|
+
complete -c cmt -n '__fish_use_subcommand' -a version -d 'Print version'
|
|
1206
|
+
complete -c cmt -n '__fish_use_subcommand' -a help -d 'Show help'
|
|
1207
|
+
EOF
|
|
1208
|
+
}
|
|
1209
|
+
|
|
921
1210
|
# cmd: version
|
|
922
1211
|
cmd_version() {
|
|
923
1212
|
printf "cmt version %s\n" "$CMT_VERSION"
|
|
@@ -932,27 +1221,44 @@ ${BOLD}USAGE${RESET}
|
|
|
932
1221
|
cmt <command> [options]
|
|
933
1222
|
|
|
934
1223
|
${BOLD}COMMANDS${RESET}
|
|
935
|
-
${CYAN}init${RESET} [--husky] [--lint]
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1224
|
+
${CYAN}init${RESET} [--husky] [--lint] Create .cmt.json + install prepare-commit-msg hook
|
|
1225
|
+
--husky write to .husky/ (husky v9, committable)
|
|
1226
|
+
--lint also install commit-msg linting hook
|
|
1227
|
+
default write to .git/hooks/ (local)
|
|
1228
|
+
|
|
1229
|
+
${CYAN}commit${RESET} [--dry-run] Interactive commit builder
|
|
1230
|
+
--dry-run show assembled message, do not commit
|
|
1231
|
+
|
|
1232
|
+
${CYAN}amend${RESET} Amend the last commit (rebuild or raw edit)
|
|
939
1233
|
|
|
940
|
-
${CYAN}
|
|
1234
|
+
${CYAN}status${RESET} Show staged changes
|
|
941
1235
|
|
|
942
|
-
${CYAN}lint${RESET} [file]
|
|
1236
|
+
${CYAN}lint${RESET} [file] Lint a commit message file or stdin
|
|
943
1237
|
|
|
944
|
-
${CYAN}log${RESET} [n]
|
|
1238
|
+
${CYAN}log${RESET} [n] [--type=<t>] Pretty log — last n commits (default: 20)
|
|
1239
|
+
[--author=<a>] filter by type or author substring
|
|
945
1240
|
|
|
946
|
-
${CYAN}types${RESET}
|
|
1241
|
+
${CYAN}types${RESET} List available types
|
|
947
1242
|
|
|
948
|
-
${CYAN}
|
|
1243
|
+
${CYAN}completions${RESET} [bash|zsh|fish] Print shell completion script
|
|
1244
|
+
|
|
1245
|
+
${CYAN}uninstall${RESET} Remove cmt-managed hooks
|
|
949
1246
|
|
|
950
1247
|
${BOLD}EXAMPLES${RESET}
|
|
951
|
-
cmt init
|
|
952
|
-
cmt init --husky
|
|
953
|
-
cmt commit
|
|
1248
|
+
cmt init # set up repo — done in one step
|
|
1249
|
+
cmt init --husky # same, but writes to .husky/ for husky v9
|
|
1250
|
+
cmt commit # guided commit
|
|
1251
|
+
cmt commit --dry-run # preview message without committing
|
|
1252
|
+
cmt amend # fix last commit
|
|
1253
|
+
cmt status # see what's staged
|
|
1254
|
+
cmt log 10 --type=feat # last 10 feat commits
|
|
954
1255
|
echo 'fix: typo' | cmt lint
|
|
955
|
-
git log --format=%s | cmt lint
|
|
1256
|
+
git log --format=%s | cmt lint # lint every commit on a branch
|
|
1257
|
+
|
|
1258
|
+
${BOLD}SHELL COMPLETIONS${RESET}
|
|
1259
|
+
bash Add to ~/.bashrc: eval \"\$(cmt completions bash)\"
|
|
1260
|
+
zsh Add to ~/.zshrc: eval \"\$(cmt completions zsh)\"
|
|
1261
|
+
fish Add to config.fish: cmt completions fish | source
|
|
956
1262
|
|
|
957
1263
|
${BOLD}INTEGRATIONS${RESET}
|
|
958
1264
|
Husky v9 cmt init --husky → .husky/prepare-commit-msg (commit it, team shares it)
|
|
@@ -974,15 +1280,20 @@ main() {
|
|
|
974
1280
|
local cmd="${1:-commit}"
|
|
975
1281
|
[[ $# -gt 0 ]] && shift
|
|
976
1282
|
case "$cmd" in
|
|
977
|
-
commit|c)
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1283
|
+
commit|c) cmd_commit "$@" ;;
|
|
1284
|
+
amend|a) cmd_amend "$@" ;;
|
|
1285
|
+
lint|l) cmd_lint "$@" ;;
|
|
1286
|
+
init) cmd_init "$@" ;;
|
|
1287
|
+
log) cmd_log "$@" ;;
|
|
1288
|
+
status|s) cmd_status "$@" ;;
|
|
1289
|
+
types|t) cmd_types "$@" ;;
|
|
1290
|
+
completions) cmd_completions "$@" ;;
|
|
1291
|
+
uninstall) cmd_uninstall "$@" ;;
|
|
983
1292
|
version|-v|--version) cmd_version ;;
|
|
984
1293
|
help|-h|--help|*) cmd_help ;;
|
|
985
1294
|
esac
|
|
986
1295
|
}
|
|
987
1296
|
|
|
1297
|
+
# Allow sourcing for tests without executing main
|
|
1298
|
+
[[ "${CMT_SOURCED:-0}" == "1" ]] && return 0
|
|
988
1299
|
main "$@"
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mihairo/cmt",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Zero-dependency
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Zero-dependency commit messages that actually follow the spec. One 1300-line bash script.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"conventional-commits",
|
|
7
7
|
"commit",
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
},
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
|
-
"url": "https://github.com/mihai-ro/cmt.git"
|
|
21
|
+
"url": "git+https://github.com/mihai-ro/cmt.git"
|
|
22
22
|
},
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"bin": {
|
|
25
|
-
"cmt": "
|
|
25
|
+
"cmt": "cmt"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"cmt",
|