xgem-cli 2.0.0-alpha.12 → 2.0.0-alpha.15
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 +8 -0
- package/bin/xgem +41 -2
- package/bin/xgem.js +22 -35
- package/lib/bootstrap.sh +165 -0
- package/lib/ci.sh +44 -0
- package/lib/create.sh +1 -9
- package/lib/git.sh +267 -17
- package/lib/registry.sh +45 -0
- package/lib/release.sh +185 -0
- package/lib/status.sh +42 -0
- package/lib/utils.sh +11 -0
- package/lib/version.sh +1 -1
- package/lib-win/bootstrap.js +189 -0
- package/lib-win/ci.js +43 -0
- package/lib-win/git.js +238 -6
- package/lib-win/registry.js +43 -0
- package/lib-win/release.js +143 -0
- package/lib-win/scaffold.js +41 -1
- package/lib-win/status.js +53 -0
- package/lib-win/utils.js +6 -1
- package/package.json +1 -1
package/lib/git.sh
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# xgem git workflow helper: cmt / init / rm-remote / rm-branch
|
|
3
|
-
#
|
|
2
|
+
# xgem git workflow helper: cmt / init / branch / rm-remote / rm-branch /
|
|
3
|
+
# pr / sync / clean-branches / hooks.
|
|
4
|
+
# Depends on lib/logger.sh, lib/utils.sh.
|
|
5
|
+
|
|
6
|
+
# _git_default_remote -> "origin" if configured, else the first remote,
|
|
7
|
+
# else empty.
|
|
8
|
+
_git_default_remote() {
|
|
9
|
+
if git remote | grep -q "^origin$"; then
|
|
10
|
+
echo origin
|
|
11
|
+
else
|
|
12
|
+
git remote | head -n 1
|
|
13
|
+
fi
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# _git_base_branch <remote> -> the repo's default branch. Prefers gh's own
|
|
17
|
+
# knowledge of it (accurate, no guessing); falls back to probing common
|
|
18
|
+
# names against the remote's tracking refs when gh is unavailable/offline.
|
|
19
|
+
_git_base_branch() {
|
|
20
|
+
local remote=$1
|
|
21
|
+
local base
|
|
22
|
+
if has_cmd gh; then
|
|
23
|
+
base=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name 2>/dev/null)
|
|
24
|
+
[ -n "$base" ] && { echo "$base"; return 0; }
|
|
25
|
+
fi
|
|
26
|
+
local candidate
|
|
27
|
+
for candidate in main master develop dev; do
|
|
28
|
+
if git show-ref --verify --quiet "refs/remotes/$remote/$candidate"; then
|
|
29
|
+
echo "$candidate"
|
|
30
|
+
return 0
|
|
31
|
+
fi
|
|
32
|
+
done
|
|
33
|
+
return 1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_git_resolve_conflicts_help() {
|
|
37
|
+
local files
|
|
38
|
+
files=$(git diff --name-only --diff-filter=U 2>/dev/null)
|
|
39
|
+
if [ -n "$files" ]; then
|
|
40
|
+
log_warn "Files with conflicts:"
|
|
41
|
+
echo "$files" | sed 's/^/ - /'
|
|
42
|
+
fi
|
|
43
|
+
log_warn "Resolve them, then run: git add <files> && git rebase --continue (or give up with: git rebase --abort)"
|
|
44
|
+
|
|
45
|
+
local open_editor
|
|
46
|
+
read -r -p "Do you want to open VS Code to resolve this now? (y/n): " open_editor
|
|
47
|
+
[[ "$open_editor" == "y" || "$open_editor" == "Y" ]] || return 0
|
|
48
|
+
|
|
49
|
+
if has_cmd code; then
|
|
50
|
+
code .
|
|
51
|
+
return 0
|
|
52
|
+
fi
|
|
53
|
+
if [ "$(detect_os)" = "darwin" ]; then
|
|
54
|
+
local app
|
|
55
|
+
for app in "Visual Studio Code" "Visual Studio Code - Insiders"; do
|
|
56
|
+
if [ -d "/Applications/$app.app" ]; then
|
|
57
|
+
open -a "$app" .
|
|
58
|
+
return 0
|
|
59
|
+
fi
|
|
60
|
+
done
|
|
61
|
+
fi
|
|
62
|
+
log_warn "VS Code's 'code' command isn't on your PATH (a shell alias doesn't count — scripts can't see it). In VS Code run Cmd+Shift+P -> 'Shell Command: Install code command in PATH', or open the files above manually."
|
|
63
|
+
}
|
|
4
64
|
|
|
5
65
|
cmd_git_cmt() {
|
|
6
66
|
local commit_msg=$1
|
|
@@ -35,11 +95,7 @@ cmd_git_cmt() {
|
|
|
35
95
|
|
|
36
96
|
local current_branch remote_name
|
|
37
97
|
current_branch=$(git branch --show-current 2>/dev/null)
|
|
38
|
-
|
|
39
|
-
remote_name="origin"
|
|
40
|
-
else
|
|
41
|
-
remote_name=$(git remote | head -n 1)
|
|
42
|
-
fi
|
|
98
|
+
remote_name=$(_git_default_remote)
|
|
43
99
|
|
|
44
100
|
if [ -z "$remote_name" ]; then
|
|
45
101
|
log_warn "No remote configured — sync was skipped."
|
|
@@ -88,9 +144,7 @@ cmd_git_cmt() {
|
|
|
88
144
|
log_warn "Your commit is safe locally. Re-run 'xgem git cmt' once connectivity is restored, or push manually: git push $remote_name $current_branch"
|
|
89
145
|
exit 1
|
|
90
146
|
fi
|
|
91
|
-
|
|
92
|
-
read -r -p "Do you want to open VS Code to resolve this now? (y/n): " open_editor
|
|
93
|
-
[[ "$open_editor" == "y" || "$open_editor" == "Y" ]] && code .
|
|
147
|
+
_git_resolve_conflicts_help
|
|
94
148
|
exit 1
|
|
95
149
|
fi
|
|
96
150
|
|
|
@@ -277,14 +331,210 @@ cmd_git_branch() {
|
|
|
277
331
|
log_success "Switched to '$selected' and set as default for this repo."
|
|
278
332
|
}
|
|
279
333
|
|
|
280
|
-
|
|
334
|
+
cmd_git_pr() {
|
|
335
|
+
local current_branch remote base
|
|
336
|
+
current_branch=$(git branch --show-current 2>/dev/null)
|
|
337
|
+
[ -n "$current_branch" ] || die "Not on a branch (detached HEAD?) — nothing to open a PR from."
|
|
338
|
+
remote=$(_git_default_remote)
|
|
339
|
+
[ -n "$remote" ] || die "No remote configured."
|
|
340
|
+
|
|
341
|
+
if ! has_cmd gh || ! gh auth status >/dev/null 2>&1; then
|
|
342
|
+
has_cmd gh && log_warn "GitHub CLI (gh) is installed but not authenticated — run 'gh auth login' to enable 'xgem git pr'." \
|
|
343
|
+
|| log_warn "GitHub CLI (gh) is not installed — install it for 'xgem git pr' to open PRs directly: https://cli.github.com"
|
|
344
|
+
local url owner_repo remote_url
|
|
345
|
+
remote_url=$(git remote get-url "$remote" 2>/dev/null)
|
|
346
|
+
owner_repo=$(echo "$remote_url" | sed -E 's#^git@[^:]+:##; s#^https?://[^/]+/##; s#\.git$##')
|
|
347
|
+
[ -n "$owner_repo" ] || die "Could not determine owner/repo from remote '$remote' ($remote_url)."
|
|
348
|
+
base=$(_git_base_branch "$remote")
|
|
349
|
+
url="https://github.com/$owner_repo/compare/${base:-main}...$current_branch?expand=1"
|
|
350
|
+
log_info "Open this URL to create the PR manually: $url"
|
|
351
|
+
confirm "Open it in your browser now?" && _open_url "$url"
|
|
352
|
+
return 0
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
base=$(_git_base_branch "$remote") || die "Could not determine the repo's base branch."
|
|
356
|
+
[ "$current_branch" != "$base" ] || die "You're on '$base' — switch to a feature branch first."
|
|
357
|
+
|
|
358
|
+
log_info "Pushing '$current_branch' to '$remote'..."
|
|
359
|
+
git push -u "$remote" "$current_branch" || die "Push failed."
|
|
360
|
+
|
|
361
|
+
if gh pr view --json number >/dev/null 2>&1; then
|
|
362
|
+
log_success "A PR for '$current_branch' already exists."
|
|
363
|
+
confirm "Open it in your browser?" && gh pr view --web
|
|
364
|
+
return 0
|
|
365
|
+
fi
|
|
366
|
+
|
|
367
|
+
local commits commit_count title body
|
|
368
|
+
commits=$(git log --format='%s' "$remote/$base..HEAD" 2>/dev/null)
|
|
369
|
+
commit_count=$(echo "$commits" | grep -c .)
|
|
370
|
+
|
|
371
|
+
if [ "$commit_count" -le 1 ]; then
|
|
372
|
+
title=$(echo "$commits" | head -1)
|
|
373
|
+
body=""
|
|
374
|
+
else
|
|
375
|
+
# sed's \U (uppercase-first) is GNU-only — BSD sed on macOS passes
|
|
376
|
+
# it through literally instead of applying it. Capitalize via tr
|
|
377
|
+
# on just the first character instead, which is portable.
|
|
378
|
+
title=$(echo "$current_branch" | sed -E 's/[-_]/ /g')
|
|
379
|
+
title="$(echo "${title:0:1}" | tr '[:lower:]' '[:upper:]')${title:1}"
|
|
380
|
+
body=$(echo "$commits" | sed 's/^/- /')
|
|
381
|
+
fi
|
|
382
|
+
|
|
383
|
+
local title_override
|
|
384
|
+
read -r -p "PR title [$title]: " title_override
|
|
385
|
+
[ -n "$title_override" ] && title="$title_override"
|
|
386
|
+
[ -n "$title" ] || die "A PR title is required."
|
|
387
|
+
|
|
388
|
+
if gh pr create --title "$title" --body "$body" --base "$base"; then
|
|
389
|
+
log_success "PR created."
|
|
390
|
+
confirm "Open it in your browser?" && gh pr view --web
|
|
391
|
+
else
|
|
392
|
+
die "gh pr create failed."
|
|
393
|
+
fi
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
cmd_git_sync() {
|
|
397
|
+
local remote base current_branch
|
|
398
|
+
remote=$(_git_default_remote)
|
|
399
|
+
[ -n "$remote" ] || die "No remote configured."
|
|
400
|
+
base=$(_git_base_branch "$remote") || die "Could not determine the repo's base branch."
|
|
401
|
+
current_branch=$(git branch --show-current 2>/dev/null)
|
|
402
|
+
|
|
403
|
+
log_info "Fetching '$base' from '$remote'..."
|
|
404
|
+
git fetch "$remote" "$base" || die "Fetch failed."
|
|
405
|
+
|
|
406
|
+
log_info "Rebasing '$current_branch' onto '$remote/$base'..."
|
|
407
|
+
if git rebase "$remote/$base"; then
|
|
408
|
+
log_success "'$current_branch' is now up to date with '$remote/$base'."
|
|
409
|
+
return 0
|
|
410
|
+
fi
|
|
411
|
+
|
|
412
|
+
local git_dir
|
|
413
|
+
git_dir=$(git rev-parse --git-dir 2>/dev/null)
|
|
414
|
+
if [ -d "$git_dir/rebase-merge" ] || [ -d "$git_dir/rebase-apply" ]; then
|
|
415
|
+
log_error "MERGE CONFLICT DETECTED!"
|
|
416
|
+
_git_resolve_conflicts_help
|
|
417
|
+
else
|
|
418
|
+
log_error "Rebase failed — this looks like a connection problem, not a merge conflict (see the git error above)."
|
|
419
|
+
fi
|
|
420
|
+
exit 1
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
cmd_git_clean_branches() {
|
|
424
|
+
local remote base current_branch
|
|
425
|
+
remote=$(_git_default_remote)
|
|
426
|
+
[ -n "$remote" ] || die "No remote configured."
|
|
427
|
+
|
|
428
|
+
log_info "Pruning stale remote-tracking refs on '$remote'..."
|
|
429
|
+
git fetch "$remote" --prune || die "Fetch failed."
|
|
430
|
+
|
|
431
|
+
base=$(_git_base_branch "$remote") || die "Could not determine the repo's base branch."
|
|
432
|
+
current_branch=$(git branch --show-current 2>/dev/null)
|
|
433
|
+
|
|
434
|
+
local -a candidates=()
|
|
435
|
+
while IFS= read -r b; do
|
|
436
|
+
[ -n "$b" ] || continue
|
|
437
|
+
case "$b" in
|
|
438
|
+
"$current_branch"|main|master|develop|dev) continue ;;
|
|
439
|
+
esac
|
|
440
|
+
candidates+=("$b")
|
|
441
|
+
done < <(git branch --format='%(refname:short)' --merged "$remote/$base" 2>/dev/null)
|
|
442
|
+
|
|
443
|
+
if [ ${#candidates[@]} -eq 0 ]; then
|
|
444
|
+
log_success "No merged local branches to clean up."
|
|
445
|
+
return 0
|
|
446
|
+
fi
|
|
447
|
+
|
|
448
|
+
log_info "Local branches already merged into '$base':"
|
|
449
|
+
printf ' - %s\n' "${candidates[@]}"
|
|
450
|
+
|
|
451
|
+
confirm "Delete all ${#candidates[@]} of these local branches?" || { log_info "Cancelled."; return 0; }
|
|
452
|
+
|
|
453
|
+
local b
|
|
454
|
+
for b in "${candidates[@]}"; do
|
|
455
|
+
if git branch -d "$b" >/dev/null 2>&1; then
|
|
456
|
+
log_success "Deleted '$b'."
|
|
457
|
+
else
|
|
458
|
+
log_warn "Could not delete '$b' (not fully merged?) — left as-is."
|
|
459
|
+
fi
|
|
460
|
+
done
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
_GIT_HOOK_MARKER="# xgem-managed-hook"
|
|
464
|
+
|
|
465
|
+
cmd_git_hooks_install() {
|
|
466
|
+
local git_dir hook_path
|
|
467
|
+
git_dir=$(git rev-parse --git-dir 2>/dev/null) || die "Not a git repository."
|
|
468
|
+
hook_path="$git_dir/hooks/pre-commit"
|
|
469
|
+
|
|
470
|
+
if [ -f "$hook_path" ] && ! grep -qF "$_GIT_HOOK_MARKER" "$hook_path"; then
|
|
471
|
+
log_warn "An existing pre-commit hook was found that xgem didn't create."
|
|
472
|
+
confirm "Overwrite it?" || { log_info "Cancelled."; return 0; }
|
|
473
|
+
fi
|
|
474
|
+
|
|
475
|
+
local run_tests xgem_path
|
|
476
|
+
read -r -p "Also run tests before commit (slower)? (y/N): " run_tests
|
|
477
|
+
xgem_path=$(command -v xgem)
|
|
478
|
+
[ -n "$xgem_path" ] || die "Could not resolve xgem's own path via 'command -v xgem'."
|
|
479
|
+
|
|
480
|
+
local checks_added=0
|
|
481
|
+
{
|
|
482
|
+
echo "#!/bin/bash"
|
|
483
|
+
echo "$_GIT_HOOK_MARKER"
|
|
484
|
+
local fw
|
|
485
|
+
for fw in "$CONFIG_DIR"/*/; do
|
|
486
|
+
[ -d "$fw" ] || continue
|
|
487
|
+
fw=$(basename "$fw")
|
|
488
|
+
if [ -f "$CONFIG_DIR/$fw/lint.sh" ]; then
|
|
489
|
+
echo "\"$xgem_path\" run $fw lint || exit 1"
|
|
490
|
+
checks_added=$((checks_added + 1))
|
|
491
|
+
fi
|
|
492
|
+
if [[ "$run_tests" == "y" || "$run_tests" == "Y" ]] && [ -f "$CONFIG_DIR/$fw/test.sh" ]; then
|
|
493
|
+
echo "\"$xgem_path\" run $fw test || exit 1"
|
|
494
|
+
checks_added=$((checks_added + 1))
|
|
495
|
+
fi
|
|
496
|
+
done
|
|
497
|
+
} > "$hook_path"
|
|
498
|
+
chmod +x "$hook_path"
|
|
499
|
+
|
|
500
|
+
if [ "$checks_added" -eq 0 ]; then
|
|
501
|
+
log_warn "No lint/test scripts found under $CONFIG_DIR — installed a hook that doesn't check anything yet."
|
|
502
|
+
fi
|
|
503
|
+
log_success "Installed pre-commit hook at $hook_path."
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
cmd_git_hooks_uninstall() {
|
|
507
|
+
local git_dir hook_path
|
|
508
|
+
git_dir=$(git rev-parse --git-dir 2>/dev/null) || die "Not a git repository."
|
|
509
|
+
hook_path="$git_dir/hooks/pre-commit"
|
|
510
|
+
|
|
511
|
+
[ -f "$hook_path" ] || { log_info "No pre-commit hook installed."; return 0; }
|
|
512
|
+
grep -qF "$_GIT_HOOK_MARKER" "$hook_path" || die "$hook_path wasn't created by xgem — not removing it."
|
|
513
|
+
|
|
514
|
+
rm -f "$hook_path"
|
|
515
|
+
log_success "Removed pre-commit hook."
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
cmd_git_hooks() {
|
|
519
|
+
case "$1" in
|
|
520
|
+
install) cmd_git_hooks_install ;;
|
|
521
|
+
uninstall) cmd_git_hooks_uninstall ;;
|
|
522
|
+
*) die "Usage: xgem git hooks <install|uninstall>" ;;
|
|
523
|
+
esac
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
# xgem git <cmt|init|rm-remote|rm-branch|branch|pr|sync|clean-branches|hooks> ...
|
|
281
527
|
cmd_git() {
|
|
282
528
|
case "$1" in
|
|
283
|
-
cmt)
|
|
284
|
-
init)
|
|
285
|
-
rm-remote)
|
|
286
|
-
rm-branch)
|
|
287
|
-
branch)
|
|
288
|
-
|
|
529
|
+
cmt) cmd_git_cmt "$2" ;;
|
|
530
|
+
init) cmd_git_init ;;
|
|
531
|
+
rm-remote) cmd_git_rm_remote ;;
|
|
532
|
+
rm-branch) cmd_git_rm_branch ;;
|
|
533
|
+
branch) cmd_git_branch ;;
|
|
534
|
+
pr) cmd_git_pr ;;
|
|
535
|
+
sync) cmd_git_sync ;;
|
|
536
|
+
clean-branches) cmd_git_clean_branches ;;
|
|
537
|
+
hooks) cmd_git_hooks "$2" ;;
|
|
538
|
+
*) die "Unknown git subcommand '$1'. Usage: xgem git <cmt|init|rm-remote|rm-branch|branch|pr|sync|clean-branches|hooks>" ;;
|
|
289
539
|
esac
|
|
290
540
|
}
|
package/lib/registry.sh
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# xgem's global (cross-project) registry: which directories have been
|
|
3
|
+
# scaffolded with `xgem init`/`xgem add`, so `xgem status` can find them
|
|
4
|
+
# without the user having to remember or list them by hand.
|
|
5
|
+
# Depends on lib/logger.sh.
|
|
6
|
+
|
|
7
|
+
REGISTRY_FILE="${XGEM_REGISTRY_FILE:-$HOME/.xgem/projects}"
|
|
8
|
+
|
|
9
|
+
registry_add() {
|
|
10
|
+
local path=$1
|
|
11
|
+
mkdir -p "$(dirname "$REGISTRY_FILE")"
|
|
12
|
+
touch "$REGISTRY_FILE"
|
|
13
|
+
grep -qxF "$path" "$REGISTRY_FILE" || echo "$path" >> "$REGISTRY_FILE"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
registry_remove() {
|
|
17
|
+
local path=$1
|
|
18
|
+
[ -f "$REGISTRY_FILE" ] || return 0
|
|
19
|
+
local tmp
|
|
20
|
+
tmp=$(mktemp)
|
|
21
|
+
grep -vxF "$path" "$REGISTRY_FILE" > "$tmp" && mv "$tmp" "$REGISTRY_FILE"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# registry_list — prints one project path per line, pruning (and rewriting
|
|
25
|
+
# the registry for) any entry that no longer exists or is no longer
|
|
26
|
+
# xgem-tracked.
|
|
27
|
+
registry_list() {
|
|
28
|
+
[ -f "$REGISTRY_FILE" ] || return 0
|
|
29
|
+
local -a live=()
|
|
30
|
+
local path
|
|
31
|
+
while IFS= read -r path; do
|
|
32
|
+
[ -n "$path" ] || continue
|
|
33
|
+
if [ -d "$path/$CONFIG_DIR" ]; then
|
|
34
|
+
live+=("$path")
|
|
35
|
+
fi
|
|
36
|
+
done < "$REGISTRY_FILE"
|
|
37
|
+
|
|
38
|
+
printf '%s\n' "${live[@]}"
|
|
39
|
+
|
|
40
|
+
local count
|
|
41
|
+
count=$(wc -l < "$REGISTRY_FILE" | tr -d ' ')
|
|
42
|
+
if [ "$count" != "${#live[@]}" ]; then
|
|
43
|
+
printf '%s\n' "${live[@]}" > "$REGISTRY_FILE"
|
|
44
|
+
fi
|
|
45
|
+
}
|
package/lib/release.sh
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# xgem release: framework-aware version bump + changelog + tag + push.
|
|
3
|
+
# Depends on lib/logger.sh, lib/utils.sh, lib/git.sh (_git_default_remote).
|
|
4
|
+
|
|
5
|
+
_release_detect_version_files() {
|
|
6
|
+
local -a found=()
|
|
7
|
+
[ -f package.json ] && found+=(package.json)
|
|
8
|
+
[ -f pubspec.yaml ] && found+=(pubspec.yaml)
|
|
9
|
+
[ -f Cargo.toml ] && found+=(Cargo.toml)
|
|
10
|
+
[ -f pyproject.toml ] && found+=(pyproject.toml)
|
|
11
|
+
printf '%s\n' "${found[@]}"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# _release_read_version <file> -> current version string (empty on failure)
|
|
15
|
+
_release_read_version() {
|
|
16
|
+
case "$1" in
|
|
17
|
+
package.json)
|
|
18
|
+
grep -m1 '"version"' package.json | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/'
|
|
19
|
+
;;
|
|
20
|
+
pubspec.yaml)
|
|
21
|
+
grep '^version:' pubspec.yaml | head -1 | awk '{print $2}'
|
|
22
|
+
;;
|
|
23
|
+
Cargo.toml|pyproject.toml)
|
|
24
|
+
grep -m1 -E '^version[[:space:]]*=' "$1" | sed -E 's/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/'
|
|
25
|
+
;;
|
|
26
|
+
esac
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# _release_write_version <file> <new_version>
|
|
30
|
+
# Uses awk (not sed's GNU-only "0,/re/" first-match addressing, which BSD
|
|
31
|
+
# sed on macOS doesn't support) to replace only the first matching line.
|
|
32
|
+
_release_write_version() {
|
|
33
|
+
local file=$1 new=$2 tmp
|
|
34
|
+
tmp=$(mktemp)
|
|
35
|
+
case "$file" in
|
|
36
|
+
package.json)
|
|
37
|
+
awk -v new="$new" '
|
|
38
|
+
!done && /"version"[[:space:]]*:[[:space:]]*"[^"]*"/ {
|
|
39
|
+
sub(/"version"[[:space:]]*:[[:space:]]*"[^"]*"/, "\"version\": \"" new "\"")
|
|
40
|
+
done = 1
|
|
41
|
+
}
|
|
42
|
+
{ print }
|
|
43
|
+
' "$file" > "$tmp" && mv "$tmp" "$file"
|
|
44
|
+
;;
|
|
45
|
+
pubspec.yaml)
|
|
46
|
+
sed -E "s/^version:.*/version: $new/" "$file" > "$tmp" && mv "$tmp" "$file"
|
|
47
|
+
;;
|
|
48
|
+
Cargo.toml|pyproject.toml)
|
|
49
|
+
awk -v new="$new" '
|
|
50
|
+
!done && /^version[[:space:]]*=[[:space:]]*"[^"]*"/ {
|
|
51
|
+
sub(/^version[[:space:]]*=[[:space:]]*"[^"]*"/, "version = \"" new "\"")
|
|
52
|
+
done = 1
|
|
53
|
+
}
|
|
54
|
+
{ print }
|
|
55
|
+
' "$file" > "$tmp" && mv "$tmp" "$file"
|
|
56
|
+
;;
|
|
57
|
+
esac
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# _release_bump <version> <major|minor|patch> -> new version, or empty if
|
|
61
|
+
# <version> isn't a plain X.Y.Z.
|
|
62
|
+
_release_bump() {
|
|
63
|
+
local version=$1 kind=$2 suffix="" core
|
|
64
|
+
core=${version%%+*}
|
|
65
|
+
[ "$core" != "$version" ] && suffix="+${version#*+}"
|
|
66
|
+
|
|
67
|
+
[[ "$core" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]] || return 1
|
|
68
|
+
local major=${BASH_REMATCH[1]} minor=${BASH_REMATCH[2]} patch=${BASH_REMATCH[3]}
|
|
69
|
+
case "$kind" in
|
|
70
|
+
major) major=$((major + 1)); minor=0; patch=0 ;;
|
|
71
|
+
minor) minor=$((minor + 1)); patch=0 ;;
|
|
72
|
+
patch) patch=$((patch + 1)) ;;
|
|
73
|
+
esac
|
|
74
|
+
echo "${major}.${minor}.${patch}${suffix}"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_release_write_changelog() {
|
|
78
|
+
local version=$1 last_tag=$2
|
|
79
|
+
local -a features=() fixes=() other=()
|
|
80
|
+
local subject
|
|
81
|
+
|
|
82
|
+
while IFS= read -r subject; do
|
|
83
|
+
[ -n "$subject" ] || continue
|
|
84
|
+
case "$subject" in
|
|
85
|
+
feat*) features+=("$subject") ;;
|
|
86
|
+
fix*) fixes+=("$subject") ;;
|
|
87
|
+
*) other+=("$subject") ;;
|
|
88
|
+
esac
|
|
89
|
+
done < <(git log --format='%s' "${last_tag:+$last_tag..}HEAD" 2>/dev/null)
|
|
90
|
+
|
|
91
|
+
local tmp
|
|
92
|
+
tmp=$(mktemp)
|
|
93
|
+
{
|
|
94
|
+
echo "## v$version - $(date +%Y-%m-%d)"
|
|
95
|
+
if [ ${#features[@]} -gt 0 ]; then
|
|
96
|
+
echo ""
|
|
97
|
+
echo "### Features"
|
|
98
|
+
printf -- '- %s\n' "${features[@]}"
|
|
99
|
+
fi
|
|
100
|
+
if [ ${#fixes[@]} -gt 0 ]; then
|
|
101
|
+
echo ""
|
|
102
|
+
echo "### Fixes"
|
|
103
|
+
printf -- '- %s\n' "${fixes[@]}"
|
|
104
|
+
fi
|
|
105
|
+
if [ ${#other[@]} -gt 0 ]; then
|
|
106
|
+
echo ""
|
|
107
|
+
echo "### Other"
|
|
108
|
+
printf -- '- %s\n' "${other[@]}"
|
|
109
|
+
fi
|
|
110
|
+
echo ""
|
|
111
|
+
if [ -f CHANGELOG.md ]; then
|
|
112
|
+
tail -n +2 CHANGELOG.md
|
|
113
|
+
fi
|
|
114
|
+
} > "$tmp"
|
|
115
|
+
{ echo "# Changelog"; echo ""; cat "$tmp"; } > CHANGELOG.md
|
|
116
|
+
rm -f "$tmp"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
cmd_release() {
|
|
120
|
+
local -a version_files=()
|
|
121
|
+
while IFS= read -r f; do version_files+=("$f"); done < <(_release_detect_version_files)
|
|
122
|
+
[ ${#version_files[@]} -gt 0 ] || die "No recognized version file (package.json/pubspec.yaml/Cargo.toml/pyproject.toml) found here."
|
|
123
|
+
|
|
124
|
+
local file
|
|
125
|
+
if [ ${#version_files[@]} -eq 1 ]; then
|
|
126
|
+
file="${version_files[0]}"
|
|
127
|
+
else
|
|
128
|
+
echo "Multiple version files found:"
|
|
129
|
+
local i=1
|
|
130
|
+
for f in "${version_files[@]}"; do
|
|
131
|
+
echo "$i) $f"
|
|
132
|
+
i=$((i + 1))
|
|
133
|
+
done
|
|
134
|
+
local choice
|
|
135
|
+
read -r -p "Which is the canonical one to bump? [1-${#version_files[@]}]: " choice
|
|
136
|
+
[[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#version_files[@]} ] \
|
|
137
|
+
|| die "Invalid selection."
|
|
138
|
+
file="${version_files[$((choice - 1))]}"
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
local current new_version bump_kind
|
|
142
|
+
current=$(_release_read_version "$file")
|
|
143
|
+
[ -n "$current" ] || die "Could not read a version from $file."
|
|
144
|
+
log_info "Current version in $file: $current"
|
|
145
|
+
|
|
146
|
+
bump_kind=$1
|
|
147
|
+
case "$bump_kind" in
|
|
148
|
+
major|minor|patch) ;;
|
|
149
|
+
"")
|
|
150
|
+
read -r -p "Bump [major/minor/patch] (default: patch): " bump_kind
|
|
151
|
+
bump_kind=${bump_kind:-patch}
|
|
152
|
+
;;
|
|
153
|
+
*) die "Unknown bump kind '$bump_kind'. Usage: xgem release [major|minor|patch]" ;;
|
|
154
|
+
esac
|
|
155
|
+
|
|
156
|
+
new_version=$(_release_bump "$current" "$bump_kind")
|
|
157
|
+
if [ -z "$new_version" ]; then
|
|
158
|
+
log_warn "'$current' isn't a plain X.Y.Z version — can't bump it automatically."
|
|
159
|
+
read -r -p "Enter the new version directly: " new_version
|
|
160
|
+
[ -n "$new_version" ] || die "A new version is required."
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
log_info "$current -> $new_version"
|
|
164
|
+
confirm "Write this version, update CHANGELOG.md, commit, and tag?" || { log_info "Cancelled."; return 0; }
|
|
165
|
+
|
|
166
|
+
_release_write_version "$file" "$new_version"
|
|
167
|
+
|
|
168
|
+
local last_tag
|
|
169
|
+
last_tag=$(git describe --tags --abbrev=0 2>/dev/null)
|
|
170
|
+
_release_write_changelog "$new_version" "$last_tag"
|
|
171
|
+
|
|
172
|
+
git add "$file" CHANGELOG.md
|
|
173
|
+
git commit -m "chore(release): v$new_version" || die "Commit failed."
|
|
174
|
+
git tag -a "v$new_version" -m "v$new_version" || die "Tag failed."
|
|
175
|
+
log_success "Committed and tagged v$new_version."
|
|
176
|
+
|
|
177
|
+
local remote current_branch
|
|
178
|
+
remote=$(_git_default_remote)
|
|
179
|
+
if [ -n "$remote" ] && confirm "Push commit and tag to '$remote'?"; then
|
|
180
|
+
current_branch=$(git branch --show-current 2>/dev/null)
|
|
181
|
+
git push "$remote" "$current_branch" && git push "$remote" "v$new_version" \
|
|
182
|
+
&& log_success "Pushed v$new_version to '$remote'." \
|
|
183
|
+
|| die "Push failed — the commit and tag are safe locally."
|
|
184
|
+
fi
|
|
185
|
+
}
|
package/lib/status.sh
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# xgem status: a one-screen dashboard across every project xgem has ever
|
|
3
|
+
# scaffolded, so a developer doesn't have to `cd` into each one just to see
|
|
4
|
+
# what's dirty. Depends on lib/logger.sh, lib/registry.sh.
|
|
5
|
+
|
|
6
|
+
# _status_row <path> — prints one tab-separated row: path, branch, dirty
|
|
7
|
+
# count, ahead/behind, frameworks. Run in a subshell so `cd` never leaks.
|
|
8
|
+
_status_row() {
|
|
9
|
+
local path=$1
|
|
10
|
+
(
|
|
11
|
+
cd "$path" || exit 0
|
|
12
|
+
local branch dirty ahead_behind frameworks
|
|
13
|
+
branch=$(git branch --show-current 2>/dev/null)
|
|
14
|
+
[ -n "$branch" ] || branch="-"
|
|
15
|
+
dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
16
|
+
ahead_behind=$(git rev-list --left-right --count '@{u}...HEAD' 2>/dev/null | awk '{print "-"$1" +"$2}')
|
|
17
|
+
[ -n "$ahead_behind" ] || ahead_behind="-"
|
|
18
|
+
frameworks=$(ls "$CONFIG_DIR" 2>/dev/null | tr '\n' ',' | sed 's/,$//')
|
|
19
|
+
[ -n "$frameworks" ] || frameworks="-"
|
|
20
|
+
printf '%s\t%s\t%s\t%s\t%s\n' "$path" "$branch" "$dirty" "$ahead_behind" "$frameworks"
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
cmd_status() {
|
|
25
|
+
local -a projects=()
|
|
26
|
+
while IFS= read -r p; do
|
|
27
|
+
[ -n "$p" ] && projects+=("$p")
|
|
28
|
+
done < <(registry_list)
|
|
29
|
+
|
|
30
|
+
if [ ${#projects[@]} -eq 0 ]; then
|
|
31
|
+
log_info "No xgem-tracked projects found yet — run 'xgem init' in a project to start tracking it."
|
|
32
|
+
return 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
printf '%-45s %-20s %-6s %-10s %s\n' "PROJECT" "BRANCH" "DIRTY" "AHEAD/BEHIND" "FRAMEWORKS"
|
|
36
|
+
local p row
|
|
37
|
+
for p in "${projects[@]}"; do
|
|
38
|
+
row=$(_status_row "$p")
|
|
39
|
+
IFS=$'\t' read -r path branch dirty ahead_behind frameworks <<< "$row"
|
|
40
|
+
printf '%-45s %-20s %-6s %-10s %s\n' "$path" "$branch" "$dirty" "$ahead_behind" "$frameworks"
|
|
41
|
+
done
|
|
42
|
+
}
|
package/lib/utils.sh
CHANGED
|
@@ -42,6 +42,17 @@ require_cmd() {
|
|
|
42
42
|
fi
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
# _open_url <url> — best-effort browser open, silent no-op on an OS xgem
|
|
46
|
+
# doesn't know how to open a browser on.
|
|
47
|
+
_open_url() {
|
|
48
|
+
local url=$1
|
|
49
|
+
case "$(detect_os)" in
|
|
50
|
+
darwin) open "$url" 2>/dev/null ;;
|
|
51
|
+
linux) xdg-open "$url" 2>/dev/null ;;
|
|
52
|
+
*) log_debug "Don't know how to open a browser on this OS — visit $url manually." ;;
|
|
53
|
+
esac
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
# confirm "prompt text" -> 0 if approved, 1 otherwise
|
|
46
57
|
# Honors XGEM_YES=1 (from --yes) to auto-approve, and always returns 1
|
|
47
58
|
# (declines) under XGEM_DRY_RUN so callers never apply changes in dry-run mode.
|