repo-nexus 0.1.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/LICENSE +21 -0
- package/README.md +273 -0
- package/docs/AGENTS.sample.md +10 -0
- package/docs/rnex.example.yaml +90 -0
- package/package.json +46 -0
- package/rnex +690 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Repo Nexus Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Repo Nexus (`rnex`)
|
|
2
|
+
|
|
3
|
+
[](https://github.com/nu-nenoi/repo-nexus/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/repo-nexus)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](#)
|
|
7
|
+
[](#)
|
|
8
|
+
|
|
9
|
+
A tooling-independent, zero-dependency workspace orchestrator for multiple repositories with shared, auto-synced AI context across projects using Unix symlinks.
|
|
10
|
+
|
|
11
|
+
- **No git submodules, subtrees, or nested git friction**
|
|
12
|
+
- **No IDE lock-in** (works across VS Code, Cursor, Antigravity, Claude Code, Zed, terminal agents)
|
|
13
|
+
- **AI provider-agnostic** (shares `AGENTS.md`, Copilot instructions, Cursor rules, and custom prompts)
|
|
14
|
+
- **Automatic AI context injection** on repository registration
|
|
15
|
+
- **Auto-detects existing AI configs** (`CLAUDE.md`, `.cursorrules`, `.windsurfrules`, Copilot instructions, etc.)
|
|
16
|
+
- **Context-aware**: Finds and uses `rnex.yaml` automatically from your current directory or parent tree
|
|
17
|
+
- **Zero dependencies** (pure POSIX shell CLI)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## How It Works: The Two Symlink Flows
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
25
|
+
│ 1. SCOPE IN — Bring member repos INTO Repo Nexus │
|
|
26
|
+
│ │
|
|
27
|
+
│ my-workspace/ │
|
|
28
|
+
│ repos/ │
|
|
29
|
+
│ backend/ ──(symlink)──> ~/code/backend-api │
|
|
30
|
+
│ frontend/ ──(symlink)──> ~/code/web-app │
|
|
31
|
+
│ AGENTS.md (universal AI instructions) │
|
|
32
|
+
│ rnex.yaml (manifest: AI files + repos) │
|
|
33
|
+
│ │
|
|
34
|
+
│ Agent opens workspace → sees all repos in one place. │
|
|
35
|
+
│ Edits through symlinks modify the original files directly. │
|
|
36
|
+
└─────────────────────────────────────────────────────────────┘
|
|
37
|
+
|
|
38
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
39
|
+
│ 2. INJECT OUT — Auto-sync AI context INTO member repos │
|
|
40
|
+
│ │
|
|
41
|
+
│ ~/code/backend-api/ │
|
|
42
|
+
│ AGENTS.md ──(symlink)──> my-workspace/AGENTS.md │
|
|
43
|
+
│ src/ (real codebase) │
|
|
44
|
+
│ │
|
|
45
|
+
│ Open backend-api standalone → AI tools automatically see │
|
|
46
|
+
│ shared instructions as if they were local files. │
|
|
47
|
+
└─────────────────────────────────────────────────────────────┘
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Key Principles
|
|
53
|
+
|
|
54
|
+
1. **Symlink Write-Through**:
|
|
55
|
+
Symlinks are transparent pointers. When an AI agent or developer edits `repos/backend/src/index.ts`, the OS resolves the link and writes directly to the source repository on disk.
|
|
56
|
+
|
|
57
|
+
2. **Autonomous Git Repositories**:
|
|
58
|
+
Each member repository retains its own Git history, branches, and remotes. Git operations (commit, push, pull) are executed directly inside each member repo.
|
|
59
|
+
|
|
60
|
+
3. **Single Source of Truth for AI Context**:
|
|
61
|
+
Edit `AGENTS.md` in Repo Nexus, and changes immediately reflect across all member repositories.
|
|
62
|
+
|
|
63
|
+
4. **Dynamic Workspace Scope**:
|
|
64
|
+
Easily show or hide member repos from the active workspace without modifying disk contents.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
You can install `rnex` using either the native zero-dependency installer or via npm:
|
|
71
|
+
|
|
72
|
+
### Option 1: Native Installer (Zero Dependencies)
|
|
73
|
+
|
|
74
|
+
Clone the repository and run the built-in installer:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
git clone https://github.com/nu-nenoi/repo-nexus.git
|
|
78
|
+
cd repo-nexus
|
|
79
|
+
./rnex install
|
|
80
|
+
```
|
|
81
|
+
*(Installs `rnex` and `repo-nexus` symlinks into `~/.local/bin`, or pass a custom directory like `./rnex install /usr/local/bin`)*
|
|
82
|
+
|
|
83
|
+
### Option 2: Via npm
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm install -g repo-nexus
|
|
87
|
+
```
|
|
88
|
+
*(Installs both `repo-nexus` and `rnex` executable commands globally)*
|
|
89
|
+
|
|
90
|
+
### Option 3: Shell Alias
|
|
91
|
+
|
|
92
|
+
Add to your `~/.zshrc` or `~/.bashrc`:
|
|
93
|
+
```bash
|
|
94
|
+
alias rnex="/path/to/repo-nexus/rnex"
|
|
95
|
+
alias repo-nexus="/path/to/repo-nexus/rnex"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Quick Start
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# 1. Initialize a new workspace
|
|
104
|
+
mkdir my-workspace && cd my-workspace
|
|
105
|
+
rnex init
|
|
106
|
+
|
|
107
|
+
# 2. Register repositories
|
|
108
|
+
rnex add backend ~/code/backend-api
|
|
109
|
+
rnex add frontend ../web-app # relative paths work too
|
|
110
|
+
|
|
111
|
+
# 3. Check workspace health
|
|
112
|
+
rnex status
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
During `rnex init`, the CLI automatically scans for existing AI configuration files (like `CLAUDE.md`, `.cursorrules`, `.github/copilot-instructions.md`) and adds them to your config.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Everyday Usage
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# Register a repository (links into scope & auto-injects AI context)
|
|
123
|
+
rnex add my-app ~/code/my-app
|
|
124
|
+
|
|
125
|
+
# Inspect workspace status & linked AI files
|
|
126
|
+
rnex status
|
|
127
|
+
|
|
128
|
+
# List all registered repositories
|
|
129
|
+
rnex list
|
|
130
|
+
|
|
131
|
+
# Temporarily hide a repo from active indexing/agent scope
|
|
132
|
+
rnex hide my-app
|
|
133
|
+
|
|
134
|
+
# Restore a hidden repo back to active scope
|
|
135
|
+
rnex show my-app
|
|
136
|
+
|
|
137
|
+
# Reconcile/repair all symlinks across all repos (idempotent)
|
|
138
|
+
rnex sync
|
|
139
|
+
|
|
140
|
+
# Unregister a repository (removes scope link & cleans up injected AI files)
|
|
141
|
+
rnex remove my-app
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Workspace Configuration (`rnex.yaml`)
|
|
147
|
+
|
|
148
|
+
The configuration file defines repo symlink directory, AI context files to sync, and registered repositories:
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
# Directory for repository symlinks (relative or absolute)
|
|
152
|
+
repos_dir: ./repos
|
|
153
|
+
|
|
154
|
+
# AI context files to automatically sync into every member repo
|
|
155
|
+
ai_files:
|
|
156
|
+
- AGENTS.md
|
|
157
|
+
- .github/copilot-instructions.md
|
|
158
|
+
# - .cursorrules
|
|
159
|
+
# - CLAUDE.md
|
|
160
|
+
|
|
161
|
+
# Member repositories
|
|
162
|
+
repos:
|
|
163
|
+
backend:
|
|
164
|
+
path: /Users/dev/code/backend-api
|
|
165
|
+
scope: visible
|
|
166
|
+
frontend:
|
|
167
|
+
path: ../web-app
|
|
168
|
+
scope: visible
|
|
169
|
+
analytics:
|
|
170
|
+
path: /Users/dev/code/analytics
|
|
171
|
+
scope: hidden
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
> See [`docs/rnex.example.yaml`](docs/rnex.example.yaml) for a comprehensive example with all options and supported AI tool configs.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## CLI Command Reference
|
|
179
|
+
|
|
180
|
+
### Global Options
|
|
181
|
+
| Option | Description |
|
|
182
|
+
|:---|:---|
|
|
183
|
+
| `-c, --config <file>` | Explicit path to `rnex.yaml` (executes in that workspace directory) |
|
|
184
|
+
| `-h, --help` | Display command help and usage instructions |
|
|
185
|
+
| `-v, --version` | Display version |
|
|
186
|
+
|
|
187
|
+
### Commands
|
|
188
|
+
| Command | Description |
|
|
189
|
+
|:---|:---|
|
|
190
|
+
| `rnex init [dir]` | Initialize a new workspace in current (or target) directory |
|
|
191
|
+
| `rnex install [dir]` | Install `rnex` & `repo-nexus` globally into `~/.local/bin` (or custom dir) |
|
|
192
|
+
| `rnex add <name> <path>` | Register repo, create scope symlink, and auto-inject AI context |
|
|
193
|
+
| `rnex remove <name>` | Unregister repo, unlink from scope, and clean up injected AI files |
|
|
194
|
+
| `rnex list` | List all registered repos and visibility scopes |
|
|
195
|
+
| `rnex status` | Display status of AI context files, active member repos, and paths |
|
|
196
|
+
| `rnex show <name>` | Make a hidden repo visible in workspace |
|
|
197
|
+
| `rnex hide <name>` | Hide a repo from active workspace indexing |
|
|
198
|
+
| `rnex sync` | Reconcile all scope symlinks and AI context files from config |
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Operating on External Workspaces via `--config`
|
|
203
|
+
|
|
204
|
+
Run `rnex` commands targeting any workspace without changing directories:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
# Inspect status of a workspace stored elsewhere
|
|
208
|
+
rnex -c /path/to/my-workspace/rnex.yaml status
|
|
209
|
+
|
|
210
|
+
# Add a repository to an external workspace
|
|
211
|
+
rnex --config /path/to/my-workspace/rnex.yaml add api-service ~/code/api
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Any command executed with an explicit config file operates within the directory where that config lives.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Supported AI Configuration Files
|
|
219
|
+
|
|
220
|
+
`rnex init` auto-detects these files and includes them in your workspace config:
|
|
221
|
+
|
|
222
|
+
| File / Directory | AI Tool |
|
|
223
|
+
|:---|:---|
|
|
224
|
+
| `AGENTS.md` | Universal AI agent instructions |
|
|
225
|
+
| `CLAUDE.md` | Claude Code |
|
|
226
|
+
| `.cursorrules` | Cursor |
|
|
227
|
+
| `.cursor/rules/` | Cursor (directory) |
|
|
228
|
+
| `.windsurfrules` | Windsurf / Codeium |
|
|
229
|
+
| `.windsurf/rules/` | Windsurf (directory) |
|
|
230
|
+
| `.github/copilot-instructions.md` | GitHub Copilot |
|
|
231
|
+
| `.aider.conf.yml` | Aider |
|
|
232
|
+
| `CONVENTIONS.md` | Coding conventions |
|
|
233
|
+
| `.clinerules` | Cline / Roo Code |
|
|
234
|
+
| `CODEX.md` | Codex |
|
|
235
|
+
| `.continue/` | Continue.dev |
|
|
236
|
+
| `SKILL.md` | Skills (emerging standard) |
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Project Structure
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
repo-nexus/
|
|
244
|
+
├── rnex.yaml # Workspace configuration
|
|
245
|
+
├── rnex # CLI executable (POSIX shell)
|
|
246
|
+
├── AGENTS.md # Universal AI coding guidelines
|
|
247
|
+
├── docs/
|
|
248
|
+
│ ├── AGENTS.sample.md # Template for AGENTS.md
|
|
249
|
+
│ └── rnex.example.yaml # Full config reference with examples
|
|
250
|
+
├── .github/
|
|
251
|
+
│ ├── workflows/ci.yml # GitHub Actions CI workflow
|
|
252
|
+
│ └── ISSUE_TEMPLATE/ # Bug report and feature request templates
|
|
253
|
+
├── tests/
|
|
254
|
+
│ └── test_cli.sh # Automated CLI test suite (11 tests)
|
|
255
|
+
├── toolkit/ # Shared prompts, scripts, templates
|
|
256
|
+
├── package.json # npm package manifest
|
|
257
|
+
├── LICENSE # MIT License
|
|
258
|
+
└── README.md
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Running Tests
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
./tests/test_cli.sh
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Multi-Repo AI Workspace Context
|
|
2
|
+
|
|
3
|
+
This workspace operates as a **Repo Nexus** linking multiple independent repositories via symlinks.
|
|
4
|
+
|
|
5
|
+
## Rules for AI Coding Assistants
|
|
6
|
+
|
|
7
|
+
1. **Symlink Write-Through**: Files edited under `repos/<name>/` directly modify the target repository.
|
|
8
|
+
2. **Repo Boundaries**: Each repository is isolated. Do not introduce cross-repo imports unless explicitly architected.
|
|
9
|
+
3. **Shared Context**: This file is symlinked across member repositories. Keep instructions universal. Use local instructions for repo-specific rules.
|
|
10
|
+
4. **Git Operations**: Commit and push changes directly within the respective member repository root.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# rnex.yaml — Repo Nexus Workspace Configuration (Full Example)
|
|
3
|
+
# ==============================================================================
|
|
4
|
+
# This file is the central manifest for a Repo Nexus workspace.
|
|
5
|
+
# Place it in your workspace root directory and run `rnex sync` to apply.
|
|
6
|
+
#
|
|
7
|
+
# For more information: https://github.com/nu-nenoi/repo-nexus
|
|
8
|
+
|
|
9
|
+
# ------------------------------------------------------------------------------
|
|
10
|
+
# repos_dir — Directory for repository symlinks
|
|
11
|
+
# ------------------------------------------------------------------------------
|
|
12
|
+
# Relative paths are resolved relative to this config file's location.
|
|
13
|
+
# Default: ./repos
|
|
14
|
+
#
|
|
15
|
+
# Examples:
|
|
16
|
+
# repos_dir: ./repos # relative (recommended)
|
|
17
|
+
# repos_dir: ./workspace/repos # nested relative path
|
|
18
|
+
# repos_dir: /opt/dev/repos # absolute path
|
|
19
|
+
|
|
20
|
+
repos_dir: ./repos
|
|
21
|
+
|
|
22
|
+
# ------------------------------------------------------------------------------
|
|
23
|
+
# ai_files — AI context files to auto-sync into member repositories
|
|
24
|
+
# ------------------------------------------------------------------------------
|
|
25
|
+
# Each file listed here is symlinked from the workspace root into every
|
|
26
|
+
# registered member repository. This keeps AI instructions consistent
|
|
27
|
+
# across all your projects.
|
|
28
|
+
#
|
|
29
|
+
# Paths are relative to the workspace root (where this config file lives).
|
|
30
|
+
# Both files and directories are supported.
|
|
31
|
+
#
|
|
32
|
+
# Well-known AI configuration files by tool:
|
|
33
|
+
# AGENTS.md — Universal AI agent instructions
|
|
34
|
+
# CLAUDE.md — Claude Code project instructions
|
|
35
|
+
# .cursorrules — Cursor editor rules
|
|
36
|
+
# .cursor/rules/ — Cursor rules directory
|
|
37
|
+
# .windsurfrules — Windsurf / Codeium rules
|
|
38
|
+
# .windsurf/rules/ — Windsurf rules directory
|
|
39
|
+
# .github/copilot-instructions.md — GitHub Copilot custom instructions
|
|
40
|
+
# .aider.conf.yml — Aider configuration
|
|
41
|
+
# CONVENTIONS.md — Coding conventions (Aider / general)
|
|
42
|
+
# .clinerules — Cline / Roo Code rules
|
|
43
|
+
# CODEX.md — Codex project instructions
|
|
44
|
+
# .continue/ — Continue.dev configuration directory
|
|
45
|
+
# SKILL.md — Skills definition (emerging standard)
|
|
46
|
+
|
|
47
|
+
ai_files:
|
|
48
|
+
- AGENTS.md
|
|
49
|
+
- .github/copilot-instructions.md
|
|
50
|
+
# - .cursorrules
|
|
51
|
+
# - CLAUDE.md
|
|
52
|
+
# - .windsurfrules
|
|
53
|
+
# - CONVENTIONS.md
|
|
54
|
+
# - toolkit/prompts/review-checklist.md
|
|
55
|
+
|
|
56
|
+
# ------------------------------------------------------------------------------
|
|
57
|
+
# repos — Member repositories managed by Repo Nexus
|
|
58
|
+
# ------------------------------------------------------------------------------
|
|
59
|
+
# Each repo has:
|
|
60
|
+
# path: Absolute or relative path to the repository on disk.
|
|
61
|
+
# Relative paths are resolved relative to this config file.
|
|
62
|
+
# scope: "visible" (default) creates a symlink in repos_dir.
|
|
63
|
+
# "hidden" removes the symlink but keeps the registration.
|
|
64
|
+
#
|
|
65
|
+
# Examples with absolute paths:
|
|
66
|
+
# backend-api:
|
|
67
|
+
# path: /Users/dev/projects/backend-api
|
|
68
|
+
# scope: visible
|
|
69
|
+
#
|
|
70
|
+
# Examples with relative paths:
|
|
71
|
+
# frontend:
|
|
72
|
+
# path: ../frontend-app
|
|
73
|
+
# scope: visible
|
|
74
|
+
#
|
|
75
|
+
# shared-lib:
|
|
76
|
+
# path: ../../libs/shared
|
|
77
|
+
# scope: hidden
|
|
78
|
+
|
|
79
|
+
repos:
|
|
80
|
+
# backend-api:
|
|
81
|
+
# path: /Users/dev/projects/backend-api
|
|
82
|
+
# scope: visible
|
|
83
|
+
#
|
|
84
|
+
# frontend-app:
|
|
85
|
+
# path: ../frontend-app
|
|
86
|
+
# scope: visible
|
|
87
|
+
#
|
|
88
|
+
# analytics:
|
|
89
|
+
# path: /Users/dev/projects/analytics-service
|
|
90
|
+
# scope: hidden
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "repo-nexus",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tooling-independent multi-repo workspace manager with auto-synced AI context via symlinks",
|
|
5
|
+
"bin": {
|
|
6
|
+
"repo-nexus": "./rnex",
|
|
7
|
+
"rnex": "./rnex"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"rnex",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"docs/"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/nu-nenoi/repo-nexus.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/nu-nenoi/repo-nexus/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/nu-nenoi/repo-nexus#readme",
|
|
23
|
+
"keywords": [
|
|
24
|
+
"repo",
|
|
25
|
+
"repository",
|
|
26
|
+
"workspace",
|
|
27
|
+
"multi-repo",
|
|
28
|
+
"monorepo",
|
|
29
|
+
"symlink",
|
|
30
|
+
"ai-context",
|
|
31
|
+
"cli",
|
|
32
|
+
"agents",
|
|
33
|
+
"copilot",
|
|
34
|
+
"cursor",
|
|
35
|
+
"developer-tools"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"author": "Anton Shramkov",
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=14"
|
|
41
|
+
},
|
|
42
|
+
"os": [
|
|
43
|
+
"darwin",
|
|
44
|
+
"linux"
|
|
45
|
+
]
|
|
46
|
+
}
|
package/rnex
ADDED
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# ============================================================================
|
|
3
|
+
# rnex — Repo Nexus CLI
|
|
4
|
+
# Tooling-independent multi-repo workspace manager with auto-synced AI context.
|
|
5
|
+
# Zero dependencies. POSIX-compatible.
|
|
6
|
+
# ============================================================================
|
|
7
|
+
set -e
|
|
8
|
+
|
|
9
|
+
RNEX_VERSION="0.1.0"
|
|
10
|
+
RNEX_CONFIG_FILE="rnex.yaml"
|
|
11
|
+
|
|
12
|
+
# ---- Terminal Colours (auto-detected) -------------------------------------
|
|
13
|
+
|
|
14
|
+
if [ -t 1 ]; then
|
|
15
|
+
_R='\033[0;31m' _G='\033[0;32m' _Y='\033[1;33m' _B='\033[0;34m' _C='\033[0;36m'
|
|
16
|
+
_BOLD='\033[1m' _DIM='\033[2m' _NC='\033[0m'
|
|
17
|
+
else
|
|
18
|
+
_R='' _G='' _Y='' _B='' _C='' _BOLD='' _DIM='' _NC=''
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# ---- Logging ---------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
log_ok() { printf "${_G}[✓]${_NC} %s\n" "$1"; }
|
|
24
|
+
log_warn() { printf "${_Y}[!]${_NC} %s\n" "$1"; }
|
|
25
|
+
log_err() { printf "${_R}[✗]${_NC} %s\n" "$1" >&2; }
|
|
26
|
+
log_info() { printf "${_C}[i]${_NC} %s\n" "$1"; }
|
|
27
|
+
log_dim() { printf "${_DIM} %s${_NC}\n" "$1"; }
|
|
28
|
+
die() { log_err "$1"; exit 1; }
|
|
29
|
+
|
|
30
|
+
# ---- Path Utilities --------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
resolve_path() {
|
|
33
|
+
_input="$1"
|
|
34
|
+
case "$_input" in
|
|
35
|
+
\~/*) _input="$HOME/${_input#\~/}" ;;
|
|
36
|
+
\~) _input="$HOME" ;;
|
|
37
|
+
esac
|
|
38
|
+
case "$_input" in
|
|
39
|
+
/*)
|
|
40
|
+
printf '%s' "$_input"
|
|
41
|
+
;;
|
|
42
|
+
*)
|
|
43
|
+
if [ -d "$_input" ]; then
|
|
44
|
+
printf '%s' "$(cd "$_input" 2>/dev/null && pwd -P)"
|
|
45
|
+
else
|
|
46
|
+
_parent="$(cd "$(dirname "$_input")" 2>/dev/null && pwd -P)"
|
|
47
|
+
_base="$(basename "$_input")"
|
|
48
|
+
if [ "$_parent" = "/" ]; then
|
|
49
|
+
printf '/%s' "$_base"
|
|
50
|
+
else
|
|
51
|
+
printf '%s/%s' "$_parent" "$_base"
|
|
52
|
+
fi
|
|
53
|
+
fi
|
|
54
|
+
;;
|
|
55
|
+
esac
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Resolve location of this script (follows symlinks)
|
|
59
|
+
resolve_script_path() {
|
|
60
|
+
_src="$1"
|
|
61
|
+
while [ -L "$_src" ]; do
|
|
62
|
+
_dir="$(cd -P "$(dirname "$_src")" && pwd)"
|
|
63
|
+
_src="$(readlink "$_src")"
|
|
64
|
+
case "$_src" in
|
|
65
|
+
/*) ;;
|
|
66
|
+
*) _src="$_dir/$_src" ;;
|
|
67
|
+
esac
|
|
68
|
+
done
|
|
69
|
+
_dir="$(cd -P "$(dirname "$_src")" && pwd)"
|
|
70
|
+
printf '%s/%s' "$_dir" "$(basename "$_src")"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
SCRIPT_LOCATION="$(resolve_script_path "$0")"
|
|
74
|
+
|
|
75
|
+
# ---- Workspace Detection (Context-Aware) ----------------------------------
|
|
76
|
+
|
|
77
|
+
# Search for rnex.yaml in current directory or parent directories
|
|
78
|
+
find_workspace_dir() {
|
|
79
|
+
_curr="$(pwd -P 2>/dev/null || pwd)"
|
|
80
|
+
while [ -n "$_curr" ] && [ "$_curr" != "/" ]; do
|
|
81
|
+
if [ -f "$_curr/$RNEX_CONFIG_FILE" ]; then
|
|
82
|
+
printf '%s' "$_curr"
|
|
83
|
+
return 0
|
|
84
|
+
fi
|
|
85
|
+
_curr="$(dirname "$_curr")"
|
|
86
|
+
done
|
|
87
|
+
if [ -f "/$RNEX_CONFIG_FILE" ]; then
|
|
88
|
+
printf '/'
|
|
89
|
+
return 0
|
|
90
|
+
fi
|
|
91
|
+
return 1
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Locate workspace root for active commands
|
|
95
|
+
require_workspace() {
|
|
96
|
+
if [ -n "$CUSTOM_CONFIG" ]; then
|
|
97
|
+
_cfg_path="$(resolve_path "$CUSTOM_CONFIG")"
|
|
98
|
+
if [ ! -f "$_cfg_path" ]; then
|
|
99
|
+
die "Specified config file does not exist: $_cfg_path"
|
|
100
|
+
fi
|
|
101
|
+
NEXUS_DIR="$(cd -P "$(dirname "$_cfg_path")" 2>/dev/null && pwd -P)"
|
|
102
|
+
WORKSPACE_YAML="$_cfg_path"
|
|
103
|
+
_load_repos_dir
|
|
104
|
+
log_info "Using workspace at ${_BOLD}$NEXUS_DIR${_NC} ${_DIM}(via config: $WORKSPACE_YAML)${_NC}"
|
|
105
|
+
return 0
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
_found_dir="$(find_workspace_dir || true)"
|
|
109
|
+
if [ -z "$_found_dir" ]; then
|
|
110
|
+
log_err "No '$RNEX_CONFIG_FILE' found in current directory ($PWD) or parent directories."
|
|
111
|
+
log_dim "To initialize a new Repo Nexus workspace here, run:"
|
|
112
|
+
log_dim " rnex init"
|
|
113
|
+
log_dim "Or specify a workspace config file directly:"
|
|
114
|
+
log_dim " rnex --config /path/to/$RNEX_CONFIG_FILE <command>"
|
|
115
|
+
log_dim "Or change directory to an existing workspace:"
|
|
116
|
+
log_dim " cd /path/to/workspace"
|
|
117
|
+
exit 1
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
NEXUS_DIR="$_found_dir"
|
|
121
|
+
WORKSPACE_YAML="$NEXUS_DIR/$RNEX_CONFIG_FILE"
|
|
122
|
+
_load_repos_dir
|
|
123
|
+
|
|
124
|
+
log_info "Using workspace at ${_BOLD}$NEXUS_DIR${_NC} ${_DIM}($RNEX_CONFIG_FILE found)${_NC}"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Parse repos_dir from config, default to ./repos
|
|
128
|
+
_load_repos_dir() {
|
|
129
|
+
_rd=""
|
|
130
|
+
if [ -f "$WORKSPACE_YAML" ]; then
|
|
131
|
+
_rd="$(awk '/^repos_dir:/ { sub(/^repos_dir:[ ]*/, ""); sub(/[ ]*#.*/, ""); print }' "$WORKSPACE_YAML")"
|
|
132
|
+
fi
|
|
133
|
+
_rd="${_rd:-./repos}"
|
|
134
|
+
case "$_rd" in
|
|
135
|
+
/*) REPOS_DIR="$_rd" ;;
|
|
136
|
+
*) REPOS_DIR="$NEXUS_DIR/$_rd" ;;
|
|
137
|
+
esac
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# ---- YAML Parser / Manipulator (for rnex.yaml) ----------------------------
|
|
141
|
+
|
|
142
|
+
# Parse list of ai_files from rnex.yaml
|
|
143
|
+
yaml_list_ai_files() {
|
|
144
|
+
[ -f "$WORKSPACE_YAML" ] || return 0
|
|
145
|
+
cat "$WORKSPACE_YAML" | awk '
|
|
146
|
+
/^ai_files:/ { a=1; next }
|
|
147
|
+
a && /^[^ #]/ { exit }
|
|
148
|
+
a && /^[ ]*-[ ]*/ {
|
|
149
|
+
sub(/^[ ]*-[ ]*/, "");
|
|
150
|
+
sub(/[ ]*#.*/, "");
|
|
151
|
+
if (length($0) > 0) print $0
|
|
152
|
+
}
|
|
153
|
+
'
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# Parse list of repos from rnex.yaml
|
|
157
|
+
yaml_list_repos() {
|
|
158
|
+
[ -f "$WORKSPACE_YAML" ] || return 0
|
|
159
|
+
cat "$WORKSPACE_YAML" | awk '
|
|
160
|
+
/^repos:/ { r=1; next }
|
|
161
|
+
r && /^[^ #]/ { exit }
|
|
162
|
+
r && /^ [a-zA-Z0-9_.-]+:[ ]*$/ {
|
|
163
|
+
sub(/^ /, ""); sub(/:.*/, ""); print
|
|
164
|
+
}
|
|
165
|
+
'
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Get a field value for a repo
|
|
169
|
+
yaml_get() {
|
|
170
|
+
_repo="$1"; _field="$2"
|
|
171
|
+
[ -f "$WORKSPACE_YAML" ] || return 0
|
|
172
|
+
cat "$WORKSPACE_YAML" | awk -v repo="$_repo" -v field="$_field" '
|
|
173
|
+
/^repos:/ { r=1; next }
|
|
174
|
+
r && /^[^ #]/ { exit }
|
|
175
|
+
r && match($0, "^ " repo ":[ ]*$") { t=1; next }
|
|
176
|
+
r && t && /^ [^ #]/ { exit }
|
|
177
|
+
r && t && match($0, "^ " field ":") {
|
|
178
|
+
sub(/^ [a-zA-Z0-9_.-]+:[ ]*/, ""); print
|
|
179
|
+
}
|
|
180
|
+
'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
yaml_has_repo() {
|
|
184
|
+
yaml_list_repos | grep -qx "$1" 2>/dev/null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
yaml_add_repo() {
|
|
188
|
+
_name="$1"; _path="$2"
|
|
189
|
+
if ! grep -q '^repos:' "$WORKSPACE_YAML"; then
|
|
190
|
+
printf '\nrepos:\n' >> "$WORKSPACE_YAML"
|
|
191
|
+
fi
|
|
192
|
+
printf ' %s:\n path: %s\n scope: visible\n' \
|
|
193
|
+
"$_name" "$_path" >> "$WORKSPACE_YAML"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
yaml_remove_repo() {
|
|
197
|
+
_name="$1"
|
|
198
|
+
_tmp="$(mktemp)"
|
|
199
|
+
awk -v name="$_name" '
|
|
200
|
+
/^repos:/ { r=1; print; next }
|
|
201
|
+
r && /^[^ #]/ { r=0 }
|
|
202
|
+
r && match($0, "^ " name ":[ ]*$") { skip=1; next }
|
|
203
|
+
r && skip && /^ [^ #]/ { skip=0 }
|
|
204
|
+
skip && /^ / { next }
|
|
205
|
+
{ print }
|
|
206
|
+
' "$WORKSPACE_YAML" > "$_tmp" && mv "$_tmp" "$WORKSPACE_YAML"
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
yaml_set() {
|
|
210
|
+
_repo="$1"; _field="$2"; _value="$3"
|
|
211
|
+
_tmp="$(mktemp)"
|
|
212
|
+
awk -v repo="$_repo" -v field="$_field" -v value="$_value" '
|
|
213
|
+
/^repos:/ { r=1; print; next }
|
|
214
|
+
r && /^[^ #]/ { r=0 }
|
|
215
|
+
r && match($0, "^ " repo ":[ ]*$") { t=1; print; next }
|
|
216
|
+
r && t && /^ [^ #]/ { t=0 }
|
|
217
|
+
r && t && match($0, "^ " field ":") {
|
|
218
|
+
printf " %s: %s\n", field, value; next
|
|
219
|
+
}
|
|
220
|
+
{ print }
|
|
221
|
+
' "$WORKSPACE_YAML" > "$_tmp" && mv "$_tmp" "$WORKSPACE_YAML"
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# ---- Symlink Operations ---------------------------------------------------
|
|
225
|
+
|
|
226
|
+
ensure_repos_dir() { mkdir -p "$REPOS_DIR"; }
|
|
227
|
+
|
|
228
|
+
create_scope_link() {
|
|
229
|
+
_name="$1"
|
|
230
|
+
_path="$(yaml_get "$_name" "path")"
|
|
231
|
+
[ -n "$_path" ] || die "No path recorded for repo '$_name'"
|
|
232
|
+
[ -d "$_path" ] || { log_warn "Path does not exist: $_path"; return 1; }
|
|
233
|
+
rm -f "$REPOS_DIR/$_name"
|
|
234
|
+
ln -s "$_path" "$REPOS_DIR/$_name"
|
|
235
|
+
log_ok "Linked scope: repos/$_name → $_path"
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
remove_scope_link() {
|
|
239
|
+
_name="$1"
|
|
240
|
+
if [ -L "$REPOS_DIR/$_name" ]; then
|
|
241
|
+
rm -f "$REPOS_DIR/$_name"
|
|
242
|
+
log_ok "Unlinked scope: repos/$_name"
|
|
243
|
+
fi
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
inject_ai_files() {
|
|
247
|
+
_name="$1"
|
|
248
|
+
_repo_path="$(yaml_get "$_name" "path")"
|
|
249
|
+
[ -n "$_repo_path" ] || die "No path recorded for repo '$_name'"
|
|
250
|
+
[ -d "$_repo_path" ] || die "Repo path does not exist: $_repo_path"
|
|
251
|
+
|
|
252
|
+
yaml_list_ai_files | while read -r _item; do
|
|
253
|
+
[ -n "$_item" ] || continue
|
|
254
|
+
|
|
255
|
+
_source="$NEXUS_DIR/$_item"
|
|
256
|
+
_target="$_repo_path/$_item"
|
|
257
|
+
|
|
258
|
+
if [ ! -e "$_source" ]; then
|
|
259
|
+
log_warn "AI file not found in nexus root, skipping: $_item"
|
|
260
|
+
continue
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
if [ -e "$_target" ] && [ ! -L "$_target" ]; then
|
|
264
|
+
log_warn "Existing non-symlink file found, skipping: $_target"
|
|
265
|
+
continue
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
_parent="$(dirname "$_target")"
|
|
269
|
+
[ -d "$_parent" ] || mkdir -p "$_parent"
|
|
270
|
+
|
|
271
|
+
rm -f "$_target"
|
|
272
|
+
ln -s "$_source" "$_target"
|
|
273
|
+
log_ok "Injected AI context: $_item → $(basename "$_repo_path")/$_item"
|
|
274
|
+
done
|
|
275
|
+
|
|
276
|
+
_gitignore_add "$_repo_path"
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
eject_ai_files() {
|
|
280
|
+
_name="$1"
|
|
281
|
+
_repo_path="$(yaml_get "$_name" "path")"
|
|
282
|
+
[ -n "$_repo_path" ] || die "No path recorded for repo '$_name'"
|
|
283
|
+
|
|
284
|
+
yaml_list_ai_files | while read -r _item; do
|
|
285
|
+
[ -n "$_item" ] || continue
|
|
286
|
+
_target="$_repo_path/$_item"
|
|
287
|
+
if [ -L "$_target" ]; then
|
|
288
|
+
rm -f "$_target"
|
|
289
|
+
log_ok "Ejected AI context: $_item from $(basename "$_repo_path")"
|
|
290
|
+
fi
|
|
291
|
+
done
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
_gitignore_add() {
|
|
295
|
+
_repo_path="$1"
|
|
296
|
+
_gi="$_repo_path/.gitignore"
|
|
297
|
+
_header_written=0
|
|
298
|
+
|
|
299
|
+
yaml_list_ai_files | while read -r _item; do
|
|
300
|
+
[ -n "$_item" ] || continue
|
|
301
|
+
if [ -f "$_gi" ] && grep -qxF "$_item" "$_gi" 2>/dev/null; then
|
|
302
|
+
continue
|
|
303
|
+
fi
|
|
304
|
+
if [ "$_header_written" -eq 0 ]; then
|
|
305
|
+
if [ ! -f "$_gi" ] || ! grep -q '# repo-nexus ai context' "$_gi" 2>/dev/null; then
|
|
306
|
+
printf '\n# repo-nexus ai context (injected symlinks)\n' >> "$_gi"
|
|
307
|
+
fi
|
|
308
|
+
_header_written=1
|
|
309
|
+
fi
|
|
310
|
+
printf '%s\n' "$_item" >> "$_gi"
|
|
311
|
+
log_dim "Added '$_item' to .gitignore in $(basename "$_repo_path")"
|
|
312
|
+
done
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
# ============================================================================
|
|
316
|
+
# Commands
|
|
317
|
+
# ============================================================================
|
|
318
|
+
|
|
319
|
+
# ---- Known AI config files for auto-detection ------------------------------
|
|
320
|
+
# Each entry: <file-or-dir-path>|<description>
|
|
321
|
+
_AI_KNOWN_FILES="
|
|
322
|
+
AGENTS.md|Universal AI agent instructions
|
|
323
|
+
CLAUDE.md|Claude Code instructions
|
|
324
|
+
.cursorrules|Cursor rules
|
|
325
|
+
.cursor/rules|Cursor rules directory
|
|
326
|
+
.windsurfrules|Windsurf rules
|
|
327
|
+
.windsurf/rules|Windsurf rules directory
|
|
328
|
+
.github/copilot-instructions.md|GitHub Copilot instructions
|
|
329
|
+
.aider.conf.yml|Aider configuration
|
|
330
|
+
CONVENTIONS.md|Coding conventions (Aider / general)
|
|
331
|
+
.clinerules|Cline / Roo Code rules
|
|
332
|
+
CODEX.md|Codex instructions
|
|
333
|
+
.continue|Continue.dev configuration
|
|
334
|
+
SKILL.md|Skills definition (emerging standard)
|
|
335
|
+
"
|
|
336
|
+
|
|
337
|
+
cmd_init() {
|
|
338
|
+
_target_dir="${1:-$PWD}"
|
|
339
|
+
_target_dir="$(resolve_path "$_target_dir")"
|
|
340
|
+
mkdir -p "$_target_dir"
|
|
341
|
+
|
|
342
|
+
_target_yaml="$_target_dir/$RNEX_CONFIG_FILE"
|
|
343
|
+
|
|
344
|
+
if [ -f "$_target_yaml" ]; then
|
|
345
|
+
log_warn "Workspace already initialized at $_target_dir ($RNEX_CONFIG_FILE exists)"
|
|
346
|
+
log_dim "To reconcile symlinks and AI files, run:"
|
|
347
|
+
log_dim " rnex sync"
|
|
348
|
+
return 0
|
|
349
|
+
fi
|
|
350
|
+
|
|
351
|
+
# ---- Create AGENTS.md from sample if not present -------------------------
|
|
352
|
+
# (done before scanning so it gets auto-detected)
|
|
353
|
+
_target_agents="$_target_dir/AGENTS.md"
|
|
354
|
+
if [ ! -f "$_target_agents" ]; then
|
|
355
|
+
_script_dir="$(dirname "$SCRIPT_LOCATION")"
|
|
356
|
+
_sample="$_script_dir/docs/AGENTS.sample.md"
|
|
357
|
+
if [ -f "$_sample" ]; then
|
|
358
|
+
cp "$_sample" "$_target_agents"
|
|
359
|
+
log_ok "Created AGENTS.md from sample template"
|
|
360
|
+
else
|
|
361
|
+
cat <<'AGENTS_EOF' > "$_target_agents"
|
|
362
|
+
# Multi-Repo AI Workspace Context
|
|
363
|
+
|
|
364
|
+
This workspace operates as a **Repo Nexus** linking multiple independent repositories via symlinks.
|
|
365
|
+
|
|
366
|
+
## Rules for AI Coding Assistants
|
|
367
|
+
|
|
368
|
+
1. **Symlink Write-Through**: Files edited under `repos/<name>/` directly modify the target repository.
|
|
369
|
+
2. **Repo Boundaries**: Each repository is isolated. Do not introduce cross-repo imports unless explicitly architected.
|
|
370
|
+
3. **Shared Context**: This file is symlinked across member repositories. Keep instructions universal. Use local instructions for repo-specific rules.
|
|
371
|
+
4. **Git Operations**: Commit and push changes directly within the respective member repository root.
|
|
372
|
+
AGENTS_EOF
|
|
373
|
+
log_ok "Created AGENTS.md with default template"
|
|
374
|
+
fi
|
|
375
|
+
else
|
|
376
|
+
log_info "AGENTS.md already exists, skipping creation"
|
|
377
|
+
fi
|
|
378
|
+
|
|
379
|
+
# ---- Auto-detect existing AI config files --------------------------------
|
|
380
|
+
printf "\n${_BOLD}Scanning for existing AI configuration files...${_NC}\n"
|
|
381
|
+
|
|
382
|
+
# Display detected files
|
|
383
|
+
echo "$_AI_KNOWN_FILES" | while IFS='|' read -r _pattern _desc; do
|
|
384
|
+
[ -n "$_pattern" ] || continue
|
|
385
|
+
if [ -e "$_target_dir/$_pattern" ]; then
|
|
386
|
+
printf " ${_G}✓${_NC} Found: ${_BOLD}%s${_NC} ${_DIM}(%s)${_NC}\n" "$_pattern" "$_desc"
|
|
387
|
+
fi
|
|
388
|
+
done
|
|
389
|
+
|
|
390
|
+
# Build ai_files list from detection
|
|
391
|
+
_ai_yaml_lines=""
|
|
392
|
+
_any_detected=0
|
|
393
|
+
for _pattern in $(echo "$_AI_KNOWN_FILES" | awk -F'|' '{ gsub(/^[ ]+/, "", $1); if (length($1) > 0) print $1 }'); do
|
|
394
|
+
if [ -e "$_target_dir/$_pattern" ]; then
|
|
395
|
+
_ai_yaml_lines="${_ai_yaml_lines} - ${_pattern}
|
|
396
|
+
"
|
|
397
|
+
_any_detected=1
|
|
398
|
+
fi
|
|
399
|
+
done
|
|
400
|
+
|
|
401
|
+
if [ "$_any_detected" -eq 0 ]; then
|
|
402
|
+
log_info "No existing AI config files detected."
|
|
403
|
+
_ai_yaml_lines=" - AGENTS.md
|
|
404
|
+
"
|
|
405
|
+
fi
|
|
406
|
+
|
|
407
|
+
# ---- Generate config file ------------------------------------------------
|
|
408
|
+
cat > "$_target_yaml" <<YAML_EOF
|
|
409
|
+
# Repo Nexus workspace configuration
|
|
410
|
+
# See docs/rnex.example.yaml for a comprehensive example with all options.
|
|
411
|
+
|
|
412
|
+
# Directory for repository symlinks (relative to this file or absolute)
|
|
413
|
+
repos_dir: ./repos
|
|
414
|
+
|
|
415
|
+
# AI context files to automatically sync into member repositories.
|
|
416
|
+
# These files are symlinked from this workspace root into each registered repo.
|
|
417
|
+
ai_files:
|
|
418
|
+
${_ai_yaml_lines}
|
|
419
|
+
# Member repositories managed by Repo Nexus
|
|
420
|
+
repos:
|
|
421
|
+
YAML_EOF
|
|
422
|
+
|
|
423
|
+
# ---- Create repos directory -----------------------------------------------
|
|
424
|
+
_repos_dir="$_target_dir/repos"
|
|
425
|
+
mkdir -p "$_repos_dir"
|
|
426
|
+
|
|
427
|
+
printf '\n'
|
|
428
|
+
log_ok "Initialized Repo Nexus workspace at ${_BOLD}$_target_dir${_NC}"
|
|
429
|
+
log_dim "Created $RNEX_CONFIG_FILE and repos/"
|
|
430
|
+
printf '\n'
|
|
431
|
+
log_info "Next steps:"
|
|
432
|
+
log_dim "1. Edit $RNEX_CONFIG_FILE to configure your AI context files"
|
|
433
|
+
log_dim "2. Add repositories with: rnex add <name> <path>"
|
|
434
|
+
log_dim "3. See docs/rnex.example.yaml for a full configuration reference"
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
cmd_add() {
|
|
438
|
+
require_workspace
|
|
439
|
+
_name="$1"; _path="$2"
|
|
440
|
+
[ -n "$_name" ] || die "Usage: rnex add <name> <path>"
|
|
441
|
+
[ -n "$_path" ] || die "Usage: rnex add <name> <path>"
|
|
442
|
+
|
|
443
|
+
yaml_has_repo "$_name" && die "Repo '$_name' is already registered in $RNEX_CONFIG_FILE"
|
|
444
|
+
|
|
445
|
+
_path="$(resolve_path "$_path")"
|
|
446
|
+
[ -d "$_path" ] || die "Directory does not exist: $_path"
|
|
447
|
+
|
|
448
|
+
ensure_repos_dir
|
|
449
|
+
yaml_add_repo "$_name" "$_path"
|
|
450
|
+
create_scope_link "$_name"
|
|
451
|
+
inject_ai_files "$_name"
|
|
452
|
+
printf '\n'
|
|
453
|
+
log_ok "Registered and synchronized repo '${_name}'"
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
cmd_remove() {
|
|
457
|
+
require_workspace
|
|
458
|
+
_name="$1"
|
|
459
|
+
[ -n "$_name" ] || die "Usage: rnex remove <name>"
|
|
460
|
+
yaml_has_repo "$_name" || die "Repo '$_name' is not registered"
|
|
461
|
+
|
|
462
|
+
eject_ai_files "$_name"
|
|
463
|
+
remove_scope_link "$_name"
|
|
464
|
+
yaml_remove_repo "$_name"
|
|
465
|
+
log_ok "Unregistered repo '$_name' from $RNEX_CONFIG_FILE"
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
cmd_show() {
|
|
469
|
+
require_workspace
|
|
470
|
+
_name="$1"
|
|
471
|
+
[ -n "$_name" ] || die "Usage: rnex show <name>"
|
|
472
|
+
yaml_has_repo "$_name" || die "Repo '$_name' is not registered"
|
|
473
|
+
|
|
474
|
+
ensure_repos_dir
|
|
475
|
+
create_scope_link "$_name"
|
|
476
|
+
yaml_set "$_name" "scope" "visible"
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
cmd_hide() {
|
|
480
|
+
require_workspace
|
|
481
|
+
_name="$1"
|
|
482
|
+
[ -n "$_name" ] || die "Usage: rnex hide <name>"
|
|
483
|
+
yaml_has_repo "$_name" || die "Repo '$_name' is not registered"
|
|
484
|
+
|
|
485
|
+
remove_scope_link "$_name"
|
|
486
|
+
yaml_set "$_name" "scope" "hidden"
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
cmd_list() {
|
|
490
|
+
require_workspace
|
|
491
|
+
printf "${_B}%-25s %-12s %s${_NC}\n" "NAME" "SCOPE" "PATH"
|
|
492
|
+
printf '%-25s %-12s %s\n' "----" "-----" "----"
|
|
493
|
+
yaml_list_repos | while read -r _name; do
|
|
494
|
+
_scope="$(yaml_get "$_name" "scope")"
|
|
495
|
+
_path="$(yaml_get "$_name" "path")"
|
|
496
|
+
printf '%-25s %-12s %s\n' \
|
|
497
|
+
"$_name" "${_scope:-visible}" "$_path"
|
|
498
|
+
done
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
cmd_status() {
|
|
502
|
+
require_workspace
|
|
503
|
+
ensure_repos_dir
|
|
504
|
+
|
|
505
|
+
printf "\n${_B}Repo Nexus Workspace Status${_NC}\n"
|
|
506
|
+
printf '=%.0s' 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
|
507
|
+
printf '\n\n'
|
|
508
|
+
|
|
509
|
+
printf "${_BOLD}Configured AI Context Files ($RNEX_CONFIG_FILE):${_NC}\n"
|
|
510
|
+
yaml_list_ai_files | while read -r _item; do
|
|
511
|
+
if [ -e "$NEXUS_DIR/$_item" ]; then
|
|
512
|
+
printf " ${_G}✓${_NC} %s\n" "$_item"
|
|
513
|
+
else
|
|
514
|
+
printf " ${_R}✗${_NC} %s (missing from nexus root)\n" "$_item"
|
|
515
|
+
fi
|
|
516
|
+
done
|
|
517
|
+
printf '\n'
|
|
518
|
+
|
|
519
|
+
printf "${_BOLD}Member Repositories:${_NC}\n"
|
|
520
|
+
if [ -z "$(yaml_list_repos)" ]; then
|
|
521
|
+
printf " ${_DIM}(No repositories registered yet. Use 'rnex add <name> <path>')${_NC}\n"
|
|
522
|
+
else
|
|
523
|
+
yaml_list_repos | while read -r _name; do
|
|
524
|
+
_scope="$(yaml_get "$_name" "scope")"
|
|
525
|
+
_path="$(yaml_get "$_name" "path")"
|
|
526
|
+
_link="$REPOS_DIR/$_name"
|
|
527
|
+
|
|
528
|
+
if [ "$_scope" = "hidden" ]; then
|
|
529
|
+
printf " ${_Y}○${_NC} %-22s ${_DIM}hidden${_NC}\n" "$_name"
|
|
530
|
+
elif [ -L "$_link" ] && [ -d "$_link" ]; then
|
|
531
|
+
printf " ${_G}●${_NC} %-22s ${_G}active${_NC} → %s\n" "$_name" "$_path"
|
|
532
|
+
elif [ -L "$_link" ]; then
|
|
533
|
+
printf " ${_R}✗${_NC} %-22s ${_R}BROKEN LINK${_NC} → %s\n" "$_name" "$_path"
|
|
534
|
+
else
|
|
535
|
+
printf " ${_R}✗${_NC} %-22s ${_R}MISSING LINK${_NC} (run 'rnex sync')\n" "$_name"
|
|
536
|
+
fi
|
|
537
|
+
done
|
|
538
|
+
fi
|
|
539
|
+
|
|
540
|
+
printf "\n${_DIM}Workspace: %s${_NC}\n" "$NEXUS_DIR"
|
|
541
|
+
printf "${_DIM}Repos Dir: %s${_NC}\n" "$REPOS_DIR"
|
|
542
|
+
printf "${_DIM}Config: %s${_NC}\n\n" "$WORKSPACE_YAML"
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
cmd_sync() {
|
|
546
|
+
require_workspace
|
|
547
|
+
ensure_repos_dir
|
|
548
|
+
|
|
549
|
+
log_info "Synchronizing workspace from $RNEX_CONFIG_FILE..."
|
|
550
|
+
yaml_list_repos | while read -r _name; do
|
|
551
|
+
_scope="$(yaml_get "$_name" "scope")"
|
|
552
|
+
|
|
553
|
+
if [ "$_scope" = "hidden" ]; then
|
|
554
|
+
remove_scope_link "$_name"
|
|
555
|
+
else
|
|
556
|
+
create_scope_link "$_name" || true
|
|
557
|
+
fi
|
|
558
|
+
|
|
559
|
+
inject_ai_files "$_name" || true
|
|
560
|
+
done
|
|
561
|
+
|
|
562
|
+
log_ok "Sync complete — all scope links and AI context files reconciled"
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
cmd_install() {
|
|
566
|
+
_target_bin="${1:-$HOME/.local/bin}"
|
|
567
|
+
_target_bin="$(resolve_path "$_target_bin")"
|
|
568
|
+
|
|
569
|
+
mkdir -p "$_target_bin" 2>/dev/null || true
|
|
570
|
+
if [ -d "$_target_bin" ] && [ -w "$_target_bin" ]; then
|
|
571
|
+
if [ -e "$_target_bin/rnex" ] && [ "$(resolve_script_path "$_target_bin/rnex" 2>/dev/null)" = "$SCRIPT_LOCATION" ]; then
|
|
572
|
+
log_ok "'rnex' is already installed at ${_BOLD}$_target_bin/rnex${_NC}"
|
|
573
|
+
else
|
|
574
|
+
rm -f "$_target_bin/rnex" "$_target_bin/repo-nexus"
|
|
575
|
+
ln -sf "$SCRIPT_LOCATION" "$_target_bin/rnex" 2>/dev/null || cp "$SCRIPT_LOCATION" "$_target_bin/rnex"
|
|
576
|
+
ln -sf "$SCRIPT_LOCATION" "$_target_bin/repo-nexus" 2>/dev/null || cp "$SCRIPT_LOCATION" "$_target_bin/repo-nexus"
|
|
577
|
+
chmod +x "$_target_bin/rnex" "$_target_bin/repo-nexus" 2>/dev/null || true
|
|
578
|
+
log_ok "Installed 'rnex' and 'repo-nexus' into ${_BOLD}$_target_bin${_NC}"
|
|
579
|
+
fi
|
|
580
|
+
else
|
|
581
|
+
log_warn "Cannot write to $_target_bin."
|
|
582
|
+
log_info "You can manually create an alias in your shell profile (~/.zshrc or ~/.bashrc):"
|
|
583
|
+
log_dim " alias rnex=\"$SCRIPT_LOCATION\""
|
|
584
|
+
log_dim " alias repo-nexus=\"$SCRIPT_LOCATION\""
|
|
585
|
+
return 1
|
|
586
|
+
fi
|
|
587
|
+
|
|
588
|
+
case ":$PATH:" in
|
|
589
|
+
*":$_target_bin:"*)
|
|
590
|
+
log_ok "$_target_bin is in your PATH"
|
|
591
|
+
;;
|
|
592
|
+
*)
|
|
593
|
+
log_warn "$_target_bin is not in your current PATH."
|
|
594
|
+
log_dim "Add this line to your shell profile (~/.zshrc or ~/.bashrc):"
|
|
595
|
+
log_dim " export PATH=\"$_target_bin:\$PATH\""
|
|
596
|
+
;;
|
|
597
|
+
esac
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
cmd_version() {
|
|
601
|
+
printf "rnex %s\n" "$RNEX_VERSION"
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
cmd_help() {
|
|
605
|
+
cat <<EOF
|
|
606
|
+
|
|
607
|
+
rnex — Repo Nexus CLI (v$RNEX_VERSION)
|
|
608
|
+
Tooling-independent multi-repo workspace manager with shared AI context.
|
|
609
|
+
|
|
610
|
+
USAGE
|
|
611
|
+
rnex [options] <command> [arguments]
|
|
612
|
+
|
|
613
|
+
OPTIONS
|
|
614
|
+
-c, --config <file> Path to $RNEX_CONFIG_FILE (executes in the directory where config lives)
|
|
615
|
+
-h, --help Show help information
|
|
616
|
+
-v, --version Show version
|
|
617
|
+
|
|
618
|
+
WORKSPACE & SYSTEM SETUP
|
|
619
|
+
init [dir] Initialize a new Repo Nexus workspace in current (or specified) dir
|
|
620
|
+
install [dir] Install rnex & repo-nexus globally into ~/.local/bin (or custom dir)
|
|
621
|
+
sync Reconcile scope links & AI files from $RNEX_CONFIG_FILE
|
|
622
|
+
|
|
623
|
+
REPO MANAGEMENT
|
|
624
|
+
add <name> <path> Register local repo, link to scope & auto-inject AI context
|
|
625
|
+
remove <name> Unregister repo, unlink scope & clean up injected AI files
|
|
626
|
+
list List all registered member repos
|
|
627
|
+
status Inspect workspace health, active repos, and AI context files
|
|
628
|
+
|
|
629
|
+
VISIBILITY CONTROL
|
|
630
|
+
show <name> Make repo visible in workspace (creates repos/<name> link)
|
|
631
|
+
hide <name> Hide repo from workspace scope (removes repos/<name> link)
|
|
632
|
+
|
|
633
|
+
EXAMPLES
|
|
634
|
+
rnex init # Initialize workspace in current folder
|
|
635
|
+
rnex install # Install rnex CLI globally to ~/.local/bin
|
|
636
|
+
rnex add my-app ~/code/my-app # Register repo (links & syncs AI context)
|
|
637
|
+
rnex add my-app ../my-app # Register repo (relative path)
|
|
638
|
+
rnex --config /path/to/rnex.yaml list # Operate on a specific workspace directly
|
|
639
|
+
rnex hide my-app # Temporarily hide from active indexing
|
|
640
|
+
rnex show my-app # Bring back into active scope
|
|
641
|
+
rnex sync # Reconcile all links from $RNEX_CONFIG_FILE
|
|
642
|
+
|
|
643
|
+
EOF
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
# ============================================================================
|
|
647
|
+
# Entry Point & Option Parsing
|
|
648
|
+
# ============================================================================
|
|
649
|
+
|
|
650
|
+
CUSTOM_CONFIG=""
|
|
651
|
+
[ -n "$RNEX_CONFIG" ] && CUSTOM_CONFIG="$RNEX_CONFIG"
|
|
652
|
+
|
|
653
|
+
# Filter --config / -c flags
|
|
654
|
+
ORIG_COUNT=$#
|
|
655
|
+
i=0
|
|
656
|
+
while [ "$i" -lt "$ORIG_COUNT" ]; do
|
|
657
|
+
i=$((i + 1))
|
|
658
|
+
case "$1" in
|
|
659
|
+
--config=*)
|
|
660
|
+
CUSTOM_CONFIG="${1#*=}"
|
|
661
|
+
shift
|
|
662
|
+
;;
|
|
663
|
+
--config|-c)
|
|
664
|
+
[ -n "$2" ] || die "Option $1 requires a file path argument"
|
|
665
|
+
CUSTOM_CONFIG="$2"
|
|
666
|
+
shift 2
|
|
667
|
+
i=$((i + 1))
|
|
668
|
+
;;
|
|
669
|
+
*)
|
|
670
|
+
_arg="$1"
|
|
671
|
+
shift
|
|
672
|
+
set -- "$@" "$_arg"
|
|
673
|
+
;;
|
|
674
|
+
esac
|
|
675
|
+
done
|
|
676
|
+
|
|
677
|
+
case "${1:-help}" in
|
|
678
|
+
init) shift; cmd_init "$@" ;;
|
|
679
|
+
install) shift; cmd_install "$@" ;;
|
|
680
|
+
add) shift; cmd_add "$@" ;;
|
|
681
|
+
remove) shift; cmd_remove "$@" ;;
|
|
682
|
+
show) shift; cmd_show "$@" ;;
|
|
683
|
+
hide) shift; cmd_hide "$@" ;;
|
|
684
|
+
list) shift; cmd_list "$@" ;;
|
|
685
|
+
status) shift; cmd_status "$@" ;;
|
|
686
|
+
sync) shift; cmd_sync "$@" ;;
|
|
687
|
+
version|-v|--version) cmd_version ;;
|
|
688
|
+
help|-h|--help) cmd_help ;;
|
|
689
|
+
*) die "Unknown command: $1 (try 'rnex help')" ;;
|
|
690
|
+
esac
|