@revotools/cli 0.4.0 → 0.6.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 +1283 -101
- 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.6.0"
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
# === lib/ui.sh ===
|
|
@@ -736,6 +736,12 @@ yaml_add_repo() {
|
|
|
736
736
|
REVO_WORKSPACE_ROOT=""
|
|
737
737
|
REVO_CONFIG_FILE=""
|
|
738
738
|
REVO_REPOS_DIR=""
|
|
739
|
+
# When invoked from inside .revo/workspaces/<name>/, REVO_REPOS_DIR is
|
|
740
|
+
# overridden to point at that workspace dir so existing commands (status,
|
|
741
|
+
# commit, push, pr, exec, ...) operate on the workspace's copies of the
|
|
742
|
+
# repos rather than the source tree under repos/. REVO_ACTIVE_WORKSPACE
|
|
743
|
+
# holds the workspace name in that case (empty otherwise).
|
|
744
|
+
REVO_ACTIVE_WORKSPACE=""
|
|
739
745
|
|
|
740
746
|
# Find workspace root by searching upward for revo.yaml (or mars.yaml as fallback)
|
|
741
747
|
# Usage: config_find_root [start_dir]
|
|
@@ -749,6 +755,7 @@ config_find_root() {
|
|
|
749
755
|
REVO_WORKSPACE_ROOT="$current"
|
|
750
756
|
REVO_CONFIG_FILE="$current/revo.yaml"
|
|
751
757
|
REVO_REPOS_DIR="$current/repos"
|
|
758
|
+
_config_apply_workspace_override "$start_dir"
|
|
752
759
|
return 0
|
|
753
760
|
fi
|
|
754
761
|
# Fallback: support mars.yaml for migration from Mars
|
|
@@ -756,6 +763,7 @@ config_find_root() {
|
|
|
756
763
|
REVO_WORKSPACE_ROOT="$current"
|
|
757
764
|
REVO_CONFIG_FILE="$current/mars.yaml"
|
|
758
765
|
REVO_REPOS_DIR="$current/repos"
|
|
766
|
+
_config_apply_workspace_override "$start_dir"
|
|
759
767
|
return 0
|
|
760
768
|
fi
|
|
761
769
|
current="$(dirname "$current")"
|
|
@@ -764,6 +772,37 @@ config_find_root() {
|
|
|
764
772
|
return 1
|
|
765
773
|
}
|
|
766
774
|
|
|
775
|
+
# If start_dir is inside .revo/workspaces/<name>/, point REVO_REPOS_DIR at
|
|
776
|
+
# that workspace and remember the active workspace name. Otherwise leave
|
|
777
|
+
# things alone. Called from config_find_root after the root has been set.
|
|
778
|
+
_config_apply_workspace_override() {
|
|
779
|
+
local start_dir="$1"
|
|
780
|
+
REVO_ACTIVE_WORKSPACE=""
|
|
781
|
+
|
|
782
|
+
[[ -z "$REVO_WORKSPACE_ROOT" ]] && return 0
|
|
783
|
+
|
|
784
|
+
local prefix="$REVO_WORKSPACE_ROOT/.revo/workspaces/"
|
|
785
|
+
case "$start_dir/" in
|
|
786
|
+
"$prefix"*)
|
|
787
|
+
local rest="${start_dir#"$prefix"}"
|
|
788
|
+
local ws_name="${rest%%/*}"
|
|
789
|
+
if [[ -n "$ws_name" ]] && [[ -d "$prefix$ws_name" ]]; then
|
|
790
|
+
REVO_REPOS_DIR="$prefix$ws_name"
|
|
791
|
+
REVO_ACTIVE_WORKSPACE="$ws_name"
|
|
792
|
+
fi
|
|
793
|
+
;;
|
|
794
|
+
esac
|
|
795
|
+
return 0
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
# Always returns the source repos dir ($REVO_WORKSPACE_ROOT/repos),
|
|
799
|
+
# regardless of any active workspace override. Used by `revo workspace`
|
|
800
|
+
# itself, which must read from the source tree even when invoked from
|
|
801
|
+
# inside another workspace.
|
|
802
|
+
config_source_repos_dir() {
|
|
803
|
+
printf '%s/repos' "$REVO_WORKSPACE_ROOT"
|
|
804
|
+
}
|
|
805
|
+
|
|
767
806
|
# Initialize workspace in current directory
|
|
768
807
|
# Usage: config_init "workspace_name"
|
|
769
808
|
# Returns: 0 on success, 1 if already initialized
|
|
@@ -2941,6 +2980,35 @@ _context_write_file() {
|
|
|
2941
2980
|
printf '\n> Warning: a dependency cycle was detected. Listed in best-effort order.\n' >> "$output"
|
|
2942
2981
|
fi
|
|
2943
2982
|
|
|
2983
|
+
# Active workspaces (any .revo/workspaces/*/ dirs)
|
|
2984
|
+
local workspaces_dir="$REVO_WORKSPACE_ROOT/.revo/workspaces"
|
|
2985
|
+
if [[ -d "$workspaces_dir" ]]; then
|
|
2986
|
+
local has_workspaces=0
|
|
2987
|
+
local ws
|
|
2988
|
+
for ws in "$workspaces_dir"/*/; do
|
|
2989
|
+
[[ -d "$ws" ]] || continue
|
|
2990
|
+
if [[ $has_workspaces -eq 0 ]]; then
|
|
2991
|
+
{
|
|
2992
|
+
printf '\n'
|
|
2993
|
+
printf '## Active Workspaces\n'
|
|
2994
|
+
printf '\n'
|
|
2995
|
+
} >> "$output"
|
|
2996
|
+
has_workspaces=1
|
|
2997
|
+
fi
|
|
2998
|
+
local ws_name
|
|
2999
|
+
ws_name=$(basename "$ws")
|
|
3000
|
+
printf -- '- **%s** — `.revo/workspaces/%s/` (branch: feature/%s)\n' \
|
|
3001
|
+
"$ws_name" "$ws_name" "$ws_name" >> "$output"
|
|
3002
|
+
done
|
|
3003
|
+
if [[ $has_workspaces -eq 1 ]]; then
|
|
3004
|
+
{
|
|
3005
|
+
printf '\n'
|
|
3006
|
+
printf 'Run `revo workspaces` for branch/dirty status, or `cd` into one\n'
|
|
3007
|
+
printf 'and run revo from there to operate on the workspace copy.\n'
|
|
3008
|
+
} >> "$output"
|
|
3009
|
+
fi
|
|
3010
|
+
fi
|
|
3011
|
+
|
|
2944
3012
|
# Active features (any .revo/features/*.md files)
|
|
2945
3013
|
local features_dir="$REVO_WORKSPACE_ROOT/.revo/features"
|
|
2946
3014
|
if [[ -d "$features_dir" ]]; then
|
|
@@ -2978,6 +3046,8 @@ _context_write_file() {
|
|
|
2978
3046
|
printf '6. Use `revo commit "msg"` to commit across all repos at once\n'
|
|
2979
3047
|
printf '7. Use `revo feature <name>` to start a coordinated feature workspace\n'
|
|
2980
3048
|
printf '8. Use `revo pr "title"` to open coordinated pull requests\n'
|
|
3049
|
+
printf '9. Use `revo workspace <name>` to get a full-copy isolated workspace\n'
|
|
3050
|
+
printf ' under `.revo/workspaces/<name>/` (zero bootstrap, .env included)\n'
|
|
2981
3051
|
printf '\n'
|
|
2982
3052
|
printf '## Workspace Tool: revo\n'
|
|
2983
3053
|
printf '\n'
|
|
@@ -3002,6 +3072,26 @@ _context_write_file() {
|
|
|
3002
3072
|
printf -- '- `revo exec "cmd" [--tag t]` — run command in filtered repos\n'
|
|
3003
3073
|
printf -- '- `revo checkout <branch> [--tag t]` — switch branch across repos\n'
|
|
3004
3074
|
printf '\n'
|
|
3075
|
+
printf '**Workspaces (full-copy isolated workspaces):**\n'
|
|
3076
|
+
printf -- '- `revo workspace <name> [--tag t]` — full copy of repos into `.revo/workspaces/<name>/` on `feature/<name>`\n'
|
|
3077
|
+
printf -- '- `revo workspaces` — list active workspaces with branch and dirty state\n'
|
|
3078
|
+
printf -- '- `revo workspace <name> --delete [--force]` — remove a workspace\n'
|
|
3079
|
+
printf -- '- `revo workspace --clean` — remove workspaces whose branches are merged\n'
|
|
3080
|
+
printf '\n'
|
|
3081
|
+
printf 'Workspaces hardlink-copy everything (including `.env`, `node_modules`,\n'
|
|
3082
|
+
printf 'build artifacts) so Claude can start work with zero bootstrap. Run\n'
|
|
3083
|
+
printf '`revo` from inside `.revo/workspaces/<name>/` and it operates on the\n'
|
|
3084
|
+
printf 'workspace copies, not the source tree.\n'
|
|
3085
|
+
printf '\n'
|
|
3086
|
+
printf '**Issues (cross-repo, via gh CLI):**\n'
|
|
3087
|
+
printf -- '- `revo issue list [--tag t] [--state open|closed|all] [--label L] [--json]` — list issues across repos\n'
|
|
3088
|
+
printf -- '- `revo issue create --repo <name> "title" [--body b] [--label L] [--feature F]` — create in one repo\n'
|
|
3089
|
+
printf -- '- `revo issue create --tag <t> "title" [--body b] [--feature F]` — create in every matching repo, cross-referenced\n'
|
|
3090
|
+
printf '\n'
|
|
3091
|
+
printf 'Use `--json` on `revo issue list` to get a flat JSON array (each entry has\n'
|
|
3092
|
+
printf 'a `repo` field) — easy to filter or pipe into jq when reasoning about\n'
|
|
3093
|
+
printf 'cross-repo issue state.\n'
|
|
3094
|
+
printf '\n'
|
|
3005
3095
|
printf '### Tag filtering\n'
|
|
3006
3096
|
printf '\n'
|
|
3007
3097
|
printf 'All commands support `--tag <tag>` to target specific repos:\n'
|
|
@@ -3658,120 +3748,1212 @@ cmd_pr() {
|
|
|
3658
3748
|
return 0
|
|
3659
3749
|
}
|
|
3660
3750
|
|
|
3661
|
-
# ===
|
|
3662
|
-
#
|
|
3663
|
-
|
|
3664
|
-
cat << EOF
|
|
3665
|
-
Revo - Claude-first multi-repo workspace manager v$REVO_VERSION
|
|
3751
|
+
# === lib/commands/issue.sh ===
|
|
3752
|
+
# Revo CLI - issue command
|
|
3753
|
+
# List and create GitHub issues across workspace repos via the gh CLI.
|
|
3666
3754
|
|
|
3667
|
-
|
|
3755
|
+
cmd_issue() {
|
|
3756
|
+
local subcommand="${1:-}"
|
|
3757
|
+
[[ $# -gt 0 ]] && shift
|
|
3668
3758
|
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3759
|
+
case "$subcommand" in
|
|
3760
|
+
list|ls)
|
|
3761
|
+
_issue_list "$@"
|
|
3762
|
+
;;
|
|
3763
|
+
create|new)
|
|
3764
|
+
_issue_create "$@"
|
|
3765
|
+
;;
|
|
3766
|
+
--help|-h|help|"")
|
|
3767
|
+
_issue_help
|
|
3768
|
+
return 0
|
|
3769
|
+
;;
|
|
3770
|
+
*)
|
|
3771
|
+
ui_step_error "Unknown subcommand: $subcommand"
|
|
3772
|
+
_issue_help
|
|
3773
|
+
return 1
|
|
3774
|
+
;;
|
|
3775
|
+
esac
|
|
3776
|
+
}
|
|
3680
3777
|
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3778
|
+
_issue_help() {
|
|
3779
|
+
cat << 'EOF'
|
|
3780
|
+
Usage: revo issue <subcommand> [options]
|
|
3781
|
+
|
|
3782
|
+
Subcommands:
|
|
3783
|
+
list List issues across workspace repos
|
|
3784
|
+
create Create issue(s) in workspace repos
|
|
3785
|
+
|
|
3786
|
+
revo issue list [options]
|
|
3787
|
+
--tag TAG Filter repos by tag
|
|
3788
|
+
--state open|closed|all Issue state (default: open)
|
|
3789
|
+
--label LABEL Filter by label
|
|
3790
|
+
--limit N Per-repo limit (default: 30)
|
|
3791
|
+
--json Emit a flat JSON array (one entry per issue,
|
|
3792
|
+
with a "repo" field)
|
|
3793
|
+
|
|
3794
|
+
revo issue create (--repo NAME | --tag TAG) "TITLE" [options]
|
|
3795
|
+
--repo NAME Target a single repo by path basename
|
|
3796
|
+
--tag TAG Target every repo matching the tag (cross-references
|
|
3797
|
+
all created issues in each body)
|
|
3798
|
+
--body BODY Issue body (default: revo-generated stub)
|
|
3799
|
+
--label L,L Comma-separated labels
|
|
3800
|
+
--assignee USER GitHub username to assign
|
|
3801
|
+
--feature NAME Append issue links to .revo/features/NAME.md and
|
|
3802
|
+
reference the brief from each issue body
|
|
3803
|
+
EOF
|
|
3804
|
+
}
|
|
3687
3805
|
|
|
3688
|
-
|
|
3689
|
-
--tag TAG Filter repositories by tag
|
|
3690
|
-
--tags TAGS Comma-separated list of tags (for add)
|
|
3691
|
-
--force, -f Force operation (clone, checkout)
|
|
3692
|
-
--rebase, -r Use rebase when syncing
|
|
3693
|
-
--quiet, -q Suppress command output (exec)
|
|
3694
|
-
--body TEXT PR body (pr)
|
|
3695
|
-
--help, -h Show this help
|
|
3696
|
-
--version, -v Show version
|
|
3806
|
+
# --- list ----------------------------------------------------------------
|
|
3697
3807
|
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
revo feature clock-student --tag backend # coordinated branch
|
|
3705
|
-
revo commit "wire up clock endpoint"
|
|
3706
|
-
revo push
|
|
3707
|
-
revo pr "Clock endpoint for students" --tag backend
|
|
3808
|
+
_issue_list() {
|
|
3809
|
+
local tag=""
|
|
3810
|
+
local state="open"
|
|
3811
|
+
local label=""
|
|
3812
|
+
local limit="30"
|
|
3813
|
+
local as_json=0
|
|
3708
3814
|
|
|
3709
|
-
|
|
3710
|
-
|
|
3815
|
+
while [[ $# -gt 0 ]]; do
|
|
3816
|
+
case "$1" in
|
|
3817
|
+
--tag) tag="$2"; shift 2 ;;
|
|
3818
|
+
--state) state="$2"; shift 2 ;;
|
|
3819
|
+
--label) label="$2"; shift 2 ;;
|
|
3820
|
+
--limit) limit="$2"; shift 2 ;;
|
|
3821
|
+
--json) as_json=1; shift ;;
|
|
3822
|
+
--help|-h) _issue_help; return 0 ;;
|
|
3823
|
+
*) ui_step_error "Unknown option: $1"; return 1 ;;
|
|
3824
|
+
esac
|
|
3825
|
+
done
|
|
3826
|
+
|
|
3827
|
+
if ! command -v gh >/dev/null 2>&1; then
|
|
3828
|
+
ui_step_error "gh CLI not found. Install from https://cli.github.com/"
|
|
3829
|
+
return 1
|
|
3830
|
+
fi
|
|
3831
|
+
|
|
3832
|
+
config_require_workspace || return 1
|
|
3833
|
+
|
|
3834
|
+
local repos
|
|
3835
|
+
repos=$(config_get_repos "$tag")
|
|
3836
|
+
if [[ -z "$repos" ]]; then
|
|
3837
|
+
if [[ $as_json -eq 1 ]]; then
|
|
3838
|
+
printf '[]\n'
|
|
3839
|
+
return 0
|
|
3840
|
+
fi
|
|
3841
|
+
ui_step_error "No repositories configured"
|
|
3842
|
+
return 1
|
|
3843
|
+
fi
|
|
3844
|
+
|
|
3845
|
+
# Build the per-repo gh args once
|
|
3846
|
+
local gh_extra=()
|
|
3847
|
+
if [[ -n "$label" ]]; then
|
|
3848
|
+
gh_extra+=( --label "$label" )
|
|
3849
|
+
fi
|
|
3850
|
+
|
|
3851
|
+
if [[ $as_json -eq 1 ]]; then
|
|
3852
|
+
_issue_list_json "$repos" "$state" "$limit" "${gh_extra[@]}"
|
|
3853
|
+
return $?
|
|
3854
|
+
fi
|
|
3855
|
+
|
|
3856
|
+
_issue_list_human "$repos" "$state" "$limit" "${gh_extra[@]}"
|
|
3711
3857
|
}
|
|
3712
3858
|
|
|
3713
|
-
|
|
3714
|
-
|
|
3859
|
+
# Print a flat JSON array of issues across all repos. Each entry has a
|
|
3860
|
+
# "repo" field added in addition to the gh-provided fields. Output goes to
|
|
3861
|
+
# stdout, errors are silent (we want callers like Claude to be able to
|
|
3862
|
+
# pipe into jq).
|
|
3863
|
+
_issue_list_json() {
|
|
3864
|
+
local repos="$1"
|
|
3865
|
+
local state="$2"
|
|
3866
|
+
local limit="$3"
|
|
3867
|
+
shift 3
|
|
3868
|
+
local gh_extra=( "$@" )
|
|
3869
|
+
|
|
3870
|
+
local entries=""
|
|
3871
|
+
local first=1
|
|
3872
|
+
|
|
3873
|
+
local repo
|
|
3874
|
+
while IFS= read -r repo; do
|
|
3875
|
+
[[ -z "$repo" ]] && continue
|
|
3876
|
+
local path
|
|
3877
|
+
path=$(yaml_get_path "$repo")
|
|
3878
|
+
local full_path="$REVO_REPOS_DIR/$path"
|
|
3879
|
+
[[ ! -d "$full_path" ]] && continue
|
|
3880
|
+
|
|
3881
|
+
# gh's --jq runs the embedded jq engine and emits one compact JSON
|
|
3882
|
+
# value per line by default. We map each issue, prepend a "repo"
|
|
3883
|
+
# field, and stream the results into our concatenated array.
|
|
3884
|
+
local lines
|
|
3885
|
+
if lines=$(cd "$full_path" && gh issue list \
|
|
3886
|
+
--state "$state" --limit "$limit" "${gh_extra[@]}" \
|
|
3887
|
+
--json number,title,state,labels,assignees,url,updatedAt,author \
|
|
3888
|
+
--jq ".[] | {repo: \"$path\"} + ." 2>/dev/null); then
|
|
3889
|
+
local line
|
|
3890
|
+
while IFS= read -r line; do
|
|
3891
|
+
[[ -z "$line" ]] && continue
|
|
3892
|
+
if [[ $first -eq 1 ]]; then
|
|
3893
|
+
entries="$line"
|
|
3894
|
+
first=0
|
|
3895
|
+
else
|
|
3896
|
+
entries="$entries,$line"
|
|
3897
|
+
fi
|
|
3898
|
+
done <<< "$lines"
|
|
3899
|
+
fi
|
|
3900
|
+
done <<< "$repos"
|
|
3901
|
+
|
|
3902
|
+
printf '[%s]\n' "$entries"
|
|
3903
|
+
return 0
|
|
3715
3904
|
}
|
|
3716
3905
|
|
|
3717
|
-
#
|
|
3718
|
-
|
|
3719
|
-
|
|
3906
|
+
# Print a human-readable per-repo summary of issues. Each repo gets its
|
|
3907
|
+
# own block separated by ui_bar_line.
|
|
3908
|
+
_issue_list_human() {
|
|
3909
|
+
local repos="$1"
|
|
3910
|
+
local state="$2"
|
|
3911
|
+
local limit="$3"
|
|
3912
|
+
shift 3
|
|
3913
|
+
local gh_extra=( "$@" )
|
|
3720
3914
|
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3915
|
+
ui_intro "Revo - Issues ($state)"
|
|
3916
|
+
|
|
3917
|
+
local total=0
|
|
3918
|
+
local repo_count=0
|
|
3919
|
+
local skip_count=0
|
|
3920
|
+
|
|
3921
|
+
local repo
|
|
3922
|
+
while IFS= read -r repo; do
|
|
3923
|
+
[[ -z "$repo" ]] && continue
|
|
3924
|
+
local path
|
|
3925
|
+
path=$(yaml_get_path "$repo")
|
|
3926
|
+
local full_path="$REVO_REPOS_DIR/$path"
|
|
3927
|
+
|
|
3928
|
+
if [[ ! -d "$full_path" ]]; then
|
|
3929
|
+
ui_step_done "Skipped (not cloned):" "$path"
|
|
3930
|
+
skip_count=$((skip_count + 1))
|
|
3931
|
+
continue
|
|
3932
|
+
fi
|
|
3933
|
+
|
|
3934
|
+
ui_bar_line
|
|
3935
|
+
ui_step "$path"
|
|
3936
|
+
|
|
3937
|
+
local count_output
|
|
3938
|
+
if ! count_output=$(cd "$full_path" && gh issue list \
|
|
3939
|
+
--state "$state" --limit "$limit" "${gh_extra[@]}" \
|
|
3940
|
+
--json number --jq 'length' 2>&1); then
|
|
3941
|
+
ui_step_error "Failed: $count_output"
|
|
3942
|
+
continue
|
|
3943
|
+
fi
|
|
3944
|
+
|
|
3945
|
+
local repo_total="${count_output:-0}"
|
|
3946
|
+
if [[ "$repo_total" == "0" ]]; then
|
|
3947
|
+
printf '%s %s\n' "$(ui_bar)" "$(ui_dim "No $state issues")"
|
|
3948
|
+
continue
|
|
3949
|
+
fi
|
|
3950
|
+
|
|
3951
|
+
local pretty
|
|
3952
|
+
if pretty=$(cd "$full_path" && gh issue list \
|
|
3953
|
+
--state "$state" --limit "$limit" "${gh_extra[@]}" 2>&1); then
|
|
3954
|
+
local line
|
|
3955
|
+
while IFS= read -r line; do
|
|
3956
|
+
printf '%s %s\n' "$(ui_bar)" "$line"
|
|
3957
|
+
done <<< "$pretty"
|
|
3958
|
+
fi
|
|
3959
|
+
|
|
3960
|
+
total=$((total + repo_total))
|
|
3961
|
+
repo_count=$((repo_count + 1))
|
|
3962
|
+
done <<< "$repos"
|
|
3963
|
+
|
|
3964
|
+
ui_bar_line
|
|
3965
|
+
local msg="Found $total issue(s) across $repo_count repo(s)"
|
|
3966
|
+
[[ $skip_count -gt 0 ]] && msg+=", $skip_count skipped"
|
|
3967
|
+
ui_outro "$msg"
|
|
3968
|
+
return 0
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
# --- create --------------------------------------------------------------
|
|
3972
|
+
|
|
3973
|
+
_issue_create() {
|
|
3974
|
+
local title=""
|
|
3975
|
+
local repo=""
|
|
3976
|
+
local tag=""
|
|
3977
|
+
local body=""
|
|
3978
|
+
local labels=""
|
|
3979
|
+
local assignee=""
|
|
3980
|
+
local feature=""
|
|
3981
|
+
|
|
3982
|
+
while [[ $# -gt 0 ]]; do
|
|
3983
|
+
case "$1" in
|
|
3984
|
+
--repo) repo="$2"; shift 2 ;;
|
|
3985
|
+
--tag) tag="$2"; shift 2 ;;
|
|
3986
|
+
--body) body="$2"; shift 2 ;;
|
|
3987
|
+
--label) labels="$2"; shift 2 ;;
|
|
3988
|
+
--assignee) assignee="$2"; shift 2 ;;
|
|
3989
|
+
--feature) feature="$2"; shift 2 ;;
|
|
3990
|
+
--help|-h) _issue_help; return 0 ;;
|
|
3991
|
+
-*) ui_step_error "Unknown option: $1"; return 1 ;;
|
|
3992
|
+
*)
|
|
3993
|
+
if [[ -z "$title" ]]; then
|
|
3994
|
+
title="$1"
|
|
3995
|
+
else
|
|
3996
|
+
ui_step_error "Unexpected argument: $1"
|
|
3997
|
+
return 1
|
|
3998
|
+
fi
|
|
3999
|
+
shift
|
|
4000
|
+
;;
|
|
4001
|
+
esac
|
|
4002
|
+
done
|
|
4003
|
+
|
|
4004
|
+
if [[ -z "$title" ]]; then
|
|
4005
|
+
ui_step_error "Usage: revo issue create (--repo NAME | --tag TAG) \"TITLE\""
|
|
4006
|
+
return 1
|
|
3725
4007
|
fi
|
|
3726
4008
|
|
|
3727
|
-
|
|
3728
|
-
|
|
4009
|
+
if [[ -z "$repo" ]] && [[ -z "$tag" ]]; then
|
|
4010
|
+
ui_step_error "Either --repo or --tag is required"
|
|
4011
|
+
return 1
|
|
4012
|
+
fi
|
|
3729
4013
|
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
4014
|
+
if [[ -n "$repo" ]] && [[ -n "$tag" ]]; then
|
|
4015
|
+
ui_step_error "Use either --repo or --tag, not both"
|
|
4016
|
+
return 1
|
|
4017
|
+
fi
|
|
4018
|
+
|
|
4019
|
+
if ! command -v gh >/dev/null 2>&1; then
|
|
4020
|
+
ui_step_error "gh CLI not found. Install from https://cli.github.com/"
|
|
4021
|
+
return 1
|
|
4022
|
+
fi
|
|
4023
|
+
|
|
4024
|
+
config_require_workspace || return 1
|
|
4025
|
+
|
|
4026
|
+
# Resolve target repo paths
|
|
4027
|
+
local target_paths=()
|
|
4028
|
+
if [[ -n "$repo" ]]; then
|
|
4029
|
+
local idx
|
|
4030
|
+
idx=$(yaml_find_by_name "$repo")
|
|
4031
|
+
if [[ $idx -lt 0 ]]; then
|
|
4032
|
+
ui_step_error "Repo not found in revo.yaml: $repo"
|
|
4033
|
+
return 1
|
|
4034
|
+
fi
|
|
4035
|
+
target_paths+=("$repo")
|
|
4036
|
+
else
|
|
4037
|
+
local repos
|
|
4038
|
+
repos=$(config_get_repos "$tag")
|
|
4039
|
+
local r
|
|
4040
|
+
while IFS= read -r r; do
|
|
4041
|
+
[[ -z "$r" ]] && continue
|
|
4042
|
+
local p
|
|
4043
|
+
p=$(yaml_get_path "$r")
|
|
4044
|
+
target_paths+=("$p")
|
|
4045
|
+
done <<< "$repos"
|
|
4046
|
+
fi
|
|
4047
|
+
|
|
4048
|
+
if [[ ${#target_paths[@]} -eq 0 ]]; then
|
|
4049
|
+
ui_step_error "No matching repos"
|
|
4050
|
+
return 1
|
|
4051
|
+
fi
|
|
4052
|
+
|
|
4053
|
+
ui_intro "Revo - Create issue: $title"
|
|
4054
|
+
|
|
4055
|
+
# Validate feature brief if --feature was given
|
|
4056
|
+
local feature_brief=""
|
|
4057
|
+
if [[ -n "$feature" ]]; then
|
|
4058
|
+
feature_brief="$REVO_WORKSPACE_ROOT/.revo/features/$feature.md"
|
|
4059
|
+
if [[ ! -f "$feature_brief" ]]; then
|
|
4060
|
+
ui_step_error "Feature brief not found: .revo/features/$feature.md"
|
|
4061
|
+
ui_info "$(ui_dim "Run 'revo feature $feature' first to create it")"
|
|
4062
|
+
return 1
|
|
4063
|
+
fi
|
|
4064
|
+
fi
|
|
4065
|
+
|
|
4066
|
+
# Build the initial body that all created issues share
|
|
4067
|
+
local effective_body="$body"
|
|
4068
|
+
if [[ -z "$effective_body" ]]; then
|
|
4069
|
+
effective_body="Created by \`revo issue create\`."
|
|
4070
|
+
fi
|
|
4071
|
+
if [[ -n "$feature" ]]; then
|
|
4072
|
+
effective_body="$effective_body"$'\n\n'"Part of feature: \`$feature\` (see \`.revo/features/$feature.md\`)"
|
|
4073
|
+
fi
|
|
4074
|
+
|
|
4075
|
+
# --- Pass 1: create issues -------------------------------------------
|
|
4076
|
+
local issue_paths=()
|
|
4077
|
+
local issue_urls=()
|
|
4078
|
+
local issue_numbers=()
|
|
4079
|
+
local fail_count=0
|
|
4080
|
+
local skip_count=0
|
|
4081
|
+
|
|
4082
|
+
local p
|
|
4083
|
+
for p in "${target_paths[@]}"; do
|
|
4084
|
+
local full_path="$REVO_REPOS_DIR/$p"
|
|
4085
|
+
|
|
4086
|
+
if [[ ! -d "$full_path" ]]; then
|
|
4087
|
+
ui_step_done "Skipped (not cloned):" "$p"
|
|
4088
|
+
skip_count=$((skip_count + 1))
|
|
4089
|
+
continue
|
|
4090
|
+
fi
|
|
4091
|
+
|
|
4092
|
+
local gh_args=( --title "$title" --body "$effective_body" )
|
|
4093
|
+
if [[ -n "$labels" ]]; then
|
|
4094
|
+
gh_args+=( --label "$labels" )
|
|
4095
|
+
fi
|
|
4096
|
+
if [[ -n "$assignee" ]]; then
|
|
4097
|
+
gh_args+=( --assignee "$assignee" )
|
|
4098
|
+
fi
|
|
4099
|
+
|
|
4100
|
+
local output
|
|
4101
|
+
if output=$(cd "$full_path" && gh issue create "${gh_args[@]}" 2>&1); then
|
|
4102
|
+
local url
|
|
4103
|
+
url=$(printf '%s' "$output" | grep -Eo 'https://github\.com/[^ ]+/issues/[0-9]+' | tail -1)
|
|
4104
|
+
if [[ -z "$url" ]]; then
|
|
4105
|
+
url="$output"
|
|
4106
|
+
fi
|
|
4107
|
+
local number="${url##*/}"
|
|
4108
|
+
issue_paths+=("$p")
|
|
4109
|
+
issue_urls+=("$url")
|
|
4110
|
+
issue_numbers+=("$number")
|
|
4111
|
+
ui_step_done "Created:" "$p → $url"
|
|
4112
|
+
else
|
|
4113
|
+
ui_step_error "Failed: $p"
|
|
4114
|
+
ui_info "$(ui_dim "$output")"
|
|
4115
|
+
fail_count=$((fail_count + 1))
|
|
4116
|
+
fi
|
|
4117
|
+
done
|
|
4118
|
+
|
|
4119
|
+
# --- Pass 2: cross-references ----------------------------------------
|
|
4120
|
+
if [[ ${#issue_paths[@]} -gt 1 ]]; then
|
|
4121
|
+
ui_bar_line
|
|
4122
|
+
ui_step "Linking issues with cross-references..."
|
|
4123
|
+
|
|
4124
|
+
local xref
|
|
4125
|
+
xref=$'\n\n---\n**Coordinated issues (revo):**\n'
|
|
4126
|
+
local i
|
|
4127
|
+
for ((i = 0; i < ${#issue_paths[@]}; i++)); do
|
|
4128
|
+
xref+="- ${issue_paths[$i]}: ${issue_urls[$i]}"$'\n'
|
|
4129
|
+
done
|
|
4130
|
+
|
|
4131
|
+
for ((i = 0; i < ${#issue_paths[@]}; i++)); do
|
|
4132
|
+
local repo_path="${issue_paths[$i]}"
|
|
4133
|
+
local issue_number="${issue_numbers[$i]}"
|
|
4134
|
+
local full_path="$REVO_REPOS_DIR/$repo_path"
|
|
4135
|
+
local combined_body="$effective_body$xref"
|
|
4136
|
+
|
|
4137
|
+
if (cd "$full_path" && gh issue edit "$issue_number" --body "$combined_body" >/dev/null 2>&1); then
|
|
4138
|
+
ui_step_done "Linked:" "$repo_path"
|
|
4139
|
+
else
|
|
4140
|
+
ui_step_error "Failed to link: $repo_path"
|
|
4141
|
+
fi
|
|
4142
|
+
done
|
|
4143
|
+
fi
|
|
4144
|
+
|
|
4145
|
+
# --- Pass 3: append to feature brief ---------------------------------
|
|
4146
|
+
if [[ -n "$feature_brief" ]] && [[ ${#issue_paths[@]} -gt 0 ]]; then
|
|
4147
|
+
ui_bar_line
|
|
4148
|
+
ui_step "Appending to feature brief: $feature"
|
|
4149
|
+
{
|
|
4150
|
+
printf '\n## Issues (created by revo)\n\n'
|
|
4151
|
+
local i
|
|
4152
|
+
for ((i = 0; i < ${#issue_paths[@]}; i++)); do
|
|
4153
|
+
printf -- '- **%s**: %s\n' "${issue_paths[$i]}" "${issue_urls[$i]}"
|
|
4154
|
+
done
|
|
4155
|
+
} >> "$feature_brief"
|
|
4156
|
+
ui_step_done "Updated:" ".revo/features/$feature.md"
|
|
4157
|
+
fi
|
|
4158
|
+
|
|
4159
|
+
ui_bar_line
|
|
4160
|
+
|
|
4161
|
+
# Print URLs to stdout (LLM-friendly: one URL per line, parseable)
|
|
4162
|
+
local i
|
|
4163
|
+
for ((i = 0; i < ${#issue_urls[@]}; i++)); do
|
|
4164
|
+
printf '%s\n' "${issue_urls[$i]}"
|
|
4165
|
+
done
|
|
4166
|
+
|
|
4167
|
+
if [[ $fail_count -eq 0 ]]; then
|
|
4168
|
+
local msg="Created ${#issue_paths[@]} issue(s)"
|
|
4169
|
+
[[ $skip_count -gt 0 ]] && msg+=", $skip_count skipped"
|
|
4170
|
+
ui_outro "$msg"
|
|
4171
|
+
return 0
|
|
4172
|
+
else
|
|
4173
|
+
ui_outro_cancel "${#issue_paths[@]} created, $fail_count failed"
|
|
4174
|
+
return 1
|
|
4175
|
+
fi
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
# === lib/commands/workspace.sh ===
|
|
4179
|
+
# Revo CLI - workspace / workspaces commands
|
|
4180
|
+
# Full-copy workspaces under .revo/workspaces/<name>/. Unlike git worktrees
|
|
4181
|
+
# these copy *everything* — .env, node_modules, build artifacts — so Claude
|
|
4182
|
+
# can start work immediately with zero bootstrap. Hardlinks are used where
|
|
4183
|
+
# possible so the cost is near-zero on APFS/HFS+ and Linux.
|
|
4184
|
+
|
|
4185
|
+
# --- helpers ---
|
|
4186
|
+
|
|
4187
|
+
# Sanitize a workspace name: lowercase, hyphens for spaces, drop anything
|
|
4188
|
+
# that isn't [a-z0-9_-]. Keeps the resulting name safe to use as both a
|
|
4189
|
+
# directory name and a git branch suffix.
|
|
4190
|
+
_workspace_sanitize_name() {
|
|
4191
|
+
local raw="$1"
|
|
4192
|
+
local lowered
|
|
4193
|
+
lowered=$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]')
|
|
4194
|
+
lowered=$(printf '%s' "$lowered" | tr ' ' '-')
|
|
4195
|
+
# Replace anything not a-z0-9-_ with -
|
|
4196
|
+
lowered=$(printf '%s' "$lowered" | sed 's/[^a-z0-9_-]/-/g')
|
|
4197
|
+
# Collapse runs of - and trim leading/trailing -
|
|
4198
|
+
lowered=$(printf '%s' "$lowered" | sed -e 's/--*/-/g' -e 's/^-//' -e 's/-$//')
|
|
4199
|
+
printf '%s' "$lowered"
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
# Verify .revo/ is in the workspace .gitignore. Workspaces hardlink-copy
|
|
4203
|
+
# everything, including .env files, so this is a hard requirement to avoid
|
|
4204
|
+
# accidentally committing secrets via the parent git repo (if any).
|
|
4205
|
+
# Returns 0 if safe, 1 if .revo/ is not gitignored.
|
|
4206
|
+
_workspace_verify_gitignore_safe() {
|
|
4207
|
+
local gitignore="$REVO_WORKSPACE_ROOT/.gitignore"
|
|
4208
|
+
|
|
4209
|
+
# If the workspace root isn't itself a git repo, there's nothing to
|
|
4210
|
+
# accidentally commit into. Treat as safe.
|
|
4211
|
+
if [[ ! -d "$REVO_WORKSPACE_ROOT/.git" ]]; then
|
|
4212
|
+
return 0
|
|
4213
|
+
fi
|
|
4214
|
+
|
|
4215
|
+
if [[ ! -f "$gitignore" ]]; then
|
|
4216
|
+
return 1
|
|
4217
|
+
fi
|
|
4218
|
+
|
|
4219
|
+
# Match `.revo/`, `.revo`, `/.revo/`, `/.revo` — any of those works
|
|
4220
|
+
awk '
|
|
4221
|
+
{ sub(/^[[:space:]]+/, ""); sub(/[[:space:]]+$/, "") }
|
|
4222
|
+
$0 == ".revo" || $0 == ".revo/" || $0 == "/.revo" || $0 == "/.revo/" { found = 1; exit }
|
|
4223
|
+
END { exit !found }
|
|
4224
|
+
' "$gitignore"
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
# Copy a source repo into the workspace. Tries hardlinks first
|
|
4228
|
+
# (`cp -RLl`), falls back to a regular recursive copy. Always follows
|
|
4229
|
+
# symlinks so init-style symlinked repos are materialized as real
|
|
4230
|
+
# directories inside the workspace.
|
|
4231
|
+
# Usage: _workspace_copy_repo "src" "dest"
|
|
4232
|
+
# Returns: 0 on success
|
|
4233
|
+
_workspace_copy_repo() {
|
|
4234
|
+
local src="$1"
|
|
4235
|
+
local dest="$2"
|
|
4236
|
+
|
|
4237
|
+
# Make sure parent exists, dest does not
|
|
4238
|
+
mkdir -p "$(dirname "$dest")"
|
|
4239
|
+
rm -rf "$dest" 2>/dev/null
|
|
4240
|
+
|
|
4241
|
+
# Try hardlinks first. -R recursive, -L follow symlinks, -l hardlink.
|
|
4242
|
+
if cp -RLl "$src" "$dest" 2>/dev/null; then
|
|
4243
|
+
return 0
|
|
4244
|
+
fi
|
|
4245
|
+
|
|
4246
|
+
# Fall back to a real copy
|
|
4247
|
+
rm -rf "$dest" 2>/dev/null
|
|
4248
|
+
if cp -RL "$src" "$dest" 2>/dev/null; then
|
|
4249
|
+
return 0
|
|
4250
|
+
fi
|
|
4251
|
+
|
|
4252
|
+
return 1
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
# Print mtime in seconds-since-epoch for a path. Handles BSD (macOS) and
|
|
4256
|
+
# GNU (Linux) stat. Prints 0 if both fail.
|
|
4257
|
+
_workspace_mtime() {
|
|
4258
|
+
local path="$1"
|
|
4259
|
+
local mtime
|
|
4260
|
+
if mtime=$(stat -f %m "$path" 2>/dev/null); then
|
|
4261
|
+
printf '%s' "$mtime"
|
|
4262
|
+
return 0
|
|
4263
|
+
fi
|
|
4264
|
+
if mtime=$(stat -c %Y "$path" 2>/dev/null); then
|
|
4265
|
+
printf '%s' "$mtime"
|
|
4266
|
+
return 0
|
|
4267
|
+
fi
|
|
4268
|
+
printf '0'
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
# Format an age in seconds as a short, human string ("2h", "3d", ...).
|
|
4272
|
+
_workspace_format_age() {
|
|
4273
|
+
local secs="$1"
|
|
4274
|
+
if [[ -z "$secs" ]] || [[ "$secs" -le 0 ]]; then
|
|
4275
|
+
printf 'just now'
|
|
4276
|
+
return
|
|
4277
|
+
fi
|
|
4278
|
+
if [[ "$secs" -lt 60 ]]; then
|
|
4279
|
+
printf '%ds' "$secs"
|
|
4280
|
+
elif [[ "$secs" -lt 3600 ]]; then
|
|
4281
|
+
printf '%dm' $((secs / 60))
|
|
4282
|
+
elif [[ "$secs" -lt 86400 ]]; then
|
|
4283
|
+
printf '%dh' $((secs / 3600))
|
|
4284
|
+
else
|
|
4285
|
+
printf '%dd' $((secs / 86400))
|
|
4286
|
+
fi
|
|
4287
|
+
}
|
|
4288
|
+
|
|
4289
|
+
# Iterate the immediate subdirectories of a workspace dir that look like
|
|
4290
|
+
# git repos. Prints one path per line.
|
|
4291
|
+
# Usage: _workspace_repo_dirs "/abs/.revo/workspaces/foo"
|
|
4292
|
+
_workspace_repo_dirs() {
|
|
4293
|
+
local ws_dir="$1"
|
|
4294
|
+
local d
|
|
4295
|
+
for d in "$ws_dir"/*/; do
|
|
4296
|
+
[[ -d "$d" ]] || continue
|
|
4297
|
+
d="${d%/}"
|
|
4298
|
+
if [[ -d "$d/.git" ]] || [[ -f "$d/.git" ]]; then
|
|
4299
|
+
printf '%s\n' "$d"
|
|
4300
|
+
fi
|
|
4301
|
+
done
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
# Returns 0 if the workspace has any repo with unpushed commits or
|
|
4305
|
+
# uncommitted changes. Used by --delete to require --force.
|
|
4306
|
+
_workspace_has_local_work() {
|
|
4307
|
+
local ws_dir="$1"
|
|
4308
|
+
local d
|
|
4309
|
+
while IFS= read -r d; do
|
|
4310
|
+
[[ -z "$d" ]] && continue
|
|
4311
|
+
# Dirty working tree?
|
|
4312
|
+
if [[ -n "$(git -C "$d" status --porcelain 2>/dev/null)" ]]; then
|
|
4313
|
+
return 0
|
|
4314
|
+
fi
|
|
4315
|
+
# Any commits not present on any remote ref?
|
|
4316
|
+
local unpushed
|
|
4317
|
+
unpushed=$(git -C "$d" log --oneline --not --remotes 2>/dev/null | head -n 1)
|
|
4318
|
+
if [[ -n "$unpushed" ]]; then
|
|
4319
|
+
return 0
|
|
4320
|
+
fi
|
|
4321
|
+
done <<< "$(_workspace_repo_dirs "$ws_dir")"
|
|
4322
|
+
return 1
|
|
4323
|
+
}
|
|
4324
|
+
|
|
4325
|
+
# Returns 0 if every repo in the workspace has its current branch merged
|
|
4326
|
+
# into the workspace default branch (local or origin/<default>).
|
|
4327
|
+
# Workspaces with no repo dirs are not considered merged.
|
|
4328
|
+
_workspace_all_merged() {
|
|
4329
|
+
local ws_dir="$1"
|
|
4330
|
+
local default_branch="$2"
|
|
4331
|
+
local any=0
|
|
4332
|
+
local d
|
|
4333
|
+
while IFS= read -r d; do
|
|
4334
|
+
[[ -z "$d" ]] && continue
|
|
4335
|
+
any=1
|
|
4336
|
+
# Try local default branch first, then origin/<default>
|
|
4337
|
+
if git -C "$d" merge-base --is-ancestor HEAD "$default_branch" 2>/dev/null; then
|
|
4338
|
+
continue
|
|
4339
|
+
fi
|
|
4340
|
+
if git -C "$d" merge-base --is-ancestor HEAD "origin/$default_branch" 2>/dev/null; then
|
|
4341
|
+
continue
|
|
4342
|
+
fi
|
|
4343
|
+
return 1
|
|
4344
|
+
done <<< "$(_workspace_repo_dirs "$ws_dir")"
|
|
4345
|
+
[[ $any -eq 1 ]]
|
|
4346
|
+
}
|
|
4347
|
+
|
|
4348
|
+
# Generate a workspace-specific CLAUDE.md inside the workspace dir. Kept
|
|
4349
|
+
# intentionally short — the root workspace CLAUDE.md is the canonical
|
|
4350
|
+
# reference; this one just orients the agent inside the isolated copy.
|
|
4351
|
+
_workspace_write_claude_md() {
|
|
4352
|
+
local name="$1"
|
|
4353
|
+
local ws_dir="$2"
|
|
4354
|
+
local branch="$3"
|
|
4355
|
+
local target="$ws_dir/CLAUDE.md"
|
|
4356
|
+
local timestamp
|
|
4357
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
4358
|
+
local parent_name
|
|
4359
|
+
parent_name=$(config_workspace_name)
|
|
4360
|
+
|
|
4361
|
+
{
|
|
4362
|
+
printf '# revo workspace: %s\n' "$name"
|
|
4363
|
+
printf '\n'
|
|
4364
|
+
printf '> WARNING: This workspace contains .env files and other secrets\n'
|
|
4365
|
+
printf '> hardlinked from the source repos. Never commit the .revo/\n'
|
|
4366
|
+
printf '> directory.\n'
|
|
4367
|
+
printf '\n'
|
|
4368
|
+
printf -- '- **Branch:** %s\n' "$branch"
|
|
4369
|
+
printf -- '- **Created:** %s\n' "$timestamp"
|
|
4370
|
+
if [[ -n "$parent_name" ]]; then
|
|
4371
|
+
printf -- '- **Parent workspace:** %s\n' "$parent_name"
|
|
4372
|
+
fi
|
|
4373
|
+
printf -- '- **Source:** ../../repos/\n'
|
|
4374
|
+
printf '\n'
|
|
4375
|
+
printf 'This is an isolated full-copy workspace. All edits, commits,\n'
|
|
4376
|
+
printf 'and pushes happen here — the source tree under `repos/` is\n'
|
|
4377
|
+
printf 'untouched. When you run `revo` from inside this directory it\n'
|
|
4378
|
+
printf 'automatically operates on the repos under this workspace, not\n'
|
|
4379
|
+
printf 'the source ones.\n'
|
|
4380
|
+
printf '\n'
|
|
4381
|
+
printf '## Repos\n'
|
|
4382
|
+
printf '\n'
|
|
4383
|
+
|
|
4384
|
+
local d
|
|
4385
|
+
while IFS= read -r d; do
|
|
4386
|
+
[[ -z "$d" ]] && continue
|
|
4387
|
+
local rname rbranch
|
|
4388
|
+
rname=$(basename "$d")
|
|
4389
|
+
rbranch=$(git -C "$d" rev-parse --abbrev-ref HEAD 2>/dev/null || printf '?')
|
|
4390
|
+
printf '### %s\n' "$rname"
|
|
4391
|
+
printf -- '- **Path:** ./%s\n' "$rname"
|
|
4392
|
+
printf -- '- **Branch:** %s\n' "$rbranch"
|
|
4393
|
+
|
|
4394
|
+
scan_repo "$d"
|
|
4395
|
+
if [[ -n "$SCAN_LANG" ]]; then
|
|
4396
|
+
if [[ -n "$SCAN_NAME" ]]; then
|
|
4397
|
+
printf -- '- **Package:** %s (%s)\n' "$SCAN_NAME" "$SCAN_LANG"
|
|
4398
|
+
else
|
|
4399
|
+
printf -- '- **Language:** %s\n' "$SCAN_LANG"
|
|
4400
|
+
fi
|
|
4401
|
+
fi
|
|
4402
|
+
if [[ -n "$SCAN_FRAMEWORK" ]]; then
|
|
4403
|
+
printf -- '- **Framework:** %s\n' "$SCAN_FRAMEWORK"
|
|
4404
|
+
fi
|
|
4405
|
+
if [[ -n "$SCAN_ROUTES" ]]; then
|
|
4406
|
+
printf -- '- **API routes:** %s\n' "$SCAN_ROUTES"
|
|
4407
|
+
fi
|
|
4408
|
+
printf '\n'
|
|
4409
|
+
done <<< "$(_workspace_repo_dirs "$ws_dir")"
|
|
4410
|
+
|
|
4411
|
+
printf '## Workflow\n'
|
|
4412
|
+
printf '\n'
|
|
4413
|
+
printf '1. Edit code across the repos above\n'
|
|
4414
|
+
printf '2. `revo commit "msg"` to commit dirty repos in one shot\n'
|
|
4415
|
+
printf '3. `revo push` to push branches\n'
|
|
4416
|
+
printf '4. `revo pr "title"` to open coordinated PRs\n'
|
|
4417
|
+
printf '5. After merge, `cd ../../.. && revo workspace %s --delete`\n' "$name"
|
|
4418
|
+
printf '\n'
|
|
4419
|
+
printf '## Workspace Tool: revo\n'
|
|
4420
|
+
printf '\n'
|
|
4421
|
+
printf 'See ../../CLAUDE.md (the parent workspace context) for the full\n'
|
|
4422
|
+
printf 'revo command reference, dependency order, and per-repo details.\n'
|
|
4423
|
+
} > "$target"
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
# --- create ---
|
|
4427
|
+
|
|
4428
|
+
_workspace_create() {
|
|
4429
|
+
local name="$1"
|
|
4430
|
+
local tag="$2"
|
|
4431
|
+
local force="$3"
|
|
4432
|
+
|
|
4433
|
+
local sanitized
|
|
4434
|
+
sanitized=$(_workspace_sanitize_name "$name")
|
|
4435
|
+
if [[ -z "$sanitized" ]]; then
|
|
4436
|
+
ui_step_error "Invalid workspace name: $name"
|
|
4437
|
+
return 1
|
|
4438
|
+
fi
|
|
4439
|
+
|
|
4440
|
+
if [[ "$sanitized" != "$name" ]]; then
|
|
4441
|
+
ui_info "$(ui_dim "Sanitized name: $name -> $sanitized")"
|
|
4442
|
+
fi
|
|
4443
|
+
name="$sanitized"
|
|
4444
|
+
|
|
4445
|
+
local branch="feature/$name"
|
|
4446
|
+
local ws_root="$REVO_WORKSPACE_ROOT/.revo/workspaces"
|
|
4447
|
+
local ws_dir="$ws_root/$name"
|
|
4448
|
+
|
|
4449
|
+
ui_intro "Revo - Create Workspace: $name"
|
|
4450
|
+
|
|
4451
|
+
if [[ -d "$ws_dir" ]]; then
|
|
4452
|
+
ui_step_error "Workspace already exists: .revo/workspaces/$name"
|
|
4453
|
+
ui_info "$(ui_dim "Use 'revo workspace $name --delete' to remove it first")"
|
|
4454
|
+
ui_outro_cancel "Aborted"
|
|
4455
|
+
return 1
|
|
4456
|
+
fi
|
|
4457
|
+
|
|
4458
|
+
if ! _workspace_verify_gitignore_safe; then
|
|
4459
|
+
ui_step_error ".revo/ is not in .gitignore at the workspace root"
|
|
4460
|
+
ui_info "$(ui_dim "Workspaces hardlink-copy .env files and secrets — committing")"
|
|
4461
|
+
ui_info "$(ui_dim ".revo/ would leak them. Add '.revo/' to .gitignore (or run 'revo init').")"
|
|
4462
|
+
if [[ $force -ne 1 ]]; then
|
|
4463
|
+
ui_info "$(ui_dim "Re-run with --force to override.")"
|
|
4464
|
+
ui_outro_cancel "Aborted for safety"
|
|
4465
|
+
return 1
|
|
4466
|
+
fi
|
|
4467
|
+
ui_step_error "Continuing anyway because --force was passed"
|
|
4468
|
+
fi
|
|
4469
|
+
|
|
4470
|
+
local source_repos_dir
|
|
4471
|
+
source_repos_dir=$(config_source_repos_dir)
|
|
4472
|
+
|
|
4473
|
+
local repos
|
|
4474
|
+
repos=$(config_get_repos "$tag")
|
|
4475
|
+
|
|
4476
|
+
if [[ -z "$repos" ]]; then
|
|
4477
|
+
if [[ -n "$tag" ]]; then
|
|
4478
|
+
ui_step_error "No repositories match tag: $tag"
|
|
4479
|
+
else
|
|
4480
|
+
ui_step_error "No repositories configured"
|
|
4481
|
+
fi
|
|
4482
|
+
ui_outro_cancel "Nothing to copy"
|
|
4483
|
+
return 1
|
|
4484
|
+
fi
|
|
4485
|
+
|
|
4486
|
+
mkdir -p "$ws_dir"
|
|
4487
|
+
|
|
4488
|
+
local success_count=0
|
|
4489
|
+
local skip_count=0
|
|
4490
|
+
local fail_count=0
|
|
4491
|
+
|
|
4492
|
+
while IFS= read -r repo; do
|
|
4493
|
+
[[ -z "$repo" ]] && continue
|
|
4494
|
+
|
|
4495
|
+
local path
|
|
4496
|
+
path=$(yaml_get_path "$repo")
|
|
4497
|
+
local src="$source_repos_dir/$path"
|
|
4498
|
+
local dest="$ws_dir/$path"
|
|
4499
|
+
|
|
4500
|
+
if [[ ! -d "$src" ]]; then
|
|
4501
|
+
ui_step_done "Skipped (not cloned):" "$path"
|
|
4502
|
+
skip_count=$((skip_count + 1))
|
|
4503
|
+
continue
|
|
4504
|
+
fi
|
|
4505
|
+
|
|
4506
|
+
ui_spinner_start "Copying $path..."
|
|
4507
|
+
if _workspace_copy_repo "$src" "$dest"; then
|
|
4508
|
+
ui_spinner_stop
|
|
4509
|
+
ui_step_done "Copied:" "$path"
|
|
4510
|
+
else
|
|
4511
|
+
ui_spinner_error "Failed to copy: $path"
|
|
4512
|
+
fail_count=$((fail_count + 1))
|
|
4513
|
+
continue
|
|
4514
|
+
fi
|
|
4515
|
+
|
|
4516
|
+
# Check out the workspace branch in the copy
|
|
4517
|
+
if git -C "$dest" rev-parse --verify "$branch" >/dev/null 2>&1; then
|
|
4518
|
+
if git -C "$dest" checkout "$branch" >/dev/null 2>&1; then
|
|
4519
|
+
ui_step_done "Checked out existing:" "$path -> $branch"
|
|
4520
|
+
else
|
|
4521
|
+
ui_step_error "Failed to checkout existing branch in: $path"
|
|
4522
|
+
fail_count=$((fail_count + 1))
|
|
4523
|
+
continue
|
|
4524
|
+
fi
|
|
4525
|
+
else
|
|
4526
|
+
if git -C "$dest" checkout -b "$branch" >/dev/null 2>&1; then
|
|
4527
|
+
ui_step_done "Branched:" "$path -> $branch"
|
|
4528
|
+
else
|
|
4529
|
+
ui_step_error "Failed to create branch in: $path"
|
|
4530
|
+
fail_count=$((fail_count + 1))
|
|
4531
|
+
continue
|
|
4532
|
+
fi
|
|
4533
|
+
fi
|
|
4534
|
+
|
|
4535
|
+
success_count=$((success_count + 1))
|
|
4536
|
+
done <<< "$repos"
|
|
4537
|
+
|
|
4538
|
+
if [[ $success_count -eq 0 ]]; then
|
|
4539
|
+
ui_outro_cancel "No repos copied; workspace cleanup needed"
|
|
4540
|
+
rm -rf "$ws_dir" 2>/dev/null
|
|
4541
|
+
return 1
|
|
4542
|
+
fi
|
|
4543
|
+
|
|
4544
|
+
# Workspace-level CLAUDE.md
|
|
4545
|
+
_workspace_write_claude_md "$name" "$ws_dir" "$branch"
|
|
4546
|
+
ui_step_done "Wrote:" "$ws_dir/CLAUDE.md"
|
|
4547
|
+
|
|
4548
|
+
ui_bar_line
|
|
4549
|
+
|
|
4550
|
+
if [[ $fail_count -gt 0 ]]; then
|
|
4551
|
+
ui_outro_cancel "Created with errors: $success_count ok, $fail_count failed"
|
|
4552
|
+
return 1
|
|
4553
|
+
fi
|
|
4554
|
+
|
|
4555
|
+
local msg="Workspace ready at .revo/workspaces/$name/"
|
|
4556
|
+
ui_info "$(ui_dim "Branch: $branch • Repos: $success_count")"
|
|
4557
|
+
if [[ $skip_count -gt 0 ]]; then
|
|
4558
|
+
ui_info "$(ui_dim "$skip_count repo(s) skipped (not cloned in source)")"
|
|
4559
|
+
fi
|
|
4560
|
+
ui_info "$(ui_dim "cd .revo/workspaces/$name and run revo from there.")"
|
|
4561
|
+
ui_outro "$msg"
|
|
4562
|
+
return 0
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4565
|
+
# --- delete ---
|
|
4566
|
+
|
|
4567
|
+
_workspace_delete() {
|
|
4568
|
+
local name="$1"
|
|
4569
|
+
local force="$2"
|
|
4570
|
+
|
|
4571
|
+
local sanitized
|
|
4572
|
+
sanitized=$(_workspace_sanitize_name "$name")
|
|
4573
|
+
name="$sanitized"
|
|
4574
|
+
|
|
4575
|
+
local ws_dir="$REVO_WORKSPACE_ROOT/.revo/workspaces/$name"
|
|
4576
|
+
|
|
4577
|
+
ui_intro "Revo - Delete Workspace: $name"
|
|
4578
|
+
|
|
4579
|
+
if [[ ! -d "$ws_dir" ]]; then
|
|
4580
|
+
ui_step_error "No such workspace: $name"
|
|
4581
|
+
ui_outro_cancel "Nothing to delete"
|
|
4582
|
+
return 1
|
|
4583
|
+
fi
|
|
4584
|
+
|
|
4585
|
+
if [[ $force -ne 1 ]] && _workspace_has_local_work "$ws_dir"; then
|
|
4586
|
+
ui_step_error "Workspace has unpushed commits or uncommitted changes"
|
|
4587
|
+
ui_info "$(ui_dim "Push or stash your work first, or re-run with --force to discard it.")"
|
|
4588
|
+
ui_outro_cancel "Aborted"
|
|
4589
|
+
return 1
|
|
4590
|
+
fi
|
|
4591
|
+
|
|
4592
|
+
rm -rf "$ws_dir"
|
|
4593
|
+
|
|
4594
|
+
ui_step_done "Deleted:" ".revo/workspaces/$name"
|
|
4595
|
+
ui_outro "Workspace removed"
|
|
4596
|
+
return 0
|
|
4597
|
+
}
|
|
4598
|
+
|
|
4599
|
+
# --- clean ---
|
|
4600
|
+
|
|
4601
|
+
_workspace_clean() {
|
|
4602
|
+
local ws_root="$REVO_WORKSPACE_ROOT/.revo/workspaces"
|
|
4603
|
+
|
|
4604
|
+
ui_intro "Revo - Clean Merged Workspaces"
|
|
4605
|
+
|
|
4606
|
+
if [[ ! -d "$ws_root" ]]; then
|
|
4607
|
+
ui_info "No workspaces"
|
|
4608
|
+
ui_outro "Nothing to clean"
|
|
4609
|
+
return 0
|
|
4610
|
+
fi
|
|
4611
|
+
|
|
4612
|
+
local default_branch
|
|
4613
|
+
default_branch=$(config_default_branch)
|
|
4614
|
+
[[ -z "$default_branch" ]] && default_branch="main"
|
|
4615
|
+
|
|
4616
|
+
local cleaned=0
|
|
4617
|
+
local kept=0
|
|
4618
|
+
local d
|
|
4619
|
+
for d in "$ws_root"/*/; do
|
|
4620
|
+
[[ -d "$d" ]] || continue
|
|
4621
|
+
d="${d%/}"
|
|
4622
|
+
local name
|
|
4623
|
+
name=$(basename "$d")
|
|
4624
|
+
|
|
4625
|
+
if _workspace_all_merged "$d" "$default_branch"; then
|
|
4626
|
+
rm -rf "$d"
|
|
4627
|
+
ui_step_done "Removed (merged):" "$name"
|
|
4628
|
+
cleaned=$((cleaned + 1))
|
|
4629
|
+
else
|
|
4630
|
+
ui_step_done "Kept:" "$name"
|
|
4631
|
+
kept=$((kept + 1))
|
|
4632
|
+
fi
|
|
4633
|
+
done
|
|
4634
|
+
|
|
4635
|
+
ui_bar_line
|
|
4636
|
+
ui_outro "Cleaned $cleaned, kept $kept"
|
|
4637
|
+
return 0
|
|
4638
|
+
}
|
|
4639
|
+
|
|
4640
|
+
# --- list (revo workspaces) ---
|
|
4641
|
+
|
|
4642
|
+
cmd_workspaces() {
|
|
4643
|
+
while [[ $# -gt 0 ]]; do
|
|
4644
|
+
case "$1" in
|
|
4645
|
+
--help|-h)
|
|
4646
|
+
printf 'Usage: revo workspaces\n\n'
|
|
4647
|
+
printf 'List all active workspaces with branch, age, and dirty state.\n'
|
|
4648
|
+
return 0
|
|
4649
|
+
;;
|
|
4650
|
+
*)
|
|
4651
|
+
ui_step_error "Unknown option: $1"
|
|
4652
|
+
return 1
|
|
4653
|
+
;;
|
|
4654
|
+
esac
|
|
4655
|
+
done
|
|
4656
|
+
|
|
4657
|
+
config_require_workspace || return 1
|
|
4658
|
+
|
|
4659
|
+
ui_intro "Revo - Workspaces"
|
|
4660
|
+
|
|
4661
|
+
local ws_root="$REVO_WORKSPACE_ROOT/.revo/workspaces"
|
|
4662
|
+
if [[ ! -d "$ws_root" ]]; then
|
|
4663
|
+
ui_info "No workspaces"
|
|
4664
|
+
ui_bar_line
|
|
4665
|
+
ui_info "$(ui_dim "Create one with: revo workspace <name>")"
|
|
4666
|
+
ui_outro "Done"
|
|
4667
|
+
return 0
|
|
4668
|
+
fi
|
|
4669
|
+
|
|
4670
|
+
# Count first so we can short-circuit empty
|
|
4671
|
+
local total=0
|
|
4672
|
+
local d
|
|
4673
|
+
for d in "$ws_root"/*/; do
|
|
4674
|
+
[[ -d "$d" ]] || continue
|
|
4675
|
+
total=$((total + 1))
|
|
4676
|
+
done
|
|
4677
|
+
|
|
4678
|
+
if [[ $total -eq 0 ]]; then
|
|
4679
|
+
ui_info "No workspaces"
|
|
4680
|
+
ui_bar_line
|
|
4681
|
+
ui_info "$(ui_dim "Create one with: revo workspace <name>")"
|
|
4682
|
+
ui_outro "Done"
|
|
4683
|
+
return 0
|
|
4684
|
+
fi
|
|
4685
|
+
|
|
4686
|
+
ui_table_widths 24 24 8 7 12
|
|
4687
|
+
ui_table_header "Workspace" "Branch" "Age" "Repos" "Dirty"
|
|
4688
|
+
|
|
4689
|
+
local now
|
|
4690
|
+
now=$(date +%s)
|
|
4691
|
+
|
|
4692
|
+
for d in "$ws_root"/*/; do
|
|
4693
|
+
[[ -d "$d" ]] || continue
|
|
4694
|
+
d="${d%/}"
|
|
4695
|
+
local name
|
|
4696
|
+
name=$(basename "$d")
|
|
4697
|
+
|
|
4698
|
+
local mtime
|
|
4699
|
+
mtime=$(_workspace_mtime "$d")
|
|
4700
|
+
local age_secs=$((now - mtime))
|
|
4701
|
+
local age
|
|
4702
|
+
age=$(_workspace_format_age "$age_secs")
|
|
4703
|
+
|
|
4704
|
+
local repos_in_ws=0
|
|
4705
|
+
local dirty_count=0
|
|
4706
|
+
local branch=""
|
|
4707
|
+
local repo
|
|
4708
|
+
while IFS= read -r repo; do
|
|
4709
|
+
[[ -z "$repo" ]] && continue
|
|
4710
|
+
repos_in_ws=$((repos_in_ws + 1))
|
|
4711
|
+
if [[ -z "$branch" ]]; then
|
|
4712
|
+
branch=$(git -C "$repo" rev-parse --abbrev-ref HEAD 2>/dev/null || printf '?')
|
|
4713
|
+
fi
|
|
4714
|
+
if [[ -n "$(git -C "$repo" status --porcelain 2>/dev/null)" ]]; then
|
|
4715
|
+
dirty_count=$((dirty_count + 1))
|
|
4716
|
+
fi
|
|
4717
|
+
done <<< "$(_workspace_repo_dirs "$d")"
|
|
4718
|
+
|
|
4719
|
+
[[ -z "$branch" ]] && branch="$(ui_dim "-")"
|
|
4720
|
+
|
|
4721
|
+
local dirty_text
|
|
4722
|
+
if [[ $dirty_count -gt 0 ]]; then
|
|
4723
|
+
dirty_text="$(ui_yellow "$dirty_count dirty")"
|
|
4724
|
+
else
|
|
4725
|
+
dirty_text="$(ui_green "clean")"
|
|
4726
|
+
fi
|
|
4727
|
+
|
|
4728
|
+
ui_table_row "$name" "$branch" "$age" "$repos_in_ws" "$dirty_text"
|
|
4729
|
+
done
|
|
4730
|
+
|
|
4731
|
+
ui_bar_line
|
|
4732
|
+
ui_outro "$total workspace(s)"
|
|
4733
|
+
return 0
|
|
4734
|
+
}
|
|
4735
|
+
|
|
4736
|
+
# --- entry point: revo workspace ---
|
|
4737
|
+
|
|
4738
|
+
cmd_workspace() {
|
|
4739
|
+
local name=""
|
|
4740
|
+
local tag=""
|
|
4741
|
+
local action="create"
|
|
4742
|
+
local force=0
|
|
4743
|
+
|
|
4744
|
+
while [[ $# -gt 0 ]]; do
|
|
4745
|
+
case "$1" in
|
|
4746
|
+
--tag)
|
|
4747
|
+
tag="$2"
|
|
4748
|
+
shift 2
|
|
4749
|
+
;;
|
|
4750
|
+
--delete)
|
|
4751
|
+
action="delete"
|
|
4752
|
+
shift
|
|
4753
|
+
;;
|
|
4754
|
+
--clean)
|
|
4755
|
+
action="clean"
|
|
4756
|
+
shift
|
|
4757
|
+
;;
|
|
4758
|
+
--force|-f)
|
|
4759
|
+
force=1
|
|
4760
|
+
shift
|
|
4761
|
+
;;
|
|
4762
|
+
--help|-h)
|
|
4763
|
+
cat << 'EOF'
|
|
4764
|
+
Usage:
|
|
4765
|
+
revo workspace <name> [--tag TAG] Create a workspace (full copy of repos)
|
|
4766
|
+
revo workspace <name> --delete [--force]
|
|
4767
|
+
Delete a workspace
|
|
4768
|
+
revo workspace --clean Remove workspaces whose branches are merged
|
|
4769
|
+
revo workspaces List active workspaces
|
|
4770
|
+
|
|
4771
|
+
Workspaces are full copies of all repos (or a tagged subset) under
|
|
4772
|
+
.revo/workspaces/<name>/. Each workspace gets its own feature/<name>
|
|
4773
|
+
branch. Hardlinks are used where possible so the cost is near-zero.
|
|
4774
|
+
|
|
4775
|
+
Run revo from inside .revo/workspaces/<name>/ and it automatically
|
|
4776
|
+
operates on the workspace's repos rather than the source tree.
|
|
4777
|
+
|
|
4778
|
+
EOF
|
|
4779
|
+
return 0
|
|
4780
|
+
;;
|
|
4781
|
+
-*)
|
|
4782
|
+
ui_step_error "Unknown option: $1"
|
|
4783
|
+
return 1
|
|
4784
|
+
;;
|
|
4785
|
+
*)
|
|
4786
|
+
if [[ -z "$name" ]]; then
|
|
4787
|
+
name="$1"
|
|
4788
|
+
else
|
|
4789
|
+
ui_step_error "Unexpected argument: $1"
|
|
4790
|
+
return 1
|
|
4791
|
+
fi
|
|
4792
|
+
shift
|
|
4793
|
+
;;
|
|
4794
|
+
esac
|
|
4795
|
+
done
|
|
4796
|
+
|
|
4797
|
+
config_require_workspace || return 1
|
|
4798
|
+
|
|
4799
|
+
case "$action" in
|
|
4800
|
+
clean)
|
|
4801
|
+
_workspace_clean
|
|
4802
|
+
;;
|
|
4803
|
+
delete)
|
|
4804
|
+
if [[ -z "$name" ]]; then
|
|
4805
|
+
ui_step_error "Usage: revo workspace <name> --delete [--force]"
|
|
4806
|
+
return 1
|
|
4807
|
+
fi
|
|
4808
|
+
_workspace_delete "$name" "$force"
|
|
4809
|
+
;;
|
|
4810
|
+
create)
|
|
4811
|
+
if [[ -z "$name" ]]; then
|
|
4812
|
+
ui_step_error "Usage: revo workspace <name> [--tag TAG]"
|
|
4813
|
+
return 1
|
|
4814
|
+
fi
|
|
4815
|
+
_workspace_create "$name" "$tag" "$force"
|
|
4816
|
+
;;
|
|
4817
|
+
esac
|
|
4818
|
+
}
|
|
4819
|
+
|
|
4820
|
+
# === Main ===
|
|
4821
|
+
# --- Help ---
|
|
4822
|
+
show_help() {
|
|
4823
|
+
cat << EOF
|
|
4824
|
+
Revo - Claude-first multi-repo workspace manager v$REVO_VERSION
|
|
4825
|
+
|
|
4826
|
+
Usage: revo <command> [options]
|
|
4827
|
+
|
|
4828
|
+
Workspace commands:
|
|
4829
|
+
init Initialize a new workspace (auto-detects existing repos)
|
|
4830
|
+
detect Bootstrap revo around existing git repos in cwd
|
|
4831
|
+
add URL [--tags TAGS] Add a repository to the workspace
|
|
4832
|
+
clone [--tag TAG] Clone configured repositories
|
|
4833
|
+
list [--tag TAG] List configured repositories
|
|
4834
|
+
status [--tag TAG] Show status of all repositories
|
|
4835
|
+
sync [--tag TAG] Pull latest changes on repositories
|
|
4836
|
+
branch NAME [--tag TAG] Create a branch on repositories
|
|
4837
|
+
checkout BRANCH [--tag TAG] Checkout a branch on repositories
|
|
4838
|
+
exec "CMD" [--tag TAG] Run a command in each repository
|
|
4839
|
+
|
|
4840
|
+
Claude-first commands:
|
|
4841
|
+
context Scan repos and regenerate workspace CLAUDE.md
|
|
4842
|
+
feature NAME [--tag TAG] Create feature branch + context file across repos
|
|
4843
|
+
commit MSG [--tag TAG] Commit changes across dirty repos
|
|
4844
|
+
push [--tag TAG] Push branches across repositories
|
|
4845
|
+
pr TITLE [--tag TAG] Create coordinated PRs via gh CLI
|
|
4846
|
+
issue list [options] List issues across workspace repos
|
|
4847
|
+
issue create [options] TITLE Create issue(s) in workspace repos
|
|
4848
|
+
workspace NAME [--tag TAG] Create a full-copy workspace under .revo/workspaces/
|
|
4849
|
+
workspace NAME --delete Delete a workspace (use --force to discard work)
|
|
4850
|
+
workspace --clean Remove workspaces whose branches are merged
|
|
4851
|
+
workspaces List active workspaces
|
|
4852
|
+
|
|
4853
|
+
Options:
|
|
4854
|
+
--tag TAG Filter repositories by tag
|
|
4855
|
+
--tags TAGS Comma-separated list of tags (for add)
|
|
4856
|
+
--force, -f Force operation (clone, checkout)
|
|
4857
|
+
--rebase, -r Use rebase when syncing
|
|
4858
|
+
--quiet, -q Suppress command output (exec)
|
|
4859
|
+
--body TEXT PR body (pr)
|
|
4860
|
+
--help, -h Show this help
|
|
4861
|
+
--version, -v Show version
|
|
4862
|
+
|
|
4863
|
+
Examples:
|
|
4864
|
+
revo init
|
|
4865
|
+
revo add git@github.com:org/shared-types.git --tags shared
|
|
4866
|
+
revo add git@github.com:org/backend.git --tags backend,api
|
|
4867
|
+
revo clone
|
|
4868
|
+
revo context # regenerate CLAUDE.md
|
|
4869
|
+
revo feature clock-student --tag backend # coordinated branch
|
|
4870
|
+
revo commit "wire up clock endpoint"
|
|
4871
|
+
revo push
|
|
4872
|
+
revo pr "Clock endpoint for students" --tag backend
|
|
4873
|
+
revo issue list --state open # all open issues
|
|
4874
|
+
revo issue list --tag backend --json # JSON for Claude/jq
|
|
4875
|
+
revo issue create --repo backend "Add stats endpoint"
|
|
4876
|
+
revo issue create --tag mobile --feature stats "Add statistics screen"
|
|
4877
|
+
revo workspace clock-student # full copy of all repos
|
|
4878
|
+
revo workspace clock-student --tag backend # only backend repos
|
|
4879
|
+
revo workspaces # list active workspaces
|
|
4880
|
+
revo workspace clock-student --delete # remove when done
|
|
4881
|
+
|
|
4882
|
+
Documentation: https://github.com/jippylong12/revo
|
|
4883
|
+
EOF
|
|
4884
|
+
}
|
|
4885
|
+
|
|
4886
|
+
show_version() {
|
|
4887
|
+
printf 'Revo v%s\n' "$REVO_VERSION"
|
|
4888
|
+
}
|
|
4889
|
+
|
|
4890
|
+
# --- Main ---
|
|
4891
|
+
main() {
|
|
4892
|
+
local command="${1:-}"
|
|
4893
|
+
|
|
4894
|
+
# Handle no arguments
|
|
4895
|
+
if [[ -z "$command" ]]; then
|
|
4896
|
+
show_help
|
|
4897
|
+
return 0
|
|
4898
|
+
fi
|
|
4899
|
+
|
|
4900
|
+
# Shift off the command
|
|
4901
|
+
shift || true
|
|
4902
|
+
|
|
4903
|
+
case "$command" in
|
|
4904
|
+
init)
|
|
4905
|
+
cmd_init "$@"
|
|
4906
|
+
;;
|
|
4907
|
+
detect)
|
|
4908
|
+
cmd_detect "$@"
|
|
4909
|
+
;;
|
|
4910
|
+
clone)
|
|
4911
|
+
cmd_clone "$@"
|
|
4912
|
+
;;
|
|
4913
|
+
status)
|
|
4914
|
+
cmd_status "$@"
|
|
4915
|
+
;;
|
|
4916
|
+
branch)
|
|
4917
|
+
cmd_branch "$@"
|
|
4918
|
+
;;
|
|
4919
|
+
checkout)
|
|
4920
|
+
cmd_checkout "$@"
|
|
4921
|
+
;;
|
|
4922
|
+
sync)
|
|
4923
|
+
cmd_sync "$@"
|
|
4924
|
+
;;
|
|
4925
|
+
exec)
|
|
4926
|
+
cmd_exec "$@"
|
|
4927
|
+
;;
|
|
4928
|
+
add)
|
|
4929
|
+
cmd_add "$@"
|
|
4930
|
+
;;
|
|
4931
|
+
list)
|
|
4932
|
+
cmd_list "$@"
|
|
4933
|
+
;;
|
|
4934
|
+
context)
|
|
4935
|
+
cmd_context "$@"
|
|
4936
|
+
;;
|
|
4937
|
+
feature)
|
|
4938
|
+
cmd_feature "$@"
|
|
4939
|
+
;;
|
|
4940
|
+
commit)
|
|
4941
|
+
cmd_commit "$@"
|
|
4942
|
+
;;
|
|
4943
|
+
push)
|
|
4944
|
+
cmd_push "$@"
|
|
4945
|
+
;;
|
|
4946
|
+
pr)
|
|
4947
|
+
cmd_pr "$@"
|
|
4948
|
+
;;
|
|
4949
|
+
issue|issues)
|
|
4950
|
+
cmd_issue "$@"
|
|
4951
|
+
;;
|
|
4952
|
+
workspace)
|
|
4953
|
+
cmd_workspace "$@"
|
|
4954
|
+
;;
|
|
4955
|
+
workspaces)
|
|
4956
|
+
cmd_workspaces "$@"
|
|
3775
4957
|
;;
|
|
3776
4958
|
--help|-h|help)
|
|
3777
4959
|
show_help
|