@revotools/cli 0.4.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.
Files changed (2) hide show
  1. package/dist/revo +446 -1
  2. 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.4.0"
11
+ REVO_VERSION="0.5.0"
12
12
 
13
13
 
14
14
  # === lib/ui.sh ===
@@ -3002,6 +3002,15 @@ _context_write_file() {
3002
3002
  printf -- '- `revo exec "cmd" [--tag t]` — run command in filtered repos\n'
3003
3003
  printf -- '- `revo checkout <branch> [--tag t]` — switch branch across repos\n'
3004
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'
3005
3014
  printf '### Tag filtering\n'
3006
3015
  printf '\n'
3007
3016
  printf 'All commands support `--tag <tag>` to target specific repos:\n'
@@ -3658,6 +3667,433 @@ cmd_pr() {
3658
3667
  return 0
3659
3668
  }
3660
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
+
3661
4097
  # === Main ===
3662
4098
  # --- Help ---
3663
4099
  show_help() {
@@ -3684,6 +4120,8 @@ Claude-first commands:
3684
4120
  commit MSG [--tag TAG] Commit changes across dirty repos
3685
4121
  push [--tag TAG] Push branches across repositories
3686
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
3687
4125
 
3688
4126
  Options:
3689
4127
  --tag TAG Filter repositories by tag
@@ -3705,6 +4143,10 @@ Examples:
3705
4143
  revo commit "wire up clock endpoint"
3706
4144
  revo push
3707
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"
3708
4150
 
3709
4151
  Documentation: https://github.com/jippylong12/revo
3710
4152
  EOF
@@ -3773,6 +4215,9 @@ main() {
3773
4215
  pr)
3774
4216
  cmd_pr "$@"
3775
4217
  ;;
4218
+ issue|issues)
4219
+ cmd_issue "$@"
4220
+ ;;
3776
4221
  --help|-h|help)
3777
4222
  show_help
3778
4223
  ;;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revotools/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Claude-first multi-repo workspace manager (fork of Mars)",
5
5
  "bin": {
6
6
  "revo": "./dist/revo"