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/lib/git.sh CHANGED
@@ -1,6 +1,66 @@
1
1
  #!/bin/bash
2
- # xgem git workflow helper: cmt / init / rm-remote / rm-branch.
3
- # Depends on lib/logger.sh.
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
- if git remote | grep -q "^origin$"; then
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
- local open_editor
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
- # xgem git <cmt|init|rm-remote|rm-branch|branch> ...
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) cmd_git_cmt "$2" ;;
284
- init) cmd_git_init ;;
285
- rm-remote) cmd_git_rm_remote ;;
286
- rm-branch) cmd_git_rm_branch ;;
287
- branch) cmd_git_branch ;;
288
- *) die "Unknown git subcommand '$1'. Usage: xgem git <cmt|init|rm-remote|rm-branch|branch>" ;;
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
  }
@@ -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.
package/lib/version.sh CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # xgem version info.
3
3
 
4
- XGEM_VERSION="2.0.0-alpha.12"
4
+ XGEM_VERSION="2.0.0-alpha.15"
5
5
 
6
6
  print_version() {
7
7
  echo "xgem $XGEM_VERSION"