@revotools/cli 0.2.2 → 0.4.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 (3) hide show
  1. package/README.md +41 -5
  2. package/dist/revo +552 -25
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,8 +8,13 @@ Revo is a fork of [Mars](https://github.com/dean0x/mars). It keeps everything
8
8
  Mars gives you — tag-based filtering, coordinated branches, zero dependencies —
9
9
  and adds features designed for working with coding agents:
10
10
 
11
+ - **`revo init`** auto-detects existing git repos in the current directory and
12
+ bootstraps a workspace around them, no manual `revo add` required.
13
+ - **`revo detect`** does the same on demand for an already-populated folder
14
+ full of clones.
11
15
  - **`revo context`** scans your repos and writes a root-level `CLAUDE.md` so
12
- Claude Code can understand the whole workspace at a glance.
16
+ Claude Code can understand the whole workspace at a glance — including
17
+ active feature briefs and a built-in revo command reference.
13
18
  - **`revo feature <name>`** creates a coordinated branch and shared context
14
19
  file across matching repos.
15
20
  - **`revo commit`**, **`revo push`**, **`revo pr`** let you commit, push, and
@@ -22,6 +27,8 @@ Pure bash 3.2+, works on macOS out of the box, no runtime dependencies beyond
22
27
 
23
28
  ## Quick Start
24
29
 
30
+ ### From an empty directory
31
+
25
32
  ```bash
26
33
  mkdir my-project && cd my-project
27
34
  revo init
@@ -30,10 +37,23 @@ revo add git@github.com:org/shared-types.git --tags shared
30
37
  revo add git@github.com:org/backend.git --tags backend,api --depends-on shared-types
31
38
  revo add git@github.com:org/frontend.git --tags frontend,web --depends-on backend
32
39
 
33
- revo clone # CLAUDE.md is auto-generated after first clone
40
+ revo clone # CLAUDE.md is regenerated after each clone batch
34
41
  revo context # regenerate it any time repos change
35
42
  ```
36
43
 
44
+ ### From a folder you already populated with clones
45
+
46
+ ```bash
47
+ cd ~/code/my-project # already has frontend/, backend/, shared/ as git repos
48
+ revo init # auto-detects existing repos, links them, writes CLAUDE.md
49
+ # — or —
50
+ revo detect # same idea, runs on its own without prompting
51
+ ```
52
+
53
+ `init` and `detect` both link root-level repos into `repos/` via relative
54
+ symlinks so the rest of revo's data model keeps working without moving any
55
+ files around.
56
+
37
57
  Then point Claude Code at the workspace directory and it will read `CLAUDE.md`
38
58
  on its own.
39
59
 
@@ -61,13 +81,14 @@ revo pr "Clock endpoint for students" ── coordinated PRs via gh
61
81
 
62
82
  ## Commands
63
83
 
64
- ### Workspace (from Mars)
84
+ ### Workspace
65
85
 
66
86
  | Command | Description |
67
87
  |---------|-------------|
68
- | `revo init` | Initialize a new workspace |
88
+ | `revo init` | Initialize a new workspace; auto-detects existing git repos in cwd |
89
+ | `revo detect` | Bootstrap revo around git repos already present in cwd |
69
90
  | `revo add <url> [--tags t1,t2] [--depends-on r1,r2]` | Add a repository to config |
70
- | `revo clone [--tag TAG]` | Clone configured repositories |
91
+ | `revo clone [--tag TAG]` | Clone configured repositories (regenerates `CLAUDE.md` after) |
71
92
  | `revo list [--tag TAG]` | List configured repositories |
72
93
  | `revo status [--tag TAG]` | Show status of all repositories |
73
94
  | `revo sync [--tag TAG] [--rebase]` | Pull latest changes |
@@ -85,6 +106,21 @@ revo pr "Clock endpoint for students" ── coordinated PRs via gh
85
106
  | `revo push [--tag TAG]` | Push branches across repos |
86
107
  | `revo pr <title> [--tag TAG] [--body BODY]` | Create coordinated PRs via `gh` |
87
108
 
109
+ ### What `revo context` detects per repo
110
+
111
+ - **Node.js** — Next.js, Nuxt, Remix, SvelteKit, Astro, Vite, NestJS, Fastify,
112
+ Express, Hono, Angular, React, Vue, Svelte, React Native, Expo
113
+ - **Python** — Django, FastAPI, Flask, Starlette
114
+ - **Go** (module name), **Rust** (Cargo), **Java/Kotlin** (Gradle), **Java**
115
+ (Maven), **Ruby** (Rails, Sinatra), **Swift** (Package.swift)
116
+ - API route directories (`src/routes/`, `src/api/`, `app/api/`, `routes/`,
117
+ `pages/api/`, etc.)
118
+ - Whether the repo ships its own `CLAUDE.md`, `Dockerfile`, or
119
+ `docker-compose.yml`
120
+ - The first non-trivial line of `README.md` as a one-line description
121
+ - Any `.revo/features/*.md` briefs at the workspace root, listed as
122
+ `## Active Features`
123
+
88
124
  ## Tag Filtering
89
125
 
90
126
  Target subsets of repos using `--tag`:
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.2.0"
11
+ REVO_VERSION="0.4.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
  }
@@ -1232,6 +1276,10 @@ _scan_node_framework() {
1232
1276
  printf 'SvelteKit'
1233
1277
  elif _scan_pkg_has_dep "$file" "astro"; then
1234
1278
  printf 'Astro'
1279
+ elif _scan_pkg_has_dep "$file" "expo"; then
1280
+ printf 'Expo (React Native)'
1281
+ elif _scan_pkg_has_dep "$file" "react-native"; then
1282
+ printf 'React Native'
1235
1283
  elif _scan_pkg_has_dep "$file" "vite"; then
1236
1284
  printf 'Vite'
1237
1285
  elif _scan_pkg_has_dep "$file" "nestjs" || _scan_pkg_has_dep "$file" "@nestjs/core"; then
@@ -1242,6 +1290,8 @@ _scan_node_framework() {
1242
1290
  printf 'Express'
1243
1291
  elif _scan_pkg_has_dep "$file" "hono"; then
1244
1292
  printf 'Hono'
1293
+ elif _scan_pkg_has_dep "$file" "@angular/core"; then
1294
+ printf 'Angular'
1245
1295
  elif _scan_pkg_has_dep "$file" "react"; then
1246
1296
  printf 'React'
1247
1297
  elif _scan_pkg_has_dep "$file" "vue"; then
@@ -1251,6 +1301,16 @@ _scan_node_framework() {
1251
1301
  fi
1252
1302
  }
1253
1303
 
1304
+ # Detect framework from a Gemfile
1305
+ _scan_ruby_framework() {
1306
+ local file="$1"
1307
+ if grep -q "rails" "$file" 2>/dev/null; then
1308
+ printf 'Rails'
1309
+ elif grep -q "sinatra" "$file" 2>/dev/null; then
1310
+ printf 'Sinatra'
1311
+ fi
1312
+ }
1313
+
1254
1314
  # Detect framework from Python dependency files
1255
1315
  _scan_python_framework() {
1256
1316
  local repo_dir="$1"
@@ -1387,6 +1447,27 @@ scan_repo() {
1387
1447
  SCAN_NAME=$(grep -m1 -E '^[[:space:]]*name[[:space:]]*=' "$repo_dir/Cargo.toml" 2>/dev/null | sed -E 's/.*=[[:space:]]*"([^"]*)".*/\1/' || true)
1388
1448
  fi
1389
1449
 
1450
+ # Java/Kotlin (Gradle)
1451
+ if [[ -z "$SCAN_LANG" ]] && { [[ -f "$repo_dir/build.gradle" ]] || [[ -f "$repo_dir/build.gradle.kts" ]]; }; then
1452
+ SCAN_LANG="Java/Kotlin (Gradle)"
1453
+ fi
1454
+
1455
+ # Java (Maven)
1456
+ if [[ -z "$SCAN_LANG" ]] && [[ -f "$repo_dir/pom.xml" ]]; then
1457
+ SCAN_LANG="Java (Maven)"
1458
+ fi
1459
+
1460
+ # Ruby
1461
+ if [[ -z "$SCAN_LANG" ]] && [[ -f "$repo_dir/Gemfile" ]]; then
1462
+ SCAN_LANG="Ruby"
1463
+ SCAN_FRAMEWORK=$(_scan_ruby_framework "$repo_dir/Gemfile")
1464
+ fi
1465
+
1466
+ # Swift
1467
+ if [[ -z "$SCAN_LANG" ]] && [[ -f "$repo_dir/Package.swift" ]]; then
1468
+ SCAN_LANG="Swift"
1469
+ fi
1470
+
1390
1471
  # Routes
1391
1472
  SCAN_ROUTES=$(_scan_list_routes "$repo_dir")
1392
1473
 
@@ -1412,12 +1493,149 @@ scan_repo() {
1412
1493
 
1413
1494
  # === lib/commands/init.sh ===
1414
1495
  # Revo CLI - init command
1415
- # Interactive workspace initialization
1496
+ # Initializes a workspace and auto-detects existing git repos in the current
1497
+ # directory (and in repos/), so that running `revo init` in a folder you
1498
+ # already populated with clones bootstraps a usable workspace immediately.
1499
+
1500
+ # Suggest tags for an auto-detected repo based on its package contents.
1501
+ # Always returns the repo name as a tag, plus an optional category tag
1502
+ # (frontend/backend) when one can be inferred. Skips the category when it
1503
+ # would be a duplicate of the name.
1504
+ _init_auto_tags() {
1505
+ local dir="$1"
1506
+ local name="$2"
1507
+ local category=""
1508
+
1509
+ if [[ -f "$dir/package.json" ]]; then
1510
+ if grep -qE '"(next|nuxt|react|vue|svelte|@angular/core|astro|@remix-run/react|@sveltejs/kit|expo|react-native)"' "$dir/package.json" 2>/dev/null; then
1511
+ category="frontend"
1512
+ elif grep -qE '"(express|fastify|hono|@nestjs/core|nestjs|koa)"' "$dir/package.json" 2>/dev/null; then
1513
+ category="backend"
1514
+ fi
1515
+ elif [[ -f "$dir/go.mod" ]] || [[ -f "$dir/Cargo.toml" ]] || [[ -f "$dir/pom.xml" ]] || [[ -f "$dir/build.gradle" ]] || [[ -f "$dir/build.gradle.kts" ]]; then
1516
+ category="backend"
1517
+ elif [[ -f "$dir/pyproject.toml" ]] || [[ -f "$dir/requirements.txt" ]]; then
1518
+ category="backend"
1519
+ fi
1520
+
1521
+ if [[ -n "$category" ]] && [[ "$category" != "$name" ]]; then
1522
+ printf '%s, %s' "$name" "$category"
1523
+ else
1524
+ printf '%s' "$name"
1525
+ fi
1526
+ }
1527
+
1528
+ # Scan for git repos in the current directory and in repos/.
1529
+ # Sets _INIT_FOUND_DIRS to a newline-separated list of repo paths.
1530
+ _INIT_FOUND_DIRS=""
1531
+ _init_scan_existing() {
1532
+ _INIT_FOUND_DIRS=""
1533
+
1534
+ local d
1535
+ for d in */; do
1536
+ d="${d%/}"
1537
+ [[ "$d" == "repos" ]] && continue
1538
+ [[ "$d" == ".revo" ]] && continue
1539
+ if [[ -d "$d/.git" ]]; then
1540
+ if [[ -z "$_INIT_FOUND_DIRS" ]]; then
1541
+ _INIT_FOUND_DIRS="$d"
1542
+ else
1543
+ _INIT_FOUND_DIRS="$_INIT_FOUND_DIRS"$'\n'"$d"
1544
+ fi
1545
+ fi
1546
+ done
1547
+
1548
+ if [[ -d "repos" ]]; then
1549
+ for d in repos/*/; do
1550
+ d="${d%/}"
1551
+ if [[ -d "$d/.git" ]]; then
1552
+ if [[ -z "$_INIT_FOUND_DIRS" ]]; then
1553
+ _INIT_FOUND_DIRS="$d"
1554
+ else
1555
+ _INIT_FOUND_DIRS="$_INIT_FOUND_DIRS"$'\n'"$d"
1556
+ fi
1557
+ fi
1558
+ done
1559
+ fi
1560
+ }
1561
+
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.
1565
+ _init_write_claude_md() {
1566
+ local out="$REVO_WORKSPACE_ROOT/CLAUDE.md"
1567
+
1568
+ if [[ -f "$out" ]]; then
1569
+ return 0
1570
+ fi
1571
+
1572
+ cat > "$out" << 'EOF'
1573
+ # Workspace managed by revo
1574
+
1575
+ This is a multi-repo workspace managed by revo.
1576
+ revo is installed and available in the terminal.
1577
+
1578
+ ## Quick reference
1579
+
1580
+ ### If repos are not yet added
1581
+ The user may give you repo URLs or descriptions. Use these commands to set up:
1582
+
1583
+ ```bash
1584
+ revo add <git-url> --tags <tag1,tag2> [--depends-on <repo-name>]
1585
+ revo clone
1586
+ revo context # regenerates this file with full repo details
1587
+ ```
1588
+
1589
+ Example:
1590
+ ```bash
1591
+ revo add git@github.com:org/shared.git --tags shared,types
1592
+ revo add git@github.com:org/backend.git --tags backend,api --depends-on shared
1593
+ revo add git@github.com:org/frontend.git --tags frontend,web --depends-on backend
1594
+ revo clone
1595
+ ```
1596
+
1597
+ ### If repos are already set up
1598
+ Use `revo status` to see all repos, branches, and dirty state.
1599
+
1600
+ ## Available commands
1601
+ - `revo status` — branch and dirty state across all repos
1602
+ - `revo sync` — pull latest across all repos
1603
+ - `revo feature <name>` — create feature branch across all repos
1604
+ - `revo commit "msg"` — commit all dirty repos with same message
1605
+ - `revo push` — push all repos
1606
+ - `revo pr "title"` — create coordinated PRs via gh CLI
1607
+ - `revo exec "cmd" --tag <tag>` — run command in filtered repos
1608
+ - `revo context` — regenerate this file after repos change
1609
+ - `revo add <url> --tags <t> --depends-on <d>` — add a repo
1610
+
1611
+ ### Tag filtering
1612
+ All commands support `--tag <tag>` to target specific repos:
1613
+
1614
+ ```bash
1615
+ revo exec "npm test" --tag frontend
1616
+ revo sync --tag backend
1617
+ revo branch hotfix --tag api
1618
+ ```
1619
+
1620
+ ## Working in this workspace
1621
+ - Repos are in the repos/ subdirectory (or as configured in revo.yaml)
1622
+ - Edit files across repos/ directly — you have full access
1623
+ - When making cross-repo changes, follow the dependency order below
1624
+ - Check .revo/features/ for active feature briefs
1625
+ - Use revo commands instead of manually running git in each repo
1626
+
1627
+ ## Dependency order
1628
+ <!-- revo context will fill this in once repos are cloned -->
1629
+ Run `revo context` after cloning to populate repo details and dependency order.
1630
+ EOF
1631
+
1632
+ ui_step_done "Created CLAUDE.md (Claude reads this automatically)"
1633
+ }
1416
1634
 
1417
1635
  cmd_init() {
1418
1636
  local workspace_name=""
1419
1637
 
1420
- # Check if already initialized
1638
+ # Already initialized? Bail out.
1421
1639
  if [[ -f "revo.yaml" ]] || [[ -f "mars.yaml" ]]; then
1422
1640
  ui_step_error "Workspace already initialized in this directory"
1423
1641
  return 1
@@ -1425,20 +1643,21 @@ cmd_init() {
1425
1643
 
1426
1644
  ui_intro "Revo - Claude-first Multi-Repo Workspace"
1427
1645
 
1428
- # Get workspace name
1646
+ # Default workspace name to current directory basename so init can run
1647
+ # non-interactively. The user can still override by typing a value.
1648
+ local default_name
1649
+ default_name=$(basename "$PWD")
1429
1650
  ui_step "Workspace name?"
1430
1651
  printf '%s ' "$(ui_bar)"
1431
1652
  read -r workspace_name
1432
-
1433
1653
  if [[ -z "$workspace_name" ]]; then
1434
- ui_outro_cancel "Cancelled - workspace name is required"
1435
- return 1
1654
+ workspace_name="$default_name"
1436
1655
  fi
1437
1656
 
1438
1657
  ui_step_done "Workspace:" "$workspace_name"
1439
1658
  ui_bar_line
1440
1659
 
1441
- # Initialize workspace
1660
+ # Initialize the on-disk config.
1442
1661
  if ! config_init "$workspace_name"; then
1443
1662
  ui_step_error "Failed to initialize workspace"
1444
1663
  return 1
@@ -1448,8 +1667,179 @@ cmd_init() {
1448
1667
  ui_step_done "Created .gitignore"
1449
1668
  ui_step_done "Created repos/ directory"
1450
1669
 
1670
+ # Auto-detect existing git repos in the current directory.
1671
+ _init_scan_existing
1672
+ local detected_count=0
1673
+ if [[ -n "$_INIT_FOUND_DIRS" ]]; then
1674
+ local dir name remote tags path
1675
+ while IFS= read -r dir; do
1676
+ [[ -z "$dir" ]] && continue
1677
+ name=$(basename "$dir")
1678
+ remote=$(cd "$dir" && git remote get-url origin 2>/dev/null || true)
1679
+ if [[ -z "$remote" ]]; then
1680
+ ui_step_error "Skipping $name (no git remote)"
1681
+ continue
1682
+ fi
1683
+
1684
+ path="$name"
1685
+
1686
+ # If the repo lives at the workspace root (not under repos/),
1687
+ # link it into repos/ so the rest of revo's data model — which
1688
+ # always resolves $REVO_REPOS_DIR/$path — works without copying
1689
+ # or moving files.
1690
+ if [[ "$dir" != repos/* ]]; then
1691
+ if [[ ! -e "repos/$name" ]]; then
1692
+ ln -s "../$name" "repos/$name"
1693
+ fi
1694
+ fi
1695
+
1696
+ tags=$(_init_auto_tags "$dir" "$name")
1697
+ yaml_add_repo "$remote" "$path" "$tags" ""
1698
+ ui_step_done "Detected:" "$name → $remote"
1699
+ detected_count=$((detected_count + 1))
1700
+ done <<< "$_INIT_FOUND_DIRS"
1701
+
1702
+ if [[ $detected_count -gt 0 ]]; then
1703
+ config_save
1704
+ ui_info "$(ui_dim "Edit revo.yaml to adjust tags or add depends_on relationships")"
1705
+ fi
1706
+ fi
1707
+
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.
1712
+ if [[ $detected_count -gt 0 ]]; then
1713
+ ui_bar_line
1714
+ cmd_context
1715
+ return 0
1716
+ fi
1717
+
1718
+ _init_write_claude_md
1719
+
1451
1720
  ui_outro "Workspace initialized! Run 'revo add <url>' to add repositories."
1721
+ return 0
1722
+ }
1723
+
1724
+ # === lib/commands/detect.sh ===
1725
+ # Revo CLI - detect command
1726
+ # Bootstraps a workspace around git repos that already exist in the current
1727
+ # directory. Use this when you have a parent folder full of clones and want
1728
+ # revo to wrap them without re-cloning.
1452
1729
 
1730
+ cmd_detect() {
1731
+ while [[ $# -gt 0 ]]; do
1732
+ case "$1" in
1733
+ --help|-h)
1734
+ printf 'Usage: revo detect\n\n'
1735
+ printf 'Auto-detect git repositories in the current directory and\n'
1736
+ printf 'bootstrap a revo workspace around them. Generates revo.yaml\n'
1737
+ printf 'and CLAUDE.md from what it finds.\n'
1738
+ return 0
1739
+ ;;
1740
+ *)
1741
+ ui_step_error "Unknown option: $1"
1742
+ return 1
1743
+ ;;
1744
+ esac
1745
+ done
1746
+
1747
+ if [[ -f "revo.yaml" ]] || [[ -f "mars.yaml" ]]; then
1748
+ ui_step_error "Workspace already initialized — run 'revo context' to regenerate CLAUDE.md"
1749
+ return 1
1750
+ fi
1751
+
1752
+ ui_intro "Revo - Detect Existing Repositories"
1753
+ ui_step "Scanning for git repos..."
1754
+
1755
+ # Initialize the workspace using the cwd basename. This sets
1756
+ # REVO_WORKSPACE_ROOT/REVO_CONFIG_FILE/REVO_REPOS_DIR and writes an empty
1757
+ # revo.yaml + .gitignore.
1758
+ local default_name
1759
+ default_name=$(basename "$PWD")
1760
+ if ! config_init "$default_name"; then
1761
+ ui_step_error "Failed to initialize workspace"
1762
+ return 1
1763
+ fi
1764
+
1765
+ local found=0
1766
+ local d name remote category tags
1767
+
1768
+ for d in */; do
1769
+ d="${d%/}"
1770
+ [[ "$d" == "repos" ]] && continue
1771
+ [[ "$d" == ".revo" ]] && continue
1772
+ [[ -d "$d/.git" ]] || continue
1773
+
1774
+ name="$d"
1775
+ remote=$(cd "$d" && git remote get-url origin 2>/dev/null || echo "local://$d")
1776
+
1777
+ # Auto-categorize from package contents.
1778
+ category=""
1779
+ if [[ -f "$d/package.json" ]]; then
1780
+ if grep -qE '"(next|nuxt|react|vue|svelte|@angular/core|astro|@remix-run/react|@sveltejs/kit|expo|react-native)"' "$d/package.json" 2>/dev/null; then
1781
+ category="frontend"
1782
+ elif grep -qE '"(express|fastify|hono|@nestjs/core|nestjs|koa)"' "$d/package.json" 2>/dev/null; then
1783
+ category="backend"
1784
+ fi
1785
+ elif [[ -f "$d/go.mod" ]] || [[ -f "$d/Cargo.toml" ]] || [[ -f "$d/pom.xml" ]] || [[ -f "$d/build.gradle" ]] || [[ -f "$d/build.gradle.kts" ]]; then
1786
+ category="backend"
1787
+ elif [[ -f "$d/pyproject.toml" ]] || [[ -f "$d/requirements.txt" ]]; then
1788
+ category="backend"
1789
+ fi
1790
+
1791
+ if [[ -n "$category" ]] && [[ "$category" != "$name" ]]; then
1792
+ tags="$name, $category"
1793
+ else
1794
+ tags="$name"
1795
+ fi
1796
+
1797
+ # Link the root-level repo into repos/ so the rest of revo's data
1798
+ # model (which always resolves $REVO_REPOS_DIR/$path) works.
1799
+ if [[ ! -e "repos/$name" ]]; then
1800
+ ln -s "../$name" "repos/$name"
1801
+ fi
1802
+
1803
+ yaml_add_repo "$remote" "$name" "$tags" ""
1804
+ ui_step_done "Found:" "$name ($remote)"
1805
+ found=$((found + 1))
1806
+ done
1807
+
1808
+ if [[ -d "repos" ]]; then
1809
+ for d in repos/*/; do
1810
+ d="${d%/}"
1811
+ [[ -d "$d/.git" ]] || continue
1812
+ name=$(basename "$d")
1813
+ # Skip repos already added via symlink in the loop above.
1814
+ local already=0
1815
+ local i
1816
+ for ((i = 0; i < YAML_REPO_COUNT; i++)); do
1817
+ if [[ "$(yaml_get_path "$i")" == "$name" ]]; then
1818
+ already=1
1819
+ break
1820
+ fi
1821
+ done
1822
+ [[ $already -eq 1 ]] && continue
1823
+ remote=$(cd "$d" && git remote get-url origin 2>/dev/null || echo "local://$d")
1824
+ yaml_add_repo "$remote" "$name" "$name" ""
1825
+ ui_step_done "Found:" "$name ($remote)"
1826
+ found=$((found + 1))
1827
+ done
1828
+ fi
1829
+
1830
+ if [[ $found -eq 0 ]]; then
1831
+ rm -f "$REVO_CONFIG_FILE"
1832
+ ui_step_error "No git repos found in current directory. Use 'revo init' instead."
1833
+ ui_outro_cancel "Nothing to detect"
1834
+ return 1
1835
+ fi
1836
+
1837
+ config_save
1838
+ ui_step_done "Detected $found repository(ies)"
1839
+ ui_info "$(ui_dim "Edit revo.yaml to adjust tags or add depends_on relationships")"
1840
+ ui_bar_line
1841
+
1842
+ cmd_context
1453
1843
  return 0
1454
1844
  }
1455
1845
 
@@ -1548,9 +1938,10 @@ cmd_clone() {
1548
1938
  fi
1549
1939
  done <<< "$repos"
1550
1940
 
1551
- # Auto-generate workspace CLAUDE.md on first successful clone
1552
- if [[ $fail_count -eq 0 ]] && [[ $success_count -gt 0 ]]; then
1553
- context_autogenerate_if_missing
1941
+ # Always regenerate the workspace CLAUDE.md after a clone batch so that
1942
+ # newly cloned repos appear in the context immediately.
1943
+ if [[ $fail_count -eq 0 ]] && { [[ $success_count -gt 0 ]] || [[ $skip_count -gt 0 ]]; }; then
1944
+ context_regenerate_silent
1554
1945
  fi
1555
1946
 
1556
1947
  # Summary
@@ -2375,15 +2766,75 @@ _context_topo_sort() {
2375
2766
  return 0
2376
2767
  }
2377
2768
 
2378
- # 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.
2379
2826
  # Usage: _context_write_file "/path/to/CLAUDE.md"
2380
2827
  _context_write_file() {
2381
- local output="$1"
2828
+ local target="$1"
2382
2829
  local workspace
2383
2830
  workspace=$(config_workspace_name)
2384
2831
 
2385
- # Start file
2832
+ local output
2833
+ output=$(mktemp -t revo-auto.XXXXXX)
2834
+
2835
+ # Start auto block (markers + header)
2386
2836
  {
2837
+ printf '%s\n' "$CONTEXT_BEGIN_MARKER"
2387
2838
  printf '# Workspace Context (auto-generated by revo)\n'
2388
2839
  printf '\n'
2389
2840
  printf 'This workspace contains multiple repositories managed by revo.\n'
@@ -2391,8 +2842,9 @@ _context_write_file() {
2391
2842
  printf 'Workspace: **%s**\n' "$workspace"
2392
2843
  fi
2393
2844
  printf '\n'
2394
- printf '> This file is regenerated by `revo context`. Manual edits below the\n'
2395
- 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'
2396
2848
  printf '\n'
2397
2849
  printf '## Repos\n'
2398
2850
  printf '\n'
@@ -2489,7 +2941,29 @@ _context_write_file() {
2489
2941
  printf '\n> Warning: a dependency cycle was detected. Listed in best-effort order.\n' >> "$output"
2490
2942
  fi
2491
2943
 
2492
- # Agent instructions
2944
+ # Active features (any .revo/features/*.md files)
2945
+ local features_dir="$REVO_WORKSPACE_ROOT/.revo/features"
2946
+ if [[ -d "$features_dir" ]]; then
2947
+ local has_features=0
2948
+ local f
2949
+ for f in "$features_dir"/*.md; do
2950
+ [[ -f "$f" ]] || continue
2951
+ if [[ $has_features -eq 0 ]]; then
2952
+ {
2953
+ printf '\n'
2954
+ printf '## Active Features\n'
2955
+ printf '\n'
2956
+ } >> "$output"
2957
+ has_features=1
2958
+ fi
2959
+ local fname rel
2960
+ fname=$(basename "$f" .md)
2961
+ rel=".revo/features/$(basename "$f")"
2962
+ printf -- '- **%s** — see [%s](%s)\n' "$fname" "$rel" "$rel" >> "$output"
2963
+ done
2964
+ fi
2965
+
2966
+ # Agent instructions + revo tool docs
2493
2967
  {
2494
2968
  printf '\n'
2495
2969
  printf '## Agent Instructions\n'
@@ -2504,7 +2978,56 @@ _context_write_file() {
2504
2978
  printf '6. Use `revo commit "msg"` to commit across all repos at once\n'
2505
2979
  printf '7. Use `revo feature <name>` to start a coordinated feature workspace\n'
2506
2980
  printf '8. Use `revo pr "title"` to open coordinated pull requests\n'
2981
+ printf '\n'
2982
+ printf '## Workspace Tool: revo\n'
2983
+ printf '\n'
2984
+ printf 'This workspace is managed by revo.\n'
2985
+ printf 'Source: https://github.com/jippylong12/revo\n'
2986
+ printf '\n'
2987
+ printf '### Available commands (run in terminal)\n'
2988
+ printf '\n'
2989
+ printf '**Setup:**\n'
2990
+ printf -- '- `revo add <git-url> --tags <tags> [--depends-on <repo>]` — add a repo\n'
2991
+ printf -- '- `revo clone` — clone all configured repos\n'
2992
+ printf -- '- `revo context` — regenerate this file\n'
2993
+ printf -- '- `revo detect` — bootstrap around existing repos in cwd\n'
2994
+ printf '\n'
2995
+ printf '**Daily workflow:**\n'
2996
+ printf -- '- `revo status` — branch and dirty state across all repos\n'
2997
+ printf -- '- `revo sync` — pull latest across all repos\n'
2998
+ printf -- '- `revo feature <name> [--tag t]` — create feature branch across repos\n'
2999
+ printf -- '- `revo commit "msg" [--tag t]` — commit all dirty repos\n'
3000
+ printf -- '- `revo push [--tag t]` — push all repos\n'
3001
+ printf -- '- `revo pr "title" [--tag t]` — create coordinated PRs via gh CLI\n'
3002
+ printf -- '- `revo exec "cmd" [--tag t]` — run command in filtered repos\n'
3003
+ printf -- '- `revo checkout <branch> [--tag t]` — switch branch across repos\n'
3004
+ printf '\n'
3005
+ printf '### Tag filtering\n'
3006
+ printf '\n'
3007
+ printf 'All commands support `--tag <tag>` to target specific repos:\n'
3008
+ printf '\n'
3009
+ printf '```\n'
3010
+ printf 'revo exec "npm test" --tag frontend\n'
3011
+ printf 'revo sync --tag backend\n'
3012
+ printf 'revo branch hotfix --tag api\n'
3013
+ printf '```\n'
3014
+ printf '\n'
3015
+ printf '### Feature workflow\n'
3016
+ printf '\n'
3017
+ printf '```\n'
3018
+ printf 'revo feature my-feature # branches all repos\n'
3019
+ printf '# edit .revo/features/my-feature.md to describe scope\n'
3020
+ printf '# work across repos\n'
3021
+ printf 'revo commit "feat: my feature" # commits all dirty repos\n'
3022
+ printf 'revo pr "My feature" # coordinated PRs\n'
3023
+ printf '```\n'
3024
+ printf '\n'
3025
+ printf '%s\n' "$CONTEXT_END_MARKER"
2507
3026
  } >> "$output"
3027
+
3028
+ # Splice the auto block into the target file, preserving any user content.
3029
+ _context_merge_into "$output" "$target"
3030
+ rm -f "$output"
2508
3031
  }
2509
3032
 
2510
3033
  cmd_context() {
@@ -2551,15 +3074,15 @@ cmd_context() {
2551
3074
  return 0
2552
3075
  }
2553
3076
 
2554
- # Called automatically from cmd_clone if CLAUDE.md does not yet exist.
2555
- context_autogenerate_if_missing() {
3077
+ # Called automatically from cmd_clone after a successful clone batch.
3078
+ # Always rewrites the workspace CLAUDE.md so newly cloned repos show up.
3079
+ context_regenerate_silent() {
2556
3080
  [[ -z "$REVO_WORKSPACE_ROOT" ]] && return 0
2557
- local output="$REVO_WORKSPACE_ROOT/CLAUDE.md"
2558
- [[ -f "$output" ]] && return 0
2559
3081
  [[ $YAML_REPO_COUNT -eq 0 ]] && return 0
2560
3082
 
3083
+ local output="$REVO_WORKSPACE_ROOT/CLAUDE.md"
2561
3084
  _context_write_file "$output"
2562
- ui_info "$(ui_dim "Auto-generated CLAUDE.md for Claude Code")"
3085
+ ui_info "$(ui_dim "Regenerated CLAUDE.md for Claude Code")"
2563
3086
  return 0
2564
3087
  }
2565
3088
 
@@ -3144,7 +3667,8 @@ Revo - Claude-first multi-repo workspace manager v$REVO_VERSION
3144
3667
  Usage: revo <command> [options]
3145
3668
 
3146
3669
  Workspace commands:
3147
- init Initialize a new workspace
3670
+ init Initialize a new workspace (auto-detects existing repos)
3671
+ detect Bootstrap revo around existing git repos in cwd
3148
3672
  add URL [--tags TAGS] Add a repository to the workspace
3149
3673
  clone [--tag TAG] Clone configured repositories
3150
3674
  list [--tag TAG] List configured repositories
@@ -3207,6 +3731,9 @@ main() {
3207
3731
  init)
3208
3732
  cmd_init "$@"
3209
3733
  ;;
3734
+ detect)
3735
+ cmd_detect "$@"
3736
+ ;;
3210
3737
  clone)
3211
3738
  cmd_clone "$@"
3212
3739
  ;;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revotools/cli",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "Claude-first multi-repo workspace manager (fork of Mars)",
5
5
  "bin": {
6
6
  "revo": "./dist/revo"