rrce-workflow 0.2.8 → 0.2.10
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/README.md +157 -48
- package/agent-core/prompts/documentation.md +8 -0
- package/agent-core/prompts/executor.md +8 -0
- package/agent-core/prompts/init.md +8 -0
- package/agent-core/prompts/planning_orchestrator.md +8 -0
- package/agent-core/prompts/research_discussion.md +8 -0
- package/agent-core/prompts/sync.md +8 -0
- package/bin/rrce-workflow.js +9 -2
- package/package.json +10 -8
- package/src/commands/wizard/link-flow.ts +32 -15
- package/src/commands/wizard/setup-flow.ts +9 -10
- package/src/commands/wizard/vscode.ts +153 -22
- package/src/lib/autocomplete-prompt.ts +190 -0
- package/src/lib/detection.ts +235 -0
- package/src/lib/prompts.ts +9 -2
package/README.md
CHANGED
|
@@ -5,89 +5,198 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/rrce-workflow)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
RRCE-Workflow is a
|
|
8
|
+
RRCE-Workflow is a CLI wizard that sets up AI agent prompts and workflows for your codebase. It works with **GitHub Copilot**, **Antigravity IDE**, and other AI coding assistants.
|
|
9
9
|
|
|
10
10
|
## Installation
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
#
|
|
13
|
+
# Quick start (no install needed)
|
|
14
14
|
npx rrce-workflow
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
bunx rrce-workflow
|
|
18
|
-
|
|
19
|
-
# Global install
|
|
16
|
+
# Or install globally
|
|
20
17
|
npm install -g rrce-workflow
|
|
21
18
|
```
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## How to Use
|
|
23
|
+
|
|
24
|
+
### 1. Initial Setup
|
|
25
|
+
|
|
26
|
+
Run the wizard in your project directory:
|
|
24
27
|
|
|
25
28
|
```bash
|
|
26
|
-
|
|
27
|
-
rrce-workflow
|
|
29
|
+
cd your-project
|
|
30
|
+
npx rrce-workflow
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The wizard will:
|
|
34
|
+
1. Ask where to store workflow data (global, workspace, or both)
|
|
35
|
+
2. Let you choose a custom global path if the default isn't writable
|
|
36
|
+
3. Ask which AI tools you use (GitHub Copilot, Antigravity)
|
|
37
|
+
4. Set up prompts and knowledge folders
|
|
38
|
+
|
|
39
|
+
### 2. Using the Agent Prompts
|
|
28
40
|
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
After setup, you'll have agent prompts in IDE-specific folders:
|
|
42
|
+
|
|
43
|
+
- **GitHub Copilot**: `.github/agents/*.agent.md`
|
|
44
|
+
- **Antigravity**: `.agent/workflows/*.md`
|
|
45
|
+
|
|
46
|
+
In your AI assistant, invoke prompts using their names:
|
|
47
|
+
|
|
48
|
+
| Agent | Invoke With | What It Does |
|
|
49
|
+
|-------|-------------|--------------|
|
|
50
|
+
| **Init** | `/init` | Analyze your codebase and create `project-context.md` |
|
|
51
|
+
| **Research** | `/research REQUEST="..." TASK_SLUG=my-task` | Clarify requirements, create research brief |
|
|
52
|
+
| **Planning** | `/plan TASK_SLUG=my-task` | Create actionable execution plan |
|
|
53
|
+
| **Execute** | `/execute TASK_SLUG=my-task` | Implement the planned work |
|
|
54
|
+
| **Docs** | `/docs DOC_TYPE=architecture` | Generate documentation |
|
|
55
|
+
| **Sync** | `/sync` | Update knowledge base after code changes |
|
|
56
|
+
|
|
57
|
+
### 3. Recommended Workflow (RRCE Pipeline)
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
1. /init → Establish project context
|
|
61
|
+
2. /research → Clarify requirements for a new task
|
|
62
|
+
3. /plan → Create execution plan
|
|
63
|
+
4. /execute → Implement the plan
|
|
64
|
+
5. /docs → Generate documentation (optional)
|
|
65
|
+
6. /sync → Keep knowledge base current (periodic)
|
|
31
66
|
```
|
|
32
67
|
|
|
33
|
-
|
|
68
|
+
#### Pipeline Stages Explained
|
|
34
69
|
|
|
35
|
-
|
|
36
|
-
- **Agent Prompts** - Pre-built prompts for init, research, planning, execution, documentation, and sync
|
|
37
|
-
- **Multi-Tool Support** - Works with GitHub Copilot (`.agent.md`) and Antigravity IDE
|
|
38
|
-
- **Cross-Project References** - Reference context from related projects
|
|
70
|
+
**🔍 Init** — Scans your codebase to understand tech stack, architecture, coding conventions, and project structure. Creates `project-context.md` that all other agents rely on. Run once at project start, and again when major changes occur.
|
|
39
71
|
|
|
40
|
-
|
|
72
|
+
**💬 Research** — Entry point for new tasks. Takes a user request and engages in clarifying discussion to refine scope, surface risks, and identify gaps. Produces a research brief for the Planning agent.
|
|
41
73
|
|
|
42
|
-
|
|
43
|
-
|-------|---------|-------------|
|
|
44
|
-
| **Init** | `/init` | Initialize or update project context |
|
|
45
|
-
| **Research** | `/research` | Clarify requirements and create research brief |
|
|
46
|
-
| **Planning** | `/plan` | Transform requirements into execution plan |
|
|
47
|
-
| **Executor** | `/execute` | Implement the planned tasks |
|
|
48
|
-
| **Documentation** | `/docs` | Generate project documentation |
|
|
49
|
-
| **Sync** | `/sync` | Reconcile codebase with knowledge base |
|
|
74
|
+
**📋 Planning** — Transforms the research brief into an ordered, actionable execution plan. Breaks work into tasks with dependencies, acceptance criteria, and testing strategy. Ensures the Executor has clear guidance.
|
|
50
75
|
|
|
51
|
-
|
|
76
|
+
**⚡ Execute** — Implements the planned work. Writes code, adds tests, runs verifications. Updates task metadata and logs execution notes for auditability.
|
|
52
77
|
|
|
53
|
-
|
|
78
|
+
**📄 Docs** — Synthesizes the completed work into documentation. Can generate API docs, architecture overviews, runbooks, or changelogs based on `DOC_TYPE`.
|
|
54
79
|
|
|
55
|
-
|
|
56
|
-
version: 1
|
|
80
|
+
**🔄 Sync** — Maintenance agent that reconciles the knowledge base with actual code. Run periodically to catch drift and keep documentation accurate.
|
|
57
81
|
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## How It Works
|
|
85
|
+
|
|
86
|
+
### Path Resolution
|
|
87
|
+
|
|
88
|
+
All agents read `.rrce-workflow/config.yaml` to resolve paths:
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
58
91
|
storage:
|
|
59
|
-
mode:
|
|
92
|
+
mode: workspace # or: global, both
|
|
93
|
+
globalPath: "~/.rrce-workflow" # optional custom path
|
|
60
94
|
|
|
61
95
|
project:
|
|
62
96
|
name: "my-project"
|
|
63
97
|
```
|
|
64
98
|
|
|
65
|
-
|
|
99
|
+
Agents resolve `{{RRCE_DATA}}` based on storage mode:
|
|
100
|
+
- `workspace` → `.rrce-workflow/`
|
|
101
|
+
- `global` → `~/.rrce-workflow/workspaces/my-project/`
|
|
102
|
+
- `both` → `.rrce-workflow/` (primary, synced to global)
|
|
66
103
|
|
|
67
|
-
|
|
68
|
-
|------|----------|----------|
|
|
69
|
-
| `global` | `~/.rrce-workflow/workspaces/<name>/` | Non-intrusive, cross-project access |
|
|
70
|
-
| `workspace` | `.rrce-workflow/` | Portable with repo, team sharing |
|
|
71
|
-
| `both` | Both locations (synced) | Redundancy + cross-project access |
|
|
104
|
+
### Cross-Project References
|
|
72
105
|
|
|
73
|
-
When `both`
|
|
74
|
-
- Primary (for reads): `<workspace>/.rrce-workflow/`
|
|
75
|
-
- Secondary (auto-synced): `~/.rrce-workflow/workspaces/<name>/`
|
|
106
|
+
When using `global` or `both` mode, you can reference other projects:
|
|
76
107
|
|
|
77
|
-
Each storage location contains:
|
|
78
108
|
```
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
109
|
+
~/.rrce-workflow/workspaces/other-project/knowledge/project-context.md
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This enables a frontend app to reference its backend API's knowledge!
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Folder Structure
|
|
117
|
+
|
|
118
|
+
After setup, your project will have:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
your-project/
|
|
122
|
+
├── .rrce-workflow/ # Data storage
|
|
123
|
+
│ ├── config.yaml # Configuration
|
|
124
|
+
│ ├── knowledge/ # Project context
|
|
125
|
+
│ ├── refs/ # External references
|
|
126
|
+
│ ├── tasks/ # Task artifacts by slug
|
|
127
|
+
│ └── templates/ # Output templates
|
|
128
|
+
├── .github/agents/ # GitHub Copilot prompts
|
|
129
|
+
│ └── *.agent.md
|
|
130
|
+
└── .agent/workflows/ # Antigravity prompts
|
|
131
|
+
└── *.md
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Wizard Options
|
|
137
|
+
|
|
138
|
+
When you run the wizard on an already-configured project, you'll see:
|
|
139
|
+
|
|
140
|
+
| Option | Description |
|
|
141
|
+
|--------|-------------|
|
|
142
|
+
| **Link other project knowledge** | Reference knowledge from other projects in global storage |
|
|
143
|
+
| **Sync to global storage** | Copy workspace data to global (enables cross-project access) |
|
|
144
|
+
| **Update from package** | Get latest prompts and templates |
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Storage Mode Comparison
|
|
149
|
+
|
|
150
|
+
| Mode | Location | Best For |
|
|
151
|
+
|------|----------|----------|
|
|
152
|
+
| `global` | `~/.rrce-workflow/workspaces/<name>/` | Clean workspace, cross-project references |
|
|
153
|
+
| `workspace` | `.rrce-workflow/` | Team sharing, portable with repo |
|
|
154
|
+
| `both` | Both locations (synced) | Full redundancy + cross-project access |
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Custom Global Path
|
|
159
|
+
|
|
160
|
+
If the default `~/.rrce-workflow` isn't writable (common with `npx` in enterprise environments), the wizard lets you choose a custom location:
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
storage:
|
|
164
|
+
mode: global
|
|
165
|
+
globalPath: "/path/to/custom/rrce-workflow"
|
|
85
166
|
```
|
|
86
167
|
|
|
168
|
+
---
|
|
169
|
+
|
|
87
170
|
## Requirements
|
|
88
171
|
|
|
89
|
-
- Node.js 18
|
|
90
|
-
- Git (for user detection)
|
|
172
|
+
- **Node.js 18+**
|
|
173
|
+
- **Git** (for user detection)
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Troubleshooting
|
|
178
|
+
|
|
179
|
+
### "Permission denied" when setting up
|
|
180
|
+
|
|
181
|
+
If you can't write to `~/.rrce-workflow`, the wizard will prompt you to choose a custom path. You can also set it manually:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
export RRCE_HOME=/path/to/writable/location
|
|
185
|
+
npx rrce-workflow
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Agents can't find data files
|
|
189
|
+
|
|
190
|
+
Make sure the agent reads `.rrce-workflow/config.yaml` first. All prompts include a mandatory first step to resolve paths from the config.
|
|
191
|
+
|
|
192
|
+
### Updating prompts after package update
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
npx rrce-workflow
|
|
196
|
+
# Select "Update from package"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
91
200
|
|
|
92
201
|
## License
|
|
93
202
|
|
|
@@ -20,6 +20,14 @@ auto-identity:
|
|
|
20
20
|
|
|
21
21
|
You are the Documentation Lead for the project. Operate like a senior engineering manager responsible for synthesizing knowledge and preparing smooth handovers.
|
|
22
22
|
|
|
23
|
+
**⚠️ FIRST STEP (MANDATORY)**
|
|
24
|
+
Before doing ANY work, read `.rrce-workflow/config.yaml` and resolve these variables:
|
|
25
|
+
```
|
|
26
|
+
RRCE_HOME = config.storage.globalPath OR "~/.rrce-workflow"
|
|
27
|
+
RRCE_DATA = (config.storage.mode == "workspace" or "both") ? ".rrce-workflow/" : "${RRCE_HOME}/workspaces/${config.project.name}/"
|
|
28
|
+
```
|
|
29
|
+
Use these resolved paths for ALL subsequent file operations.
|
|
30
|
+
|
|
23
31
|
Pipeline Position
|
|
24
32
|
- **Optional**: Documentation can be run at any point, but is most valuable after Execution.
|
|
25
33
|
- **Best After**: Executor phase complete (if documenting a specific task).
|
|
@@ -16,6 +16,14 @@ auto-identity:
|
|
|
16
16
|
|
|
17
17
|
You are the Executor for the project. Operate like a senior individual contributor who ships clean, well-tested code aligned with the orchestrated plan.
|
|
18
18
|
|
|
19
|
+
**⚠️ FIRST STEP (MANDATORY)**
|
|
20
|
+
Before doing ANY work, read `.rrce-workflow/config.yaml` and resolve these variables:
|
|
21
|
+
```
|
|
22
|
+
RRCE_HOME = config.storage.globalPath OR "~/.rrce-workflow"
|
|
23
|
+
RRCE_DATA = (config.storage.mode == "workspace" or "both") ? ".rrce-workflow/" : "${RRCE_HOME}/workspaces/${config.project.name}/"
|
|
24
|
+
```
|
|
25
|
+
Use these resolved paths for ALL subsequent file operations.
|
|
26
|
+
|
|
19
27
|
Pipeline Position
|
|
20
28
|
- **Requires**: Planning phase must be complete before execution can begin.
|
|
21
29
|
- **Next Step**: After execution is complete, optionally hand off to `/docs` (Documentation agent).
|
|
@@ -15,6 +15,14 @@ auto-identity:
|
|
|
15
15
|
|
|
16
16
|
You are the Project Initializer for RRCE-Workflow. Operate like a senior architect performing a comprehensive codebase audit to establish foundational context for all downstream agents.
|
|
17
17
|
|
|
18
|
+
**⚠️ FIRST STEP (MANDATORY)**
|
|
19
|
+
Before doing ANY work, read `.rrce-workflow/config.yaml` (if it exists) and resolve these variables:
|
|
20
|
+
```
|
|
21
|
+
RRCE_HOME = config.storage.globalPath OR "~/.rrce-workflow"
|
|
22
|
+
RRCE_DATA = (config.storage.mode == "workspace" or "both") ? ".rrce-workflow/" : "${RRCE_HOME}/workspaces/${config.project.name}/"
|
|
23
|
+
```
|
|
24
|
+
If config doesn't exist yet (new project), use defaults: `RRCE_HOME=~/.rrce-workflow`, `RRCE_DATA=.rrce-workflow/`
|
|
25
|
+
|
|
18
26
|
Pipeline Position
|
|
19
27
|
- **Entry Point**: Init can be run at any time to establish or update project context.
|
|
20
28
|
- **Correlation**: Init and Planning work together to maintain project context. Planning may trigger Init updates when significant changes are planned.
|
|
@@ -13,6 +13,14 @@ auto-identity:
|
|
|
13
13
|
|
|
14
14
|
You are the Planning & Task Orchestrator for the project. Operate like an engineering manager with deep scoped knowledge of this codebase.
|
|
15
15
|
|
|
16
|
+
**⚠️ FIRST STEP (MANDATORY)**
|
|
17
|
+
Before doing ANY work, read `.rrce-workflow/config.yaml` and resolve these variables:
|
|
18
|
+
```
|
|
19
|
+
RRCE_HOME = config.storage.globalPath OR "~/.rrce-workflow"
|
|
20
|
+
RRCE_DATA = (config.storage.mode == "workspace" or "both") ? ".rrce-workflow/" : "${RRCE_HOME}/workspaces/${config.project.name}/"
|
|
21
|
+
```
|
|
22
|
+
Use these resolved paths for ALL subsequent file operations.
|
|
23
|
+
|
|
16
24
|
Pipeline Position
|
|
17
25
|
- **Requires**: Research phase must be complete before planning can begin.
|
|
18
26
|
- **Correlation**: Planning works with Init to maintain project context. If planning reveals significant architectural changes, recommend running `/init` to update project context.
|
|
@@ -20,6 +20,14 @@ auto-identity:
|
|
|
20
20
|
|
|
21
21
|
You are the Research & Discussion Lead for the project. Operate like a staff-level tech lead who owns broad project awareness.
|
|
22
22
|
|
|
23
|
+
**⚠️ FIRST STEP (MANDATORY)**
|
|
24
|
+
Before doing ANY work, read `.rrce-workflow/config.yaml` and resolve these variables:
|
|
25
|
+
```
|
|
26
|
+
RRCE_HOME = config.storage.globalPath OR "~/.rrce-workflow"
|
|
27
|
+
RRCE_DATA = (config.storage.mode == "workspace" or "both") ? ".rrce-workflow/" : "${RRCE_HOME}/workspaces/${config.project.name}/"
|
|
28
|
+
```
|
|
29
|
+
Use these resolved paths for ALL subsequent file operations.
|
|
30
|
+
|
|
23
31
|
Pipeline Position
|
|
24
32
|
- **Entry Point**: Research can be the first agent invoked for a new task.
|
|
25
33
|
- **Recommendation**: If `{{RRCE_DATA}}/knowledge/project-context.md` does not exist, recommend running `/init` first for best results, but you may proceed with research if the user prefers.
|
|
@@ -14,6 +14,14 @@ auto-identity:
|
|
|
14
14
|
|
|
15
15
|
You are the Knowledge Sync Lead. Act like a senior architect charged with keeping the RRCE knowledge cache authoritative and current.
|
|
16
16
|
|
|
17
|
+
**⚠️ FIRST STEP (MANDATORY)**
|
|
18
|
+
Before doing ANY work, read `.rrce-workflow/config.yaml` and resolve these variables:
|
|
19
|
+
```
|
|
20
|
+
RRCE_HOME = config.storage.globalPath OR "~/.rrce-workflow"
|
|
21
|
+
RRCE_DATA = (config.storage.mode == "workspace" or "both") ? ".rrce-workflow/" : "${RRCE_HOME}/workspaces/${config.project.name}/"
|
|
22
|
+
```
|
|
23
|
+
Use these resolved paths for ALL subsequent file operations.
|
|
24
|
+
|
|
17
25
|
Pipeline Position
|
|
18
26
|
- **Maintenance Agent**: Sync runs periodically or after significant codebase changes to keep knowledge current.
|
|
19
27
|
- **Requires**: Init must have been run at least once (project-context.md must exist).
|
package/bin/rrce-workflow.js
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
import
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { register } from 'node:module';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
// Register tsx for TypeScript support
|
|
6
|
+
register('tsx/esm', pathToFileURL('./'));
|
|
7
|
+
|
|
8
|
+
// Import and run the main module
|
|
9
|
+
import('../src/index.ts');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rrce-workflow",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "RRCE-Workflow TUI - Agentic code workflow generator for AI-assisted development",
|
|
5
5
|
"author": "RRCE Team",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,22 +35,24 @@
|
|
|
35
35
|
"bin"
|
|
36
36
|
],
|
|
37
37
|
"scripts": {
|
|
38
|
-
"dev": "
|
|
39
|
-
"wizard": "
|
|
40
|
-
"select": "
|
|
41
|
-
"start": "
|
|
38
|
+
"dev": "npx tsx src/index.ts",
|
|
39
|
+
"wizard": "npx tsx src/index.ts wizard",
|
|
40
|
+
"select": "npx tsx src/index.ts select",
|
|
41
|
+
"start": "npx tsx src/index.ts"
|
|
42
42
|
},
|
|
43
43
|
"engines": {
|
|
44
|
-
"node": ">=18"
|
|
45
|
-
"bun": ">=1.0"
|
|
44
|
+
"node": ">=18"
|
|
46
45
|
},
|
|
47
46
|
"dependencies": {
|
|
47
|
+
"@clack/core": "^0.5.0",
|
|
48
48
|
"@clack/prompts": "^0.11.0",
|
|
49
49
|
"gray-matter": "^4.0.3",
|
|
50
50
|
"picocolors": "^1.1.1",
|
|
51
|
+
"tsx": "^4.21.0",
|
|
51
52
|
"zod": "^4.2.1"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
|
-
"@types/
|
|
55
|
+
"@types/node": "^25.0.3",
|
|
56
|
+
"typescript": "^5.9.3"
|
|
55
57
|
}
|
|
56
58
|
}
|
|
@@ -1,33 +1,45 @@
|
|
|
1
1
|
import { multiselect, spinner, note, outro, cancel, isCancel } from '@clack/prompts';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
|
-
import {
|
|
4
|
+
import { getEffectiveRRCEHome, getConfigPath } from '../../lib/paths';
|
|
5
5
|
import { generateVSCodeWorkspace } from './vscode';
|
|
6
|
+
import { scanForProjects, getProjectDisplayLabel, type DetectedProject } from '../../lib/detection';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Run the link-only flow for adding other project knowledge to an existing workspace
|
|
10
|
+
* Now supports detecting workspace-scoped sibling projects, not just global storage
|
|
9
11
|
*/
|
|
10
12
|
export async function runLinkProjectsFlow(
|
|
11
13
|
workspacePath: string,
|
|
12
14
|
workspaceName: string,
|
|
13
15
|
existingProjects?: string[]
|
|
14
16
|
) {
|
|
15
|
-
//
|
|
16
|
-
const
|
|
17
|
+
// Scan for projects using the new detection system
|
|
18
|
+
const detectedProjects = scanForProjects({
|
|
19
|
+
excludeWorkspace: workspaceName,
|
|
20
|
+
workspacePath: workspacePath,
|
|
21
|
+
scanSiblings: true,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// If legacy string array is passed, use that instead (for backwards compat)
|
|
25
|
+
const projects = existingProjects
|
|
26
|
+
? existingProjects.map(name => ({ name, source: 'global' as const } as DetectedProject))
|
|
27
|
+
: detectedProjects;
|
|
17
28
|
|
|
18
29
|
if (projects.length === 0) {
|
|
19
|
-
outro(pc.yellow('No other projects found
|
|
30
|
+
outro(pc.yellow('No other projects found. Try setting up another project first.'));
|
|
20
31
|
return;
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
const customGlobalPath = getEffectiveRRCEHome(workspacePath);
|
|
24
35
|
|
|
36
|
+
// Build options with source labels
|
|
25
37
|
const linkedProjects = await multiselect({
|
|
26
38
|
message: 'Select projects to link:',
|
|
27
|
-
options: projects.map(
|
|
28
|
-
value: name,
|
|
29
|
-
label: name,
|
|
30
|
-
hint:
|
|
39
|
+
options: projects.map(project => ({
|
|
40
|
+
value: project.name,
|
|
41
|
+
label: project.name,
|
|
42
|
+
hint: pc.dim(getProjectDisplayLabel(project)),
|
|
31
43
|
})),
|
|
32
44
|
required: true,
|
|
33
45
|
});
|
|
@@ -37,13 +49,16 @@ export async function runLinkProjectsFlow(
|
|
|
37
49
|
process.exit(0);
|
|
38
50
|
}
|
|
39
51
|
|
|
40
|
-
const
|
|
52
|
+
const selectedProjectNames = linkedProjects as string[];
|
|
41
53
|
|
|
42
|
-
if (
|
|
54
|
+
if (selectedProjectNames.length === 0) {
|
|
43
55
|
outro('No projects selected.');
|
|
44
56
|
return;
|
|
45
57
|
}
|
|
46
58
|
|
|
59
|
+
// Get the full DetectedProject objects for selected projects
|
|
60
|
+
const selectedProjects = projects.filter(p => selectedProjectNames.includes(p.name));
|
|
61
|
+
|
|
47
62
|
const s = spinner();
|
|
48
63
|
s.start('Linking projects');
|
|
49
64
|
|
|
@@ -63,7 +78,7 @@ export async function runLinkProjectsFlow(
|
|
|
63
78
|
insertIndex++;
|
|
64
79
|
}
|
|
65
80
|
// Add new projects that aren't already there
|
|
66
|
-
for (const name of
|
|
81
|
+
for (const name of selectedProjectNames) {
|
|
67
82
|
if (!configContent.includes(` - ${name}`)) {
|
|
68
83
|
lines.splice(insertIndex, 0, ` - ${name}`);
|
|
69
84
|
insertIndex++;
|
|
@@ -74,25 +89,27 @@ export async function runLinkProjectsFlow(
|
|
|
74
89
|
} else {
|
|
75
90
|
// Add new linked_projects section
|
|
76
91
|
configContent += `\nlinked_projects:\n`;
|
|
77
|
-
|
|
92
|
+
selectedProjectNames.forEach(name => {
|
|
78
93
|
configContent += ` - ${name}\n`;
|
|
79
94
|
});
|
|
80
95
|
}
|
|
81
96
|
|
|
82
97
|
fs.writeFileSync(configFilePath, configContent);
|
|
83
98
|
|
|
84
|
-
// Update VSCode workspace file
|
|
99
|
+
// Update VSCode workspace file with full project info (includes refs, tasks)
|
|
85
100
|
generateVSCodeWorkspace(workspacePath, workspaceName, selectedProjects, customGlobalPath);
|
|
86
101
|
|
|
87
102
|
s.stop('Projects linked');
|
|
88
103
|
|
|
89
|
-
// Show summary
|
|
104
|
+
// Show summary with project sources
|
|
90
105
|
const workspaceFile = `${workspaceName}.code-workspace`;
|
|
91
106
|
const summary = [
|
|
92
107
|
`Linked projects:`,
|
|
93
|
-
...selectedProjects.map(p => ` ✓ ${p}`),
|
|
108
|
+
...selectedProjects.map(p => ` ✓ ${p.name} ${pc.dim(`(${p.source})`)}`),
|
|
94
109
|
``,
|
|
95
110
|
`Workspace file: ${pc.cyan(workspaceFile)}`,
|
|
111
|
+
``,
|
|
112
|
+
pc.dim('Includes: knowledge, refs, tasks folders'),
|
|
96
113
|
];
|
|
97
114
|
|
|
98
115
|
note(summary.join('\n'), 'Link Summary');
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { loadPromptsFromDir, getAgentCorePromptsDir, getAgentCoreDir } from '../../lib/prompts';
|
|
15
15
|
import { copyPromptsToDir } from './utils';
|
|
16
16
|
import { generateVSCodeWorkspace } from './vscode';
|
|
17
|
+
import { directoryAutocomplete, isCancel as isAutocompleteCancel } from '../../lib/autocomplete-prompt';
|
|
17
18
|
|
|
18
19
|
interface SetupConfig {
|
|
19
20
|
storageMode: StorageMode;
|
|
@@ -206,10 +207,12 @@ async function resolveGlobalPath(): Promise<string | undefined> {
|
|
|
206
207
|
return defaultPath;
|
|
207
208
|
}
|
|
208
209
|
|
|
209
|
-
// Custom path input
|
|
210
|
-
const
|
|
210
|
+
// Custom path input with Tab autocomplete
|
|
211
|
+
const suggestedPath = path.join(process.env.HOME || '~', '.local', 'share', 'rrce-workflow');
|
|
212
|
+
const customPath = await directoryAutocomplete({
|
|
211
213
|
message: 'Enter custom global path:',
|
|
212
|
-
|
|
214
|
+
initialValue: suggestedPath,
|
|
215
|
+
hint: 'Tab to autocomplete',
|
|
213
216
|
validate: (value) => {
|
|
214
217
|
if (!value.trim()) {
|
|
215
218
|
return 'Path cannot be empty';
|
|
@@ -226,16 +229,12 @@ async function resolveGlobalPath(): Promise<string | undefined> {
|
|
|
226
229
|
},
|
|
227
230
|
});
|
|
228
231
|
|
|
229
|
-
if (
|
|
232
|
+
if (isAutocompleteCancel(customPath)) {
|
|
230
233
|
return undefined;
|
|
231
234
|
}
|
|
232
235
|
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
? (customPath as string).replace('~', process.env.HOME || '')
|
|
236
|
-
: customPath as string;
|
|
237
|
-
|
|
238
|
-
return expandedPath;
|
|
236
|
+
// Path is already expanded by directoryAutocomplete
|
|
237
|
+
return customPath as string;
|
|
239
238
|
}
|
|
240
239
|
|
|
241
240
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { getRRCEHome } from '../../lib/paths';
|
|
4
|
+
import { type DetectedProject, getProjectFolders } from '../../lib/detection';
|
|
4
5
|
|
|
5
6
|
interface VSCodeWorkspaceFolder {
|
|
6
7
|
path: string;
|
|
@@ -12,13 +13,22 @@ interface VSCodeWorkspace {
|
|
|
12
13
|
settings?: Record<string, unknown>;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
// Reference folder group prefix - used to visually group linked folders
|
|
17
|
+
const REFERENCE_GROUP_PREFIX = '📁 References';
|
|
18
|
+
|
|
15
19
|
/**
|
|
16
|
-
* Generate or update VSCode workspace file with linked project
|
|
20
|
+
* Generate or update VSCode workspace file with linked project folders
|
|
21
|
+
*
|
|
22
|
+
* Features:
|
|
23
|
+
* - Main workspace is clearly marked as the primary project
|
|
24
|
+
* - Linked folders are grouped under a "References" section (via naming)
|
|
25
|
+
* - Folders are organized by project with icons for type (📚 📎 📋)
|
|
26
|
+
* - Reference folders are marked as readonly in workspace settings
|
|
17
27
|
*/
|
|
18
28
|
export function generateVSCodeWorkspace(
|
|
19
29
|
workspacePath: string,
|
|
20
30
|
workspaceName: string,
|
|
21
|
-
linkedProjects: string[],
|
|
31
|
+
linkedProjects: string[] | DetectedProject[],
|
|
22
32
|
customGlobalPath?: string
|
|
23
33
|
) {
|
|
24
34
|
const workspaceFilePath = path.join(workspacePath, `${workspaceName}.code-workspace`);
|
|
@@ -32,35 +42,156 @@ export function generateVSCodeWorkspace(
|
|
|
32
42
|
workspace = JSON.parse(content);
|
|
33
43
|
} catch {
|
|
34
44
|
// If parse fails, create new
|
|
35
|
-
workspace = { folders: [] };
|
|
45
|
+
workspace = { folders: [], settings: {} };
|
|
36
46
|
}
|
|
37
47
|
} else {
|
|
38
|
-
workspace = { folders: [] };
|
|
48
|
+
workspace = { folders: [], settings: {} };
|
|
39
49
|
}
|
|
40
50
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (existingMainIndex === -1) {
|
|
45
|
-
workspace.folders.unshift(mainFolder);
|
|
51
|
+
// Initialize settings if not present
|
|
52
|
+
if (!workspace.settings) {
|
|
53
|
+
workspace.settings = {};
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
//
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
// Clear existing folders and rebuild (to ensure proper ordering)
|
|
57
|
+
const existingNonReferencesFolders = workspace.folders.filter(f =>
|
|
58
|
+
f.path === '.' || (!f.name?.includes(REFERENCE_GROUP_PREFIX) && !f.name?.startsWith('📚') && !f.name?.startsWith('📎') && !f.name?.startsWith('📋'))
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
workspace.folders = [];
|
|
62
|
+
|
|
63
|
+
// 1. Add main workspace folder first with clear label
|
|
64
|
+
const mainFolder: VSCodeWorkspaceFolder = {
|
|
65
|
+
path: '.',
|
|
66
|
+
name: `🏠 ${workspaceName} (workspace)`
|
|
67
|
+
};
|
|
68
|
+
workspace.folders.push(mainFolder);
|
|
69
|
+
|
|
70
|
+
// 2. Add any other existing non-references folders
|
|
71
|
+
for (const folder of existingNonReferencesFolders) {
|
|
72
|
+
if (folder.path !== '.') {
|
|
73
|
+
workspace.folders.push(folder);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
56
76
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
77
|
+
// 3. Add reference folders grouped by project
|
|
78
|
+
const referenceFolderPaths: string[] = [];
|
|
79
|
+
|
|
80
|
+
// Determine if we're working with DetectedProject[] or string[]
|
|
81
|
+
const isDetectedProjects = linkedProjects.length > 0 && typeof linkedProjects[0] === 'object';
|
|
82
|
+
|
|
83
|
+
if (isDetectedProjects) {
|
|
84
|
+
// New behavior: use DetectedProject[] with knowledge, refs, tasks folders
|
|
85
|
+
const projects = linkedProjects as DetectedProject[];
|
|
86
|
+
|
|
87
|
+
for (const project of projects) {
|
|
88
|
+
const folders = getProjectFolders(project);
|
|
89
|
+
const sourceLabel = project.source === 'global' ? 'global' : 'local';
|
|
90
|
+
|
|
91
|
+
for (const folder of folders) {
|
|
92
|
+
referenceFolderPaths.push(folder.path);
|
|
93
|
+
|
|
94
|
+
// Check if already exists
|
|
95
|
+
const existingIndex = workspace.folders.findIndex(f => f.path === folder.path);
|
|
96
|
+
if (existingIndex === -1) {
|
|
97
|
+
workspace.folders.push({
|
|
98
|
+
path: folder.path,
|
|
99
|
+
name: `${folder.displayName} [${sourceLabel}]`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
// Legacy behavior: string[] of project names (global storage only)
|
|
106
|
+
const projectNames = linkedProjects as string[];
|
|
107
|
+
const rrceHome = customGlobalPath || getRRCEHome();
|
|
108
|
+
|
|
109
|
+
for (const projectName of projectNames) {
|
|
110
|
+
const projectDataPath = path.join(rrceHome, 'workspaces', projectName);
|
|
111
|
+
|
|
112
|
+
const folderTypes = [
|
|
113
|
+
{ subpath: 'knowledge', icon: '📚', type: 'knowledge' },
|
|
114
|
+
{ subpath: 'refs', icon: '📎', type: 'refs' },
|
|
115
|
+
{ subpath: 'tasks', icon: '📋', type: 'tasks' },
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
for (const { subpath, icon, type } of folderTypes) {
|
|
119
|
+
const folderPath = path.join(projectDataPath, subpath);
|
|
120
|
+
if (fs.existsSync(folderPath)) {
|
|
121
|
+
referenceFolderPaths.push(folderPath);
|
|
122
|
+
|
|
123
|
+
const existingIndex = workspace.folders.findIndex(f => f.path === folderPath);
|
|
124
|
+
if (existingIndex === -1) {
|
|
125
|
+
workspace.folders.push({
|
|
126
|
+
path: folderPath,
|
|
127
|
+
name: `${icon} ${projectName} (${type}) [global]`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
61
132
|
}
|
|
62
133
|
}
|
|
63
134
|
|
|
64
|
-
//
|
|
135
|
+
// 4. Add workspace settings to mark reference folders as readonly
|
|
136
|
+
// This uses files.readonlyInclude to make imported folders read-only
|
|
137
|
+
if (referenceFolderPaths.length > 0) {
|
|
138
|
+
const readonlyPatterns: Record<string, boolean> = {};
|
|
139
|
+
|
|
140
|
+
for (const folderPath of referenceFolderPaths) {
|
|
141
|
+
// Create a pattern that matches all files in this folder
|
|
142
|
+
readonlyPatterns[`${folderPath}/**`] = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Merge with existing readonly patterns
|
|
146
|
+
const existingReadonly = (workspace.settings['files.readonlyInclude'] as Record<string, boolean>) || {};
|
|
147
|
+
workspace.settings['files.readonlyInclude'] = {
|
|
148
|
+
...existingReadonly,
|
|
149
|
+
...readonlyPatterns,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 5. Add helpful workspace settings for multi-root experience
|
|
154
|
+
workspace.settings['explorer.sortOrder'] = workspace.settings['explorer.sortOrder'] || 'default';
|
|
155
|
+
|
|
156
|
+
// Write workspace file with nice formatting
|
|
65
157
|
fs.writeFileSync(workspaceFilePath, JSON.stringify(workspace, null, 2));
|
|
66
158
|
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Remove a project's folders from the workspace file
|
|
162
|
+
*/
|
|
163
|
+
export function removeProjectFromWorkspace(
|
|
164
|
+
workspacePath: string,
|
|
165
|
+
workspaceName: string,
|
|
166
|
+
projectName: string
|
|
167
|
+
) {
|
|
168
|
+
const workspaceFilePath = path.join(workspacePath, `${workspaceName}.code-workspace`);
|
|
169
|
+
|
|
170
|
+
if (!fs.existsSync(workspaceFilePath)) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const content = fs.readFileSync(workspaceFilePath, 'utf-8');
|
|
176
|
+
const workspace: VSCodeWorkspace = JSON.parse(content);
|
|
177
|
+
|
|
178
|
+
// Filter out folders that match the project name
|
|
179
|
+
workspace.folders = workspace.folders.filter(f =>
|
|
180
|
+
!f.name?.includes(projectName)
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Also remove readonly patterns for this project
|
|
184
|
+
if (workspace.settings?.['files.readonlyInclude']) {
|
|
185
|
+
const readonly = workspace.settings['files.readonlyInclude'] as Record<string, boolean>;
|
|
186
|
+
for (const pattern of Object.keys(readonly)) {
|
|
187
|
+
if (pattern.includes(projectName)) {
|
|
188
|
+
delete readonly[pattern];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
fs.writeFileSync(workspaceFilePath, JSON.stringify(workspace, null, 2));
|
|
194
|
+
} catch {
|
|
195
|
+
// Ignore errors
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { TextPrompt, isCancel, type Prompt } from '@clack/core';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
|
|
6
|
+
interface DirectoryAutocompleteOptions {
|
|
7
|
+
message: string;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
initialValue?: string;
|
|
10
|
+
validate?: (value: string) => string | undefined;
|
|
11
|
+
hint?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Custom text input with Tab-completion for directory paths
|
|
16
|
+
* Uses @clack/core TextPrompt with custom key handling
|
|
17
|
+
*/
|
|
18
|
+
export async function directoryAutocomplete(opts: DirectoryAutocompleteOptions): Promise<string | symbol> {
|
|
19
|
+
let completions: string[] = [];
|
|
20
|
+
let completionIndex = 0;
|
|
21
|
+
let lastTabValue = '';
|
|
22
|
+
|
|
23
|
+
const prompt = new TextPrompt({
|
|
24
|
+
initialValue: opts.initialValue,
|
|
25
|
+
validate: opts.validate,
|
|
26
|
+
render() {
|
|
27
|
+
const title = `${pc.cyan('◆')} ${opts.message}`;
|
|
28
|
+
const hintText = opts.hint ? pc.dim(` (${opts.hint})`) : '';
|
|
29
|
+
|
|
30
|
+
let inputLine: string;
|
|
31
|
+
if (this.state === 'error') {
|
|
32
|
+
inputLine = `${pc.yellow('▲')} ${this.valueWithCursor}`;
|
|
33
|
+
} else if (this.state === 'submit') {
|
|
34
|
+
inputLine = `${pc.green('✓')} ${pc.dim(String(this.value || ''))}`;
|
|
35
|
+
} else {
|
|
36
|
+
inputLine = `${pc.cyan('│')} ${this.valueWithCursor || pc.dim(opts.placeholder || '')}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let result = `${title}${hintText}\n${inputLine}`;
|
|
40
|
+
|
|
41
|
+
if (this.state === 'error' && this.error) {
|
|
42
|
+
result += `\n${pc.yellow('│')} ${pc.yellow(this.error)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Show completion hint if multiple options
|
|
46
|
+
if (completions.length > 1 && this.state === 'active') {
|
|
47
|
+
const remaining = completions.length - 1;
|
|
48
|
+
result += `\n${pc.dim('│')} ${pc.dim(`+${remaining} more, press Tab again to cycle`)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Listen for key events - Tab key handling
|
|
56
|
+
prompt.on('key', (key) => {
|
|
57
|
+
if (key === '\t' || key === 'tab') {
|
|
58
|
+
handleTabCompletion(prompt);
|
|
59
|
+
} else {
|
|
60
|
+
// Reset completion state on any other key
|
|
61
|
+
completions = [];
|
|
62
|
+
completionIndex = 0;
|
|
63
|
+
lastTabValue = '';
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
function handleTabCompletion(p: TextPrompt) {
|
|
68
|
+
const input = String(p.value || '');
|
|
69
|
+
|
|
70
|
+
// Expand ~ to home directory
|
|
71
|
+
const expanded = input.startsWith('~')
|
|
72
|
+
? input.replace(/^~/, process.env.HOME || '')
|
|
73
|
+
: input;
|
|
74
|
+
|
|
75
|
+
// If user hasn't changed input since last tab, cycle through completions
|
|
76
|
+
if (lastTabValue === input && completions.length > 1) {
|
|
77
|
+
completionIndex = (completionIndex + 1) % completions.length;
|
|
78
|
+
const completion = completions[completionIndex] || '';
|
|
79
|
+
setPromptValue(p, completion);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Get new completions
|
|
84
|
+
completions = getDirectoryCompletions(expanded);
|
|
85
|
+
completionIndex = 0;
|
|
86
|
+
lastTabValue = input;
|
|
87
|
+
|
|
88
|
+
if (completions.length === 1) {
|
|
89
|
+
// Single match - auto-complete with trailing slash if directory
|
|
90
|
+
const completion = completions[0] || '';
|
|
91
|
+
setPromptValue(p, completion.endsWith('/') ? completion : completion + '/');
|
|
92
|
+
completions = []; // Clear so next Tab gets fresh completions
|
|
93
|
+
lastTabValue = '';
|
|
94
|
+
} else if (completions.length > 1) {
|
|
95
|
+
// Multiple matches - complete common prefix and show first
|
|
96
|
+
const commonPrefix = getCommonPrefix(completions);
|
|
97
|
+
if (commonPrefix.length > expanded.length) {
|
|
98
|
+
setPromptValue(p, commonPrefix);
|
|
99
|
+
lastTabValue = formatForDisplay(commonPrefix);
|
|
100
|
+
} else {
|
|
101
|
+
// Show first completion
|
|
102
|
+
setPromptValue(p, completions[0] || '');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function setPromptValue(p: TextPrompt, value: string) {
|
|
108
|
+
// Convert back to ~ format if in home directory for display
|
|
109
|
+
const displayValue = formatForDisplay(value);
|
|
110
|
+
|
|
111
|
+
// Update the prompt's value by emitting a value event
|
|
112
|
+
// This is a workaround since TextPrompt doesn't expose a direct setValue method
|
|
113
|
+
(p as any).value = displayValue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function formatForDisplay(value: string): string {
|
|
117
|
+
const home = process.env.HOME || '';
|
|
118
|
+
return value.startsWith(home)
|
|
119
|
+
? value.replace(home, '~')
|
|
120
|
+
: value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getDirectoryCompletions(inputPath: string): string[] {
|
|
124
|
+
try {
|
|
125
|
+
let dirToScan: string;
|
|
126
|
+
let prefix: string;
|
|
127
|
+
|
|
128
|
+
if (inputPath === '' || inputPath === '/') {
|
|
129
|
+
dirToScan = inputPath || '/';
|
|
130
|
+
prefix = '';
|
|
131
|
+
} else if (inputPath.endsWith('/')) {
|
|
132
|
+
// User typed a complete directory path
|
|
133
|
+
dirToScan = inputPath;
|
|
134
|
+
prefix = '';
|
|
135
|
+
} else {
|
|
136
|
+
// User is typing a partial name
|
|
137
|
+
dirToScan = path.dirname(inputPath);
|
|
138
|
+
prefix = path.basename(inputPath).toLowerCase();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!fs.existsSync(dirToScan)) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const entries = fs.readdirSync(dirToScan, { withFileTypes: true })
|
|
146
|
+
.filter(entry => {
|
|
147
|
+
// Only directories
|
|
148
|
+
if (!entry.isDirectory()) return false;
|
|
149
|
+
// Skip hidden directories unless explicitly typing them
|
|
150
|
+
if (entry.name.startsWith('.') && !prefix.startsWith('.')) return false;
|
|
151
|
+
// Match prefix
|
|
152
|
+
return prefix === '' || entry.name.toLowerCase().startsWith(prefix);
|
|
153
|
+
})
|
|
154
|
+
.map(entry => path.join(dirToScan, entry.name))
|
|
155
|
+
.sort();
|
|
156
|
+
|
|
157
|
+
return entries;
|
|
158
|
+
} catch {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getCommonPrefix(strings: string[]): string {
|
|
164
|
+
if (strings.length === 0) return '';
|
|
165
|
+
if (strings.length === 1) return strings[0] || '';
|
|
166
|
+
|
|
167
|
+
let prefix = strings[0] || '';
|
|
168
|
+
for (let i = 1; i < strings.length; i++) {
|
|
169
|
+
const str = strings[i] || '';
|
|
170
|
+
while (prefix.length > 0 && !str.startsWith(prefix)) {
|
|
171
|
+
prefix = prefix.slice(0, -1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return prefix;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const result = await prompt.prompt();
|
|
178
|
+
|
|
179
|
+
if (isCancel(result)) {
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Expand ~ in final result
|
|
184
|
+
const value = String(result || '');
|
|
185
|
+
return value.startsWith('~')
|
|
186
|
+
? value.replace(/^~/, process.env.HOME || '')
|
|
187
|
+
: value;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export { isCancel };
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import type { StorageMode } from '../types/prompt';
|
|
4
|
+
import { getDefaultRRCEHome } from './paths';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detected rrce-workflow project information
|
|
8
|
+
*/
|
|
9
|
+
export interface DetectedProject {
|
|
10
|
+
name: string;
|
|
11
|
+
path: string; // Absolute path to project root
|
|
12
|
+
dataPath: string; // Path to .rrce-workflow data directory
|
|
13
|
+
source: 'global' | 'sibling' | 'parent';
|
|
14
|
+
storageMode?: StorageMode;
|
|
15
|
+
knowledgePath?: string;
|
|
16
|
+
refsPath?: string;
|
|
17
|
+
tasksPath?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ScanOptions {
|
|
21
|
+
excludeWorkspace?: string; // Current workspace name to exclude
|
|
22
|
+
workspacePath?: string; // Current workspace path for sibling detection
|
|
23
|
+
scanSiblings?: boolean; // Whether to scan sibling directories (default: true)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Scan for rrce-workflow projects in various locations
|
|
28
|
+
*/
|
|
29
|
+
export function scanForProjects(options: ScanOptions = {}): DetectedProject[] {
|
|
30
|
+
const { excludeWorkspace, workspacePath, scanSiblings = true } = options;
|
|
31
|
+
const projects: DetectedProject[] = [];
|
|
32
|
+
const seenPaths = new Set<string>();
|
|
33
|
+
|
|
34
|
+
// 1. Scan global storage (~/.rrce-workflow/workspaces/)
|
|
35
|
+
const globalProjects = scanGlobalStorage(excludeWorkspace);
|
|
36
|
+
for (const project of globalProjects) {
|
|
37
|
+
if (!seenPaths.has(project.path)) {
|
|
38
|
+
seenPaths.add(project.path);
|
|
39
|
+
projects.push(project);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. Scan sibling directories (same parent as current workspace)
|
|
44
|
+
if (scanSiblings && workspacePath) {
|
|
45
|
+
const siblingProjects = scanSiblingDirectories(workspacePath, excludeWorkspace);
|
|
46
|
+
for (const project of siblingProjects) {
|
|
47
|
+
if (!seenPaths.has(project.path)) {
|
|
48
|
+
seenPaths.add(project.path);
|
|
49
|
+
projects.push(project);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return projects;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Scan global storage for projects
|
|
59
|
+
*/
|
|
60
|
+
function scanGlobalStorage(excludeWorkspace?: string): DetectedProject[] {
|
|
61
|
+
const rrceHome = getDefaultRRCEHome();
|
|
62
|
+
const workspacesDir = path.join(rrceHome, 'workspaces');
|
|
63
|
+
const projects: DetectedProject[] = [];
|
|
64
|
+
|
|
65
|
+
if (!fs.existsSync(workspacesDir)) {
|
|
66
|
+
return projects;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const entries = fs.readdirSync(workspacesDir, { withFileTypes: true });
|
|
71
|
+
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (!entry.isDirectory()) continue;
|
|
74
|
+
if (entry.name === excludeWorkspace) continue;
|
|
75
|
+
|
|
76
|
+
const projectDataPath = path.join(workspacesDir, entry.name);
|
|
77
|
+
const knowledgePath = path.join(projectDataPath, 'knowledge');
|
|
78
|
+
const refsPath = path.join(projectDataPath, 'refs');
|
|
79
|
+
const tasksPath = path.join(projectDataPath, 'tasks');
|
|
80
|
+
|
|
81
|
+
projects.push({
|
|
82
|
+
name: entry.name,
|
|
83
|
+
path: projectDataPath, // For global projects, path is the data path
|
|
84
|
+
dataPath: projectDataPath,
|
|
85
|
+
source: 'global',
|
|
86
|
+
knowledgePath: fs.existsSync(knowledgePath) ? knowledgePath : undefined,
|
|
87
|
+
refsPath: fs.existsSync(refsPath) ? refsPath : undefined,
|
|
88
|
+
tasksPath: fs.existsSync(tasksPath) ? tasksPath : undefined,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// Ignore errors
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return projects;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Scan sibling directories for workspace-scoped projects
|
|
100
|
+
*/
|
|
101
|
+
function scanSiblingDirectories(workspacePath: string, excludeWorkspace?: string): DetectedProject[] {
|
|
102
|
+
const parentDir = path.dirname(workspacePath);
|
|
103
|
+
const projects: DetectedProject[] = [];
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const entries = fs.readdirSync(parentDir, { withFileTypes: true });
|
|
107
|
+
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
if (!entry.isDirectory()) continue;
|
|
110
|
+
|
|
111
|
+
const projectPath = path.join(parentDir, entry.name);
|
|
112
|
+
|
|
113
|
+
// Skip current workspace
|
|
114
|
+
if (projectPath === workspacePath) continue;
|
|
115
|
+
if (entry.name === excludeWorkspace) continue;
|
|
116
|
+
|
|
117
|
+
// Check for .rrce-workflow/config.yaml
|
|
118
|
+
const configPath = path.join(projectPath, '.rrce-workflow', 'config.yaml');
|
|
119
|
+
if (!fs.existsSync(configPath)) continue;
|
|
120
|
+
|
|
121
|
+
// Parse config to get project details
|
|
122
|
+
const config = parseWorkspaceConfig(configPath);
|
|
123
|
+
if (!config) continue;
|
|
124
|
+
|
|
125
|
+
const dataPath = path.join(projectPath, '.rrce-workflow');
|
|
126
|
+
const knowledgePath = path.join(dataPath, 'knowledge');
|
|
127
|
+
const refsPath = path.join(dataPath, 'refs');
|
|
128
|
+
const tasksPath = path.join(dataPath, 'tasks');
|
|
129
|
+
|
|
130
|
+
projects.push({
|
|
131
|
+
name: config.name || entry.name,
|
|
132
|
+
path: projectPath,
|
|
133
|
+
dataPath,
|
|
134
|
+
source: 'sibling',
|
|
135
|
+
storageMode: config.storageMode,
|
|
136
|
+
knowledgePath: fs.existsSync(knowledgePath) ? knowledgePath : undefined,
|
|
137
|
+
refsPath: fs.existsSync(refsPath) ? refsPath : undefined,
|
|
138
|
+
tasksPath: fs.existsSync(tasksPath) ? tasksPath : undefined,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// Ignore errors
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return projects;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Parse a workspace config file
|
|
150
|
+
*/
|
|
151
|
+
export function parseWorkspaceConfig(configPath: string): {
|
|
152
|
+
name: string;
|
|
153
|
+
storageMode: StorageMode;
|
|
154
|
+
linkedProjects?: string[];
|
|
155
|
+
} | null {
|
|
156
|
+
try {
|
|
157
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
158
|
+
|
|
159
|
+
// Simple YAML parsing (we don't want to add a full YAML library)
|
|
160
|
+
const nameMatch = content.match(/name:\s*["']?([^"'\n]+)["']?/);
|
|
161
|
+
const modeMatch = content.match(/mode:\s*(global|workspace|both)/);
|
|
162
|
+
|
|
163
|
+
// Parse linked projects
|
|
164
|
+
const linkedProjects: string[] = [];
|
|
165
|
+
const linkedMatch = content.match(/linked_projects:\s*\n((?:\s+-\s+[^\n]+\n?)+)/);
|
|
166
|
+
if (linkedMatch && linkedMatch[1]) {
|
|
167
|
+
const lines = linkedMatch[1].split('\n');
|
|
168
|
+
for (const line of lines) {
|
|
169
|
+
const projectMatch = line.match(/^\s+-\s+(.+)$/);
|
|
170
|
+
if (projectMatch && projectMatch[1]) {
|
|
171
|
+
linkedProjects.push(projectMatch[1].trim());
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
name: nameMatch?.[1]?.trim() || path.basename(path.dirname(path.dirname(configPath))),
|
|
178
|
+
storageMode: (modeMatch?.[1] as StorageMode) || 'global',
|
|
179
|
+
linkedProjects: linkedProjects.length > 0 ? linkedProjects : undefined,
|
|
180
|
+
};
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get display label for a detected project
|
|
188
|
+
*/
|
|
189
|
+
export function getProjectDisplayLabel(project: DetectedProject): string {
|
|
190
|
+
switch (project.source) {
|
|
191
|
+
case 'global':
|
|
192
|
+
return `global: ~/.rrce-workflow/workspaces/${project.name}`;
|
|
193
|
+
case 'sibling':
|
|
194
|
+
return `sibling: ${project.path}/.rrce-workflow`;
|
|
195
|
+
default:
|
|
196
|
+
return project.dataPath;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get all linkable folders from a detected project
|
|
202
|
+
*/
|
|
203
|
+
export function getProjectFolders(project: DetectedProject): Array<{
|
|
204
|
+
path: string;
|
|
205
|
+
type: 'knowledge' | 'refs' | 'tasks';
|
|
206
|
+
displayName: string;
|
|
207
|
+
}> {
|
|
208
|
+
const folders: Array<{ path: string; type: 'knowledge' | 'refs' | 'tasks'; displayName: string }> = [];
|
|
209
|
+
|
|
210
|
+
if (project.knowledgePath) {
|
|
211
|
+
folders.push({
|
|
212
|
+
path: project.knowledgePath,
|
|
213
|
+
type: 'knowledge',
|
|
214
|
+
displayName: `📚 ${project.name} (knowledge)`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (project.refsPath) {
|
|
219
|
+
folders.push({
|
|
220
|
+
path: project.refsPath,
|
|
221
|
+
type: 'refs',
|
|
222
|
+
displayName: `📎 ${project.name} (refs)`,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (project.tasksPath) {
|
|
227
|
+
folders.push({
|
|
228
|
+
path: project.tasksPath,
|
|
229
|
+
type: 'tasks',
|
|
230
|
+
displayName: `📋 ${project.name} (tasks)`,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return folders;
|
|
235
|
+
}
|
package/src/lib/prompts.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
3
4
|
import matter from 'gray-matter';
|
|
4
5
|
import { PromptFrontmatterSchema, type ParsedPrompt } from '../types/prompt';
|
|
5
6
|
|
|
7
|
+
// Get __dirname equivalent for ESM (works with both npm/tsx and Bun)
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
6
11
|
/**
|
|
7
12
|
* Parse a prompt file and extract frontmatter + content
|
|
8
13
|
*/
|
|
@@ -52,10 +57,11 @@ export function loadPromptsFromDir(dirPath: string): ParsedPrompt[] {
|
|
|
52
57
|
|
|
53
58
|
/**
|
|
54
59
|
* Get the agent-core root directory
|
|
60
|
+
* Works with both npm/tsx and Bun
|
|
55
61
|
*/
|
|
56
62
|
export function getAgentCoreDir(): string {
|
|
57
|
-
// Relative to
|
|
58
|
-
return path.join(
|
|
63
|
+
// Relative to this file: src/lib/prompts.ts -> ../../agent-core
|
|
64
|
+
return path.join(__dirname, '..', '..', 'agent-core');
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
/**
|
|
@@ -64,3 +70,4 @@ export function getAgentCoreDir(): string {
|
|
|
64
70
|
export function getAgentCorePromptsDir(): string {
|
|
65
71
|
return path.join(getAgentCoreDir(), 'prompts');
|
|
66
72
|
}
|
|
73
|
+
|