@revotools/cli 0.3.0 → 0.5.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/dist/revo +574 -48
- package/package.json +1 -1
package/dist/revo
CHANGED
|
@@ -8,7 +8,7 @@ set -euo pipefail
|
|
|
8
8
|
# Exit cleanly on SIGPIPE (e.g., revo clone | grep, revo status | head)
|
|
9
9
|
trap 'exit 0' PIPE
|
|
10
10
|
|
|
11
|
-
REVO_VERSION="0.
|
|
11
|
+
REVO_VERSION="0.5.0"
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
# === lib/ui.sh ===
|
|
@@ -794,8 +794,52 @@ config_init() {
|
|
|
794
794
|
# Write config
|
|
795
795
|
yaml_write "$REVO_CONFIG_FILE"
|
|
796
796
|
|
|
797
|
-
#
|
|
798
|
-
|
|
797
|
+
# Merge revo's required entries into .gitignore without clobbering
|
|
798
|
+
# any existing user entries.
|
|
799
|
+
config_ensure_gitignore "$dir/.gitignore"
|
|
800
|
+
|
|
801
|
+
return 0
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
# Ensure the given .gitignore contains the entries revo needs
|
|
805
|
+
# (`repos/` and `.revo/`). Existing content is preserved; only missing
|
|
806
|
+
# entries are appended. Creates the file if it doesn't exist.
|
|
807
|
+
# Usage: config_ensure_gitignore "/path/to/.gitignore"
|
|
808
|
+
config_ensure_gitignore() {
|
|
809
|
+
local gitignore="$1"
|
|
810
|
+
local needed
|
|
811
|
+
needed=$(printf 'repos/\n.revo/\n')
|
|
812
|
+
|
|
813
|
+
if [[ ! -f "$gitignore" ]]; then
|
|
814
|
+
printf '%s\n' "$needed" > "$gitignore"
|
|
815
|
+
return 0
|
|
816
|
+
fi
|
|
817
|
+
|
|
818
|
+
# Append only the entries that aren't already present (exact line match,
|
|
819
|
+
# ignoring leading/trailing whitespace and comments).
|
|
820
|
+
local entry has_entry needs_newline=0
|
|
821
|
+
# If the file is non-empty and doesn't end in a newline, we need to add
|
|
822
|
+
# one before appending so we don't merge into an existing line.
|
|
823
|
+
if [[ -s "$gitignore" ]] && [[ -n "$(tail -c1 "$gitignore" 2>/dev/null)" ]]; then
|
|
824
|
+
needs_newline=1
|
|
825
|
+
fi
|
|
826
|
+
|
|
827
|
+
local appended=0
|
|
828
|
+
while IFS= read -r entry; do
|
|
829
|
+
[[ -z "$entry" ]] && continue
|
|
830
|
+
has_entry=$(awk -v e="$entry" '
|
|
831
|
+
{ sub(/^[[:space:]]+/, ""); sub(/[[:space:]]+$/, "") }
|
|
832
|
+
$0 == e { found = 1; exit }
|
|
833
|
+
END { exit !found }
|
|
834
|
+
' "$gitignore" && printf 'yes' || printf 'no')
|
|
835
|
+
if [[ "$has_entry" == "no" ]]; then
|
|
836
|
+
if [[ $appended -eq 0 ]] && [[ $needs_newline -eq 1 ]]; then
|
|
837
|
+
printf '\n' >> "$gitignore"
|
|
838
|
+
fi
|
|
839
|
+
printf '%s\n' "$entry" >> "$gitignore"
|
|
840
|
+
appended=1
|
|
841
|
+
fi
|
|
842
|
+
done <<< "$needed"
|
|
799
843
|
|
|
800
844
|
return 0
|
|
801
845
|
}
|
|
@@ -1515,45 +1559,13 @@ _init_scan_existing() {
|
|
|
1515
1559
|
fi
|
|
1516
1560
|
}
|
|
1517
1561
|
|
|
1518
|
-
# Write a Claude-first onboarding CLAUDE.md
|
|
1519
|
-
#
|
|
1562
|
+
# Write a Claude-first onboarding CLAUDE.md if and only if the workspace
|
|
1563
|
+
# root has no CLAUDE.md yet. Existing files are left alone — `revo context`
|
|
1564
|
+
# will append its marker-wrapped auto block once repos are added.
|
|
1520
1565
|
_init_write_claude_md() {
|
|
1521
1566
|
local out="$REVO_WORKSPACE_ROOT/CLAUDE.md"
|
|
1522
1567
|
|
|
1523
|
-
if [[ -f "$out" ]]
|
|
1524
|
-
if grep -q "Workspace Tool: revo" "$out" 2>/dev/null; then
|
|
1525
|
-
return 0
|
|
1526
|
-
fi
|
|
1527
|
-
cat >> "$out" << 'EOF'
|
|
1528
|
-
|
|
1529
|
-
---
|
|
1530
|
-
|
|
1531
|
-
## Workspace Tool: revo
|
|
1532
|
-
|
|
1533
|
-
This workspace uses revo to manage multiple repos.
|
|
1534
|
-
Source: https://github.com/jippylong12/revo
|
|
1535
|
-
|
|
1536
|
-
### Setup commands
|
|
1537
|
-
- `revo add <git-url> --tags <tags> [--depends-on <repo>]` — add a repo to workspace
|
|
1538
|
-
- `revo clone` — clone all configured repos
|
|
1539
|
-
- `revo context` — scan repos and regenerate the workspace context section
|
|
1540
|
-
|
|
1541
|
-
### Daily commands
|
|
1542
|
-
- `revo status` — branch and dirty state across all repos
|
|
1543
|
-
- `revo sync` — pull latest across all repos
|
|
1544
|
-
- `revo feature <name>` — create feature branch across all repos
|
|
1545
|
-
- `revo commit "msg"` — commit all dirty repos with same message
|
|
1546
|
-
- `revo push` — push all repos
|
|
1547
|
-
- `revo pr "title"` — create coordinated PRs via gh CLI
|
|
1548
|
-
- `revo exec "cmd" --tag <tag>` — run command in filtered repos
|
|
1549
|
-
|
|
1550
|
-
### Working in this workspace
|
|
1551
|
-
- Repos live in the repos/ subdirectory (or wherever configured in revo.yaml)
|
|
1552
|
-
- Edit files across repos directly
|
|
1553
|
-
- Check .revo/features/ for active feature briefs
|
|
1554
|
-
- Follow dependency order when making cross-repo changes
|
|
1555
|
-
EOF
|
|
1556
|
-
ui_step_done "Appended revo section to existing CLAUDE.md"
|
|
1568
|
+
if [[ -f "$out" ]]; then
|
|
1557
1569
|
return 0
|
|
1558
1570
|
fi
|
|
1559
1571
|
|
|
@@ -1693,16 +1705,18 @@ cmd_init() {
|
|
|
1693
1705
|
fi
|
|
1694
1706
|
fi
|
|
1695
1707
|
|
|
1696
|
-
#
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
#
|
|
1708
|
+
# If we detected repos, hand off to cmd_context — it now wraps its output
|
|
1709
|
+
# in BEGIN/END markers and preserves any user content in CLAUDE.md, so we
|
|
1710
|
+
# don't need the onboarding placeholder. When no repos were detected,
|
|
1711
|
+
# write the placeholder so Claude has something to read.
|
|
1700
1712
|
if [[ $detected_count -gt 0 ]]; then
|
|
1701
1713
|
ui_bar_line
|
|
1702
1714
|
cmd_context
|
|
1703
1715
|
return 0
|
|
1704
1716
|
fi
|
|
1705
1717
|
|
|
1718
|
+
_init_write_claude_md
|
|
1719
|
+
|
|
1706
1720
|
ui_outro "Workspace initialized! Run 'revo add <url>' to add repositories."
|
|
1707
1721
|
return 0
|
|
1708
1722
|
}
|
|
@@ -2752,15 +2766,75 @@ _context_topo_sort() {
|
|
|
2752
2766
|
return 0
|
|
2753
2767
|
}
|
|
2754
2768
|
|
|
2755
|
-
#
|
|
2769
|
+
# Markers used to delimit revo's auto-generated block inside the workspace
|
|
2770
|
+
# CLAUDE.md. Anything between these markers is owned by `revo context` and
|
|
2771
|
+
# replaced on every regeneration. Anything outside is preserved untouched.
|
|
2772
|
+
CONTEXT_BEGIN_MARKER='<!-- BEGIN revo:auto - regenerated by `revo context`, do not edit between markers -->'
|
|
2773
|
+
CONTEXT_END_MARKER='<!-- END revo:auto -->'
|
|
2774
|
+
|
|
2775
|
+
# Splice an auto-generated block into the target CLAUDE.md without
|
|
2776
|
+
# clobbering user content.
|
|
2777
|
+
# - If target doesn't exist: copy auto file to target.
|
|
2778
|
+
# - If target has BEGIN+END markers: replace the marker block in place.
|
|
2779
|
+
# - Otherwise: append the auto block (with a separator) at the end.
|
|
2780
|
+
# Usage: _context_merge_into "/tmp/auto" "/path/to/CLAUDE.md"
|
|
2781
|
+
_context_merge_into() {
|
|
2782
|
+
local auto_file="$1"
|
|
2783
|
+
local target="$2"
|
|
2784
|
+
|
|
2785
|
+
if [[ ! -f "$target" ]]; then
|
|
2786
|
+
cp "$auto_file" "$target"
|
|
2787
|
+
return 0
|
|
2788
|
+
fi
|
|
2789
|
+
|
|
2790
|
+
if grep -qF "$CONTEXT_BEGIN_MARKER" "$target" 2>/dev/null \
|
|
2791
|
+
&& grep -qF "$CONTEXT_END_MARKER" "$target" 2>/dev/null; then
|
|
2792
|
+
local merged
|
|
2793
|
+
merged=$(mktemp -t revo-merge.XXXXXX)
|
|
2794
|
+
awk -v auto="$auto_file" -v begin="$CONTEXT_BEGIN_MARKER" -v end="$CONTEXT_END_MARKER" '
|
|
2795
|
+
BEGIN { in_block = 0 }
|
|
2796
|
+
index($0, begin) == 1 {
|
|
2797
|
+
in_block = 1
|
|
2798
|
+
while ((getline line < auto) > 0) print line
|
|
2799
|
+
close(auto)
|
|
2800
|
+
next
|
|
2801
|
+
}
|
|
2802
|
+
index($0, end) == 1 && in_block {
|
|
2803
|
+
in_block = 0
|
|
2804
|
+
next
|
|
2805
|
+
}
|
|
2806
|
+
!in_block { print }
|
|
2807
|
+
' "$target" > "$merged"
|
|
2808
|
+
mv "$merged" "$target"
|
|
2809
|
+
return 0
|
|
2810
|
+
fi
|
|
2811
|
+
|
|
2812
|
+
# No markers — append at the end with a blank-line separator. If the
|
|
2813
|
+
# existing file doesn't end in a newline, add one first so we don't merge
|
|
2814
|
+
# into the user's last line.
|
|
2815
|
+
if [[ -s "$target" ]] && [[ -n "$(tail -c1 "$target" 2>/dev/null)" ]]; then
|
|
2816
|
+
printf '\n' >> "$target"
|
|
2817
|
+
fi
|
|
2818
|
+
printf '\n' >> "$target"
|
|
2819
|
+
cat "$auto_file" >> "$target"
|
|
2820
|
+
return 0
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
# Write the workspace CLAUDE.md to the given path. The auto-generated content
|
|
2824
|
+
# is wrapped in BEGIN/END markers and spliced into any existing CLAUDE.md so
|
|
2825
|
+
# user content above and below the markers is preserved across regenerations.
|
|
2756
2826
|
# Usage: _context_write_file "/path/to/CLAUDE.md"
|
|
2757
2827
|
_context_write_file() {
|
|
2758
|
-
local
|
|
2828
|
+
local target="$1"
|
|
2759
2829
|
local workspace
|
|
2760
2830
|
workspace=$(config_workspace_name)
|
|
2761
2831
|
|
|
2762
|
-
|
|
2832
|
+
local output
|
|
2833
|
+
output=$(mktemp -t revo-auto.XXXXXX)
|
|
2834
|
+
|
|
2835
|
+
# Start auto block (markers + header)
|
|
2763
2836
|
{
|
|
2837
|
+
printf '%s\n' "$CONTEXT_BEGIN_MARKER"
|
|
2764
2838
|
printf '# Workspace Context (auto-generated by revo)\n'
|
|
2765
2839
|
printf '\n'
|
|
2766
2840
|
printf 'This workspace contains multiple repositories managed by revo.\n'
|
|
@@ -2768,8 +2842,9 @@ _context_write_file() {
|
|
|
2768
2842
|
printf 'Workspace: **%s**\n' "$workspace"
|
|
2769
2843
|
fi
|
|
2770
2844
|
printf '\n'
|
|
2771
|
-
printf '> This
|
|
2772
|
-
printf '>
|
|
2845
|
+
printf '> This block is auto-generated by `revo context`. Edits between the\n'
|
|
2846
|
+
printf '> BEGIN/END markers will be lost on regeneration. Add your own content\n'
|
|
2847
|
+
printf '> above or below the markers.\n'
|
|
2773
2848
|
printf '\n'
|
|
2774
2849
|
printf '## Repos\n'
|
|
2775
2850
|
printf '\n'
|
|
@@ -2927,6 +3002,15 @@ _context_write_file() {
|
|
|
2927
3002
|
printf -- '- `revo exec "cmd" [--tag t]` — run command in filtered repos\n'
|
|
2928
3003
|
printf -- '- `revo checkout <branch> [--tag t]` — switch branch across repos\n'
|
|
2929
3004
|
printf '\n'
|
|
3005
|
+
printf '**Issues (cross-repo, via gh CLI):**\n'
|
|
3006
|
+
printf -- '- `revo issue list [--tag t] [--state open|closed|all] [--label L] [--json]` — list issues across repos\n'
|
|
3007
|
+
printf -- '- `revo issue create --repo <name> "title" [--body b] [--label L] [--feature F]` — create in one repo\n'
|
|
3008
|
+
printf -- '- `revo issue create --tag <t> "title" [--body b] [--feature F]` — create in every matching repo, cross-referenced\n'
|
|
3009
|
+
printf '\n'
|
|
3010
|
+
printf 'Use `--json` on `revo issue list` to get a flat JSON array (each entry has\n'
|
|
3011
|
+
printf 'a `repo` field) — easy to filter or pipe into jq when reasoning about\n'
|
|
3012
|
+
printf 'cross-repo issue state.\n'
|
|
3013
|
+
printf '\n'
|
|
2930
3014
|
printf '### Tag filtering\n'
|
|
2931
3015
|
printf '\n'
|
|
2932
3016
|
printf 'All commands support `--tag <tag>` to target specific repos:\n'
|
|
@@ -2946,7 +3030,13 @@ _context_write_file() {
|
|
|
2946
3030
|
printf 'revo commit "feat: my feature" # commits all dirty repos\n'
|
|
2947
3031
|
printf 'revo pr "My feature" # coordinated PRs\n'
|
|
2948
3032
|
printf '```\n'
|
|
3033
|
+
printf '\n'
|
|
3034
|
+
printf '%s\n' "$CONTEXT_END_MARKER"
|
|
2949
3035
|
} >> "$output"
|
|
3036
|
+
|
|
3037
|
+
# Splice the auto block into the target file, preserving any user content.
|
|
3038
|
+
_context_merge_into "$output" "$target"
|
|
3039
|
+
rm -f "$output"
|
|
2950
3040
|
}
|
|
2951
3041
|
|
|
2952
3042
|
cmd_context() {
|
|
@@ -3577,6 +3667,433 @@ cmd_pr() {
|
|
|
3577
3667
|
return 0
|
|
3578
3668
|
}
|
|
3579
3669
|
|
|
3670
|
+
# === lib/commands/issue.sh ===
|
|
3671
|
+
# Revo CLI - issue command
|
|
3672
|
+
# List and create GitHub issues across workspace repos via the gh CLI.
|
|
3673
|
+
|
|
3674
|
+
cmd_issue() {
|
|
3675
|
+
local subcommand="${1:-}"
|
|
3676
|
+
[[ $# -gt 0 ]] && shift
|
|
3677
|
+
|
|
3678
|
+
case "$subcommand" in
|
|
3679
|
+
list|ls)
|
|
3680
|
+
_issue_list "$@"
|
|
3681
|
+
;;
|
|
3682
|
+
create|new)
|
|
3683
|
+
_issue_create "$@"
|
|
3684
|
+
;;
|
|
3685
|
+
--help|-h|help|"")
|
|
3686
|
+
_issue_help
|
|
3687
|
+
return 0
|
|
3688
|
+
;;
|
|
3689
|
+
*)
|
|
3690
|
+
ui_step_error "Unknown subcommand: $subcommand"
|
|
3691
|
+
_issue_help
|
|
3692
|
+
return 1
|
|
3693
|
+
;;
|
|
3694
|
+
esac
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
_issue_help() {
|
|
3698
|
+
cat << 'EOF'
|
|
3699
|
+
Usage: revo issue <subcommand> [options]
|
|
3700
|
+
|
|
3701
|
+
Subcommands:
|
|
3702
|
+
list List issues across workspace repos
|
|
3703
|
+
create Create issue(s) in workspace repos
|
|
3704
|
+
|
|
3705
|
+
revo issue list [options]
|
|
3706
|
+
--tag TAG Filter repos by tag
|
|
3707
|
+
--state open|closed|all Issue state (default: open)
|
|
3708
|
+
--label LABEL Filter by label
|
|
3709
|
+
--limit N Per-repo limit (default: 30)
|
|
3710
|
+
--json Emit a flat JSON array (one entry per issue,
|
|
3711
|
+
with a "repo" field)
|
|
3712
|
+
|
|
3713
|
+
revo issue create (--repo NAME | --tag TAG) "TITLE" [options]
|
|
3714
|
+
--repo NAME Target a single repo by path basename
|
|
3715
|
+
--tag TAG Target every repo matching the tag (cross-references
|
|
3716
|
+
all created issues in each body)
|
|
3717
|
+
--body BODY Issue body (default: revo-generated stub)
|
|
3718
|
+
--label L,L Comma-separated labels
|
|
3719
|
+
--assignee USER GitHub username to assign
|
|
3720
|
+
--feature NAME Append issue links to .revo/features/NAME.md and
|
|
3721
|
+
reference the brief from each issue body
|
|
3722
|
+
EOF
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
# --- list ----------------------------------------------------------------
|
|
3726
|
+
|
|
3727
|
+
_issue_list() {
|
|
3728
|
+
local tag=""
|
|
3729
|
+
local state="open"
|
|
3730
|
+
local label=""
|
|
3731
|
+
local limit="30"
|
|
3732
|
+
local as_json=0
|
|
3733
|
+
|
|
3734
|
+
while [[ $# -gt 0 ]]; do
|
|
3735
|
+
case "$1" in
|
|
3736
|
+
--tag) tag="$2"; shift 2 ;;
|
|
3737
|
+
--state) state="$2"; shift 2 ;;
|
|
3738
|
+
--label) label="$2"; shift 2 ;;
|
|
3739
|
+
--limit) limit="$2"; shift 2 ;;
|
|
3740
|
+
--json) as_json=1; shift ;;
|
|
3741
|
+
--help|-h) _issue_help; return 0 ;;
|
|
3742
|
+
*) ui_step_error "Unknown option: $1"; return 1 ;;
|
|
3743
|
+
esac
|
|
3744
|
+
done
|
|
3745
|
+
|
|
3746
|
+
if ! command -v gh >/dev/null 2>&1; then
|
|
3747
|
+
ui_step_error "gh CLI not found. Install from https://cli.github.com/"
|
|
3748
|
+
return 1
|
|
3749
|
+
fi
|
|
3750
|
+
|
|
3751
|
+
config_require_workspace || return 1
|
|
3752
|
+
|
|
3753
|
+
local repos
|
|
3754
|
+
repos=$(config_get_repos "$tag")
|
|
3755
|
+
if [[ -z "$repos" ]]; then
|
|
3756
|
+
if [[ $as_json -eq 1 ]]; then
|
|
3757
|
+
printf '[]\n'
|
|
3758
|
+
return 0
|
|
3759
|
+
fi
|
|
3760
|
+
ui_step_error "No repositories configured"
|
|
3761
|
+
return 1
|
|
3762
|
+
fi
|
|
3763
|
+
|
|
3764
|
+
# Build the per-repo gh args once
|
|
3765
|
+
local gh_extra=()
|
|
3766
|
+
if [[ -n "$label" ]]; then
|
|
3767
|
+
gh_extra+=( --label "$label" )
|
|
3768
|
+
fi
|
|
3769
|
+
|
|
3770
|
+
if [[ $as_json -eq 1 ]]; then
|
|
3771
|
+
_issue_list_json "$repos" "$state" "$limit" "${gh_extra[@]}"
|
|
3772
|
+
return $?
|
|
3773
|
+
fi
|
|
3774
|
+
|
|
3775
|
+
_issue_list_human "$repos" "$state" "$limit" "${gh_extra[@]}"
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
# Print a flat JSON array of issues across all repos. Each entry has a
|
|
3779
|
+
# "repo" field added in addition to the gh-provided fields. Output goes to
|
|
3780
|
+
# stdout, errors are silent (we want callers like Claude to be able to
|
|
3781
|
+
# pipe into jq).
|
|
3782
|
+
_issue_list_json() {
|
|
3783
|
+
local repos="$1"
|
|
3784
|
+
local state="$2"
|
|
3785
|
+
local limit="$3"
|
|
3786
|
+
shift 3
|
|
3787
|
+
local gh_extra=( "$@" )
|
|
3788
|
+
|
|
3789
|
+
local entries=""
|
|
3790
|
+
local first=1
|
|
3791
|
+
|
|
3792
|
+
local repo
|
|
3793
|
+
while IFS= read -r repo; do
|
|
3794
|
+
[[ -z "$repo" ]] && continue
|
|
3795
|
+
local path
|
|
3796
|
+
path=$(yaml_get_path "$repo")
|
|
3797
|
+
local full_path="$REVO_REPOS_DIR/$path"
|
|
3798
|
+
[[ ! -d "$full_path" ]] && continue
|
|
3799
|
+
|
|
3800
|
+
# gh's --jq runs the embedded jq engine and emits one compact JSON
|
|
3801
|
+
# value per line by default. We map each issue, prepend a "repo"
|
|
3802
|
+
# field, and stream the results into our concatenated array.
|
|
3803
|
+
local lines
|
|
3804
|
+
if lines=$(cd "$full_path" && gh issue list \
|
|
3805
|
+
--state "$state" --limit "$limit" "${gh_extra[@]}" \
|
|
3806
|
+
--json number,title,state,labels,assignees,url,updatedAt,author \
|
|
3807
|
+
--jq ".[] | {repo: \"$path\"} + ." 2>/dev/null); then
|
|
3808
|
+
local line
|
|
3809
|
+
while IFS= read -r line; do
|
|
3810
|
+
[[ -z "$line" ]] && continue
|
|
3811
|
+
if [[ $first -eq 1 ]]; then
|
|
3812
|
+
entries="$line"
|
|
3813
|
+
first=0
|
|
3814
|
+
else
|
|
3815
|
+
entries="$entries,$line"
|
|
3816
|
+
fi
|
|
3817
|
+
done <<< "$lines"
|
|
3818
|
+
fi
|
|
3819
|
+
done <<< "$repos"
|
|
3820
|
+
|
|
3821
|
+
printf '[%s]\n' "$entries"
|
|
3822
|
+
return 0
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
# Print a human-readable per-repo summary of issues. Each repo gets its
|
|
3826
|
+
# own block separated by ui_bar_line.
|
|
3827
|
+
_issue_list_human() {
|
|
3828
|
+
local repos="$1"
|
|
3829
|
+
local state="$2"
|
|
3830
|
+
local limit="$3"
|
|
3831
|
+
shift 3
|
|
3832
|
+
local gh_extra=( "$@" )
|
|
3833
|
+
|
|
3834
|
+
ui_intro "Revo - Issues ($state)"
|
|
3835
|
+
|
|
3836
|
+
local total=0
|
|
3837
|
+
local repo_count=0
|
|
3838
|
+
local skip_count=0
|
|
3839
|
+
|
|
3840
|
+
local repo
|
|
3841
|
+
while IFS= read -r repo; do
|
|
3842
|
+
[[ -z "$repo" ]] && continue
|
|
3843
|
+
local path
|
|
3844
|
+
path=$(yaml_get_path "$repo")
|
|
3845
|
+
local full_path="$REVO_REPOS_DIR/$path"
|
|
3846
|
+
|
|
3847
|
+
if [[ ! -d "$full_path" ]]; then
|
|
3848
|
+
ui_step_done "Skipped (not cloned):" "$path"
|
|
3849
|
+
skip_count=$((skip_count + 1))
|
|
3850
|
+
continue
|
|
3851
|
+
fi
|
|
3852
|
+
|
|
3853
|
+
ui_bar_line
|
|
3854
|
+
ui_step "$path"
|
|
3855
|
+
|
|
3856
|
+
local count_output
|
|
3857
|
+
if ! count_output=$(cd "$full_path" && gh issue list \
|
|
3858
|
+
--state "$state" --limit "$limit" "${gh_extra[@]}" \
|
|
3859
|
+
--json number --jq 'length' 2>&1); then
|
|
3860
|
+
ui_step_error "Failed: $count_output"
|
|
3861
|
+
continue
|
|
3862
|
+
fi
|
|
3863
|
+
|
|
3864
|
+
local repo_total="${count_output:-0}"
|
|
3865
|
+
if [[ "$repo_total" == "0" ]]; then
|
|
3866
|
+
printf '%s %s\n' "$(ui_bar)" "$(ui_dim "No $state issues")"
|
|
3867
|
+
continue
|
|
3868
|
+
fi
|
|
3869
|
+
|
|
3870
|
+
local pretty
|
|
3871
|
+
if pretty=$(cd "$full_path" && gh issue list \
|
|
3872
|
+
--state "$state" --limit "$limit" "${gh_extra[@]}" 2>&1); then
|
|
3873
|
+
local line
|
|
3874
|
+
while IFS= read -r line; do
|
|
3875
|
+
printf '%s %s\n' "$(ui_bar)" "$line"
|
|
3876
|
+
done <<< "$pretty"
|
|
3877
|
+
fi
|
|
3878
|
+
|
|
3879
|
+
total=$((total + repo_total))
|
|
3880
|
+
repo_count=$((repo_count + 1))
|
|
3881
|
+
done <<< "$repos"
|
|
3882
|
+
|
|
3883
|
+
ui_bar_line
|
|
3884
|
+
local msg="Found $total issue(s) across $repo_count repo(s)"
|
|
3885
|
+
[[ $skip_count -gt 0 ]] && msg+=", $skip_count skipped"
|
|
3886
|
+
ui_outro "$msg"
|
|
3887
|
+
return 0
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
# --- create --------------------------------------------------------------
|
|
3891
|
+
|
|
3892
|
+
_issue_create() {
|
|
3893
|
+
local title=""
|
|
3894
|
+
local repo=""
|
|
3895
|
+
local tag=""
|
|
3896
|
+
local body=""
|
|
3897
|
+
local labels=""
|
|
3898
|
+
local assignee=""
|
|
3899
|
+
local feature=""
|
|
3900
|
+
|
|
3901
|
+
while [[ $# -gt 0 ]]; do
|
|
3902
|
+
case "$1" in
|
|
3903
|
+
--repo) repo="$2"; shift 2 ;;
|
|
3904
|
+
--tag) tag="$2"; shift 2 ;;
|
|
3905
|
+
--body) body="$2"; shift 2 ;;
|
|
3906
|
+
--label) labels="$2"; shift 2 ;;
|
|
3907
|
+
--assignee) assignee="$2"; shift 2 ;;
|
|
3908
|
+
--feature) feature="$2"; shift 2 ;;
|
|
3909
|
+
--help|-h) _issue_help; return 0 ;;
|
|
3910
|
+
-*) ui_step_error "Unknown option: $1"; return 1 ;;
|
|
3911
|
+
*)
|
|
3912
|
+
if [[ -z "$title" ]]; then
|
|
3913
|
+
title="$1"
|
|
3914
|
+
else
|
|
3915
|
+
ui_step_error "Unexpected argument: $1"
|
|
3916
|
+
return 1
|
|
3917
|
+
fi
|
|
3918
|
+
shift
|
|
3919
|
+
;;
|
|
3920
|
+
esac
|
|
3921
|
+
done
|
|
3922
|
+
|
|
3923
|
+
if [[ -z "$title" ]]; then
|
|
3924
|
+
ui_step_error "Usage: revo issue create (--repo NAME | --tag TAG) \"TITLE\""
|
|
3925
|
+
return 1
|
|
3926
|
+
fi
|
|
3927
|
+
|
|
3928
|
+
if [[ -z "$repo" ]] && [[ -z "$tag" ]]; then
|
|
3929
|
+
ui_step_error "Either --repo or --tag is required"
|
|
3930
|
+
return 1
|
|
3931
|
+
fi
|
|
3932
|
+
|
|
3933
|
+
if [[ -n "$repo" ]] && [[ -n "$tag" ]]; then
|
|
3934
|
+
ui_step_error "Use either --repo or --tag, not both"
|
|
3935
|
+
return 1
|
|
3936
|
+
fi
|
|
3937
|
+
|
|
3938
|
+
if ! command -v gh >/dev/null 2>&1; then
|
|
3939
|
+
ui_step_error "gh CLI not found. Install from https://cli.github.com/"
|
|
3940
|
+
return 1
|
|
3941
|
+
fi
|
|
3942
|
+
|
|
3943
|
+
config_require_workspace || return 1
|
|
3944
|
+
|
|
3945
|
+
# Resolve target repo paths
|
|
3946
|
+
local target_paths=()
|
|
3947
|
+
if [[ -n "$repo" ]]; then
|
|
3948
|
+
local idx
|
|
3949
|
+
idx=$(yaml_find_by_name "$repo")
|
|
3950
|
+
if [[ $idx -lt 0 ]]; then
|
|
3951
|
+
ui_step_error "Repo not found in revo.yaml: $repo"
|
|
3952
|
+
return 1
|
|
3953
|
+
fi
|
|
3954
|
+
target_paths+=("$repo")
|
|
3955
|
+
else
|
|
3956
|
+
local repos
|
|
3957
|
+
repos=$(config_get_repos "$tag")
|
|
3958
|
+
local r
|
|
3959
|
+
while IFS= read -r r; do
|
|
3960
|
+
[[ -z "$r" ]] && continue
|
|
3961
|
+
local p
|
|
3962
|
+
p=$(yaml_get_path "$r")
|
|
3963
|
+
target_paths+=("$p")
|
|
3964
|
+
done <<< "$repos"
|
|
3965
|
+
fi
|
|
3966
|
+
|
|
3967
|
+
if [[ ${#target_paths[@]} -eq 0 ]]; then
|
|
3968
|
+
ui_step_error "No matching repos"
|
|
3969
|
+
return 1
|
|
3970
|
+
fi
|
|
3971
|
+
|
|
3972
|
+
ui_intro "Revo - Create issue: $title"
|
|
3973
|
+
|
|
3974
|
+
# Validate feature brief if --feature was given
|
|
3975
|
+
local feature_brief=""
|
|
3976
|
+
if [[ -n "$feature" ]]; then
|
|
3977
|
+
feature_brief="$REVO_WORKSPACE_ROOT/.revo/features/$feature.md"
|
|
3978
|
+
if [[ ! -f "$feature_brief" ]]; then
|
|
3979
|
+
ui_step_error "Feature brief not found: .revo/features/$feature.md"
|
|
3980
|
+
ui_info "$(ui_dim "Run 'revo feature $feature' first to create it")"
|
|
3981
|
+
return 1
|
|
3982
|
+
fi
|
|
3983
|
+
fi
|
|
3984
|
+
|
|
3985
|
+
# Build the initial body that all created issues share
|
|
3986
|
+
local effective_body="$body"
|
|
3987
|
+
if [[ -z "$effective_body" ]]; then
|
|
3988
|
+
effective_body="Created by \`revo issue create\`."
|
|
3989
|
+
fi
|
|
3990
|
+
if [[ -n "$feature" ]]; then
|
|
3991
|
+
effective_body="$effective_body"$'\n\n'"Part of feature: \`$feature\` (see \`.revo/features/$feature.md\`)"
|
|
3992
|
+
fi
|
|
3993
|
+
|
|
3994
|
+
# --- Pass 1: create issues -------------------------------------------
|
|
3995
|
+
local issue_paths=()
|
|
3996
|
+
local issue_urls=()
|
|
3997
|
+
local issue_numbers=()
|
|
3998
|
+
local fail_count=0
|
|
3999
|
+
local skip_count=0
|
|
4000
|
+
|
|
4001
|
+
local p
|
|
4002
|
+
for p in "${target_paths[@]}"; do
|
|
4003
|
+
local full_path="$REVO_REPOS_DIR/$p"
|
|
4004
|
+
|
|
4005
|
+
if [[ ! -d "$full_path" ]]; then
|
|
4006
|
+
ui_step_done "Skipped (not cloned):" "$p"
|
|
4007
|
+
skip_count=$((skip_count + 1))
|
|
4008
|
+
continue
|
|
4009
|
+
fi
|
|
4010
|
+
|
|
4011
|
+
local gh_args=( --title "$title" --body "$effective_body" )
|
|
4012
|
+
if [[ -n "$labels" ]]; then
|
|
4013
|
+
gh_args+=( --label "$labels" )
|
|
4014
|
+
fi
|
|
4015
|
+
if [[ -n "$assignee" ]]; then
|
|
4016
|
+
gh_args+=( --assignee "$assignee" )
|
|
4017
|
+
fi
|
|
4018
|
+
|
|
4019
|
+
local output
|
|
4020
|
+
if output=$(cd "$full_path" && gh issue create "${gh_args[@]}" 2>&1); then
|
|
4021
|
+
local url
|
|
4022
|
+
url=$(printf '%s' "$output" | grep -Eo 'https://github\.com/[^ ]+/issues/[0-9]+' | tail -1)
|
|
4023
|
+
if [[ -z "$url" ]]; then
|
|
4024
|
+
url="$output"
|
|
4025
|
+
fi
|
|
4026
|
+
local number="${url##*/}"
|
|
4027
|
+
issue_paths+=("$p")
|
|
4028
|
+
issue_urls+=("$url")
|
|
4029
|
+
issue_numbers+=("$number")
|
|
4030
|
+
ui_step_done "Created:" "$p → $url"
|
|
4031
|
+
else
|
|
4032
|
+
ui_step_error "Failed: $p"
|
|
4033
|
+
ui_info "$(ui_dim "$output")"
|
|
4034
|
+
fail_count=$((fail_count + 1))
|
|
4035
|
+
fi
|
|
4036
|
+
done
|
|
4037
|
+
|
|
4038
|
+
# --- Pass 2: cross-references ----------------------------------------
|
|
4039
|
+
if [[ ${#issue_paths[@]} -gt 1 ]]; then
|
|
4040
|
+
ui_bar_line
|
|
4041
|
+
ui_step "Linking issues with cross-references..."
|
|
4042
|
+
|
|
4043
|
+
local xref
|
|
4044
|
+
xref=$'\n\n---\n**Coordinated issues (revo):**\n'
|
|
4045
|
+
local i
|
|
4046
|
+
for ((i = 0; i < ${#issue_paths[@]}; i++)); do
|
|
4047
|
+
xref+="- ${issue_paths[$i]}: ${issue_urls[$i]}"$'\n'
|
|
4048
|
+
done
|
|
4049
|
+
|
|
4050
|
+
for ((i = 0; i < ${#issue_paths[@]}; i++)); do
|
|
4051
|
+
local repo_path="${issue_paths[$i]}"
|
|
4052
|
+
local issue_number="${issue_numbers[$i]}"
|
|
4053
|
+
local full_path="$REVO_REPOS_DIR/$repo_path"
|
|
4054
|
+
local combined_body="$effective_body$xref"
|
|
4055
|
+
|
|
4056
|
+
if (cd "$full_path" && gh issue edit "$issue_number" --body "$combined_body" >/dev/null 2>&1); then
|
|
4057
|
+
ui_step_done "Linked:" "$repo_path"
|
|
4058
|
+
else
|
|
4059
|
+
ui_step_error "Failed to link: $repo_path"
|
|
4060
|
+
fi
|
|
4061
|
+
done
|
|
4062
|
+
fi
|
|
4063
|
+
|
|
4064
|
+
# --- Pass 3: append to feature brief ---------------------------------
|
|
4065
|
+
if [[ -n "$feature_brief" ]] && [[ ${#issue_paths[@]} -gt 0 ]]; then
|
|
4066
|
+
ui_bar_line
|
|
4067
|
+
ui_step "Appending to feature brief: $feature"
|
|
4068
|
+
{
|
|
4069
|
+
printf '\n## Issues (created by revo)\n\n'
|
|
4070
|
+
local i
|
|
4071
|
+
for ((i = 0; i < ${#issue_paths[@]}; i++)); do
|
|
4072
|
+
printf -- '- **%s**: %s\n' "${issue_paths[$i]}" "${issue_urls[$i]}"
|
|
4073
|
+
done
|
|
4074
|
+
} >> "$feature_brief"
|
|
4075
|
+
ui_step_done "Updated:" ".revo/features/$feature.md"
|
|
4076
|
+
fi
|
|
4077
|
+
|
|
4078
|
+
ui_bar_line
|
|
4079
|
+
|
|
4080
|
+
# Print URLs to stdout (LLM-friendly: one URL per line, parseable)
|
|
4081
|
+
local i
|
|
4082
|
+
for ((i = 0; i < ${#issue_urls[@]}; i++)); do
|
|
4083
|
+
printf '%s\n' "${issue_urls[$i]}"
|
|
4084
|
+
done
|
|
4085
|
+
|
|
4086
|
+
if [[ $fail_count -eq 0 ]]; then
|
|
4087
|
+
local msg="Created ${#issue_paths[@]} issue(s)"
|
|
4088
|
+
[[ $skip_count -gt 0 ]] && msg+=", $skip_count skipped"
|
|
4089
|
+
ui_outro "$msg"
|
|
4090
|
+
return 0
|
|
4091
|
+
else
|
|
4092
|
+
ui_outro_cancel "${#issue_paths[@]} created, $fail_count failed"
|
|
4093
|
+
return 1
|
|
4094
|
+
fi
|
|
4095
|
+
}
|
|
4096
|
+
|
|
3580
4097
|
# === Main ===
|
|
3581
4098
|
# --- Help ---
|
|
3582
4099
|
show_help() {
|
|
@@ -3603,6 +4120,8 @@ Claude-first commands:
|
|
|
3603
4120
|
commit MSG [--tag TAG] Commit changes across dirty repos
|
|
3604
4121
|
push [--tag TAG] Push branches across repositories
|
|
3605
4122
|
pr TITLE [--tag TAG] Create coordinated PRs via gh CLI
|
|
4123
|
+
issue list [options] List issues across workspace repos
|
|
4124
|
+
issue create [options] TITLE Create issue(s) in workspace repos
|
|
3606
4125
|
|
|
3607
4126
|
Options:
|
|
3608
4127
|
--tag TAG Filter repositories by tag
|
|
@@ -3624,6 +4143,10 @@ Examples:
|
|
|
3624
4143
|
revo commit "wire up clock endpoint"
|
|
3625
4144
|
revo push
|
|
3626
4145
|
revo pr "Clock endpoint for students" --tag backend
|
|
4146
|
+
revo issue list --state open # all open issues
|
|
4147
|
+
revo issue list --tag backend --json # JSON for Claude/jq
|
|
4148
|
+
revo issue create --repo backend "Add stats endpoint"
|
|
4149
|
+
revo issue create --tag mobile --feature stats "Add statistics screen"
|
|
3627
4150
|
|
|
3628
4151
|
Documentation: https://github.com/jippylong12/revo
|
|
3629
4152
|
EOF
|
|
@@ -3692,6 +4215,9 @@ main() {
|
|
|
3692
4215
|
pr)
|
|
3693
4216
|
cmd_pr "$@"
|
|
3694
4217
|
;;
|
|
4218
|
+
issue|issues)
|
|
4219
|
+
cmd_issue "$@"
|
|
4220
|
+
;;
|
|
3695
4221
|
--help|-h|help)
|
|
3696
4222
|
show_help
|
|
3697
4223
|
;;
|