@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.
Files changed (2) hide show
  1. package/dist/revo +574 -48
  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.3.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
- # Create .gitignore
798
- printf 'repos/\n.revo/\n' > "$dir/.gitignore"
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, or append a revo section if the
1519
- # user already has their own CLAUDE.md in the workspace root.
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" ]] && ! grep -q "managed by revo" "$out" 2>/dev/null; then
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
- # Write the Claude-first onboarding CLAUDE.md (or append to user's own).
1697
- _init_write_claude_md
1698
-
1699
- # If we detected repos, immediately generate the full workspace context.
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
- # Write the workspace CLAUDE.md to the given path.
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 output="$1"
2828
+ local target="$1"
2759
2829
  local workspace
2760
2830
  workspace=$(config_workspace_name)
2761
2831
 
2762
- # Start file
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 file is regenerated by `revo context`. Manual edits below the\n'
2772
- printf '> `## Agent Instructions` section will be preserved across regeneration.\n'
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
  ;;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revotools/cli",
3
- "version": "0.3.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"