rrce-workflow 0.1.2 → 0.1.4

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 CHANGED
@@ -66,9 +66,23 @@ project:
66
66
 
67
67
  | Mode | Location | Use Case |
68
68
  |------|----------|----------|
69
- | `global` | `~/.rrce-workflow/workspaces/<name>/` | Non-intrusive |
70
- | `workspace` | `.rrce-workflow/` | Portable with repo |
71
- | `both` | Both locations | Redundancy |
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 |
72
+
73
+ When `both` is selected, data is stored in **both** locations simultaneously:
74
+ - Primary (for reads): `<workspace>/.rrce-workflow/`
75
+ - Secondary (auto-synced): `~/.rrce-workflow/workspaces/<name>/`
76
+
77
+ Each storage location contains:
78
+ ```
79
+ <storage-path>/
80
+ ├── knowledge/ # Project context and domain knowledge
81
+ ├── prompts/ # Agent prompt files
82
+ ├── refs/ # External references
83
+ ├── tasks/ # Task artifacts and metadata
84
+ └── templates/ # Output templates
85
+ ```
72
86
 
73
87
  ## Requirements
74
88
 
@@ -39,8 +39,11 @@ Path Resolution
39
39
  - Storage mode: Determined by `.rrce-workflow.yaml` → global config → default (`global`)
40
40
  - `global`: Data in `~/.rrce-workflow/workspaces/<workspace-name>/`
41
41
  - `workspace`: Data in `<workspace>/.rrce-workflow/`
42
- - `both`: Dual storage with sync
43
- - Data path: `{{RRCE_DATA}}` (resolves based on storage mode)
42
+ - `both`: **Dual storage** - data stored in BOTH locations simultaneously
43
+ - Primary (for reads): `<workspace>/.rrce-workflow/`
44
+ - Secondary (auto-synced): `~/.rrce-workflow/workspaces/<workspace-name>/`
45
+ - When writing, always write to `{{RRCE_DATA}}` - the system ensures both locations stay in sync
46
+ - Data path: `{{RRCE_DATA}}` (resolves to primary storage based on mode)
44
47
  - Global home: `{{RRCE_HOME}}` (always `~/.rrce-workflow`)
45
48
  - Workspace root: `{{WORKSPACE_ROOT}}` (auto-detected or via `$RRCE_WORKSPACE`)
46
49
  - Workspace name: `{{WORKSPACE_NAME}}` (from config or directory name)
@@ -35,8 +35,11 @@ Path Resolution
35
35
  - Storage mode: Determined by `.rrce-workflow.yaml` → global config → default (`global`)
36
36
  - `global`: Data in `~/.rrce-workflow/workspaces/<workspace-name>/`
37
37
  - `workspace`: Data in `<workspace>/.rrce-workflow/`
38
- - `both`: Dual storage with sync
39
- - Data path: `{{RRCE_DATA}}` (resolves based on storage mode)
38
+ - `both`: **Dual storage** - data stored in BOTH locations simultaneously
39
+ - Primary (for reads): `<workspace>/.rrce-workflow/`
40
+ - Secondary (auto-synced): `~/.rrce-workflow/workspaces/<workspace-name>/`
41
+ - When writing, always write to `{{RRCE_DATA}}` - the system ensures both locations stay in sync
42
+ - Data path: `{{RRCE_DATA}}` (resolves to primary storage based on mode)
40
43
  - Global home: `{{RRCE_HOME}}` (always `~/.rrce-workflow`)
41
44
  - Workspace root: `{{WORKSPACE_ROOT}}` (auto-detected or via `$RRCE_WORKSPACE`)
42
45
  - Workspace name: `{{WORKSPACE_NAME}}` (from config or directory name)
@@ -32,8 +32,11 @@ Path Resolution
32
32
  - Storage mode: Determined by `.rrce-workflow.yaml` → global config → default (`global`)
33
33
  - `global`: Data stored in `~/.rrce-workflow/workspaces/<workspace-name>/`
34
34
  - `workspace`: Data stored in `<workspace>/.rrce-workflow/`
35
- - `both`: Dual storage with sync
36
- - Data path: `{{RRCE_DATA}}` (resolves based on storage mode)
35
+ - `both`: **Dual storage** - data stored in BOTH locations simultaneously
36
+ - Primary (for reads): `<workspace>/.rrce-workflow/`
37
+ - Secondary (auto-synced): `~/.rrce-workflow/workspaces/<workspace-name>/`
38
+ - When writing, always write to `{{RRCE_DATA}}` - the system ensures both locations stay in sync
39
+ - Data path: `{{RRCE_DATA}}` (resolves to primary storage based on mode)
37
40
  - Global home: `{{RRCE_HOME}}` (always `~/.rrce-workflow`)
38
41
  - Workspace root: `{{WORKSPACE_ROOT}}` (auto-detected or via `$RRCE_WORKSPACE`)
39
42
  - Workspace name: `{{WORKSPACE_NAME}}` (from config or directory name)
@@ -32,8 +32,11 @@ Path Resolution
32
32
  - Storage mode: Determined by `.rrce-workflow.yaml` → global config → default (`global`)
33
33
  - `global`: Data in `~/.rrce-workflow/workspaces/<workspace-name>/`
34
34
  - `workspace`: Data in `<workspace>/.rrce-workflow/`
35
- - `both`: Dual storage with sync
36
- - Data path: `{{RRCE_DATA}}` (resolves based on storage mode)
35
+ - `both`: **Dual storage** - data stored in BOTH locations simultaneously
36
+ - Primary (for reads): `<workspace>/.rrce-workflow/`
37
+ - Secondary (auto-synced): `~/.rrce-workflow/workspaces/<workspace-name>/`
38
+ - When writing, always write to `{{RRCE_DATA}}` - the system ensures both locations stay in sync
39
+ - Data path: `{{RRCE_DATA}}` (resolves to primary storage based on mode)
37
40
  - Global home: `{{RRCE_HOME}}` (always `~/.rrce-workflow`)
38
41
  - Workspace root: `{{WORKSPACE_ROOT}}` (auto-detected or via `$RRCE_WORKSPACE`)
39
42
  - Workspace name: `{{WORKSPACE_NAME}}` (from config or directory name)
@@ -39,8 +39,11 @@ Path Resolution
39
39
  - Storage mode: Determined by `.rrce-workflow.yaml` → global config → default (`global`)
40
40
  - `global`: Data in `~/.rrce-workflow/workspaces/<workspace-name>/`
41
41
  - `workspace`: Data in `<workspace>/.rrce-workflow/`
42
- - `both`: Dual storage with sync
43
- - Data path: `{{RRCE_DATA}}` (resolves based on storage mode)
42
+ - `both`: **Dual storage** - data stored in BOTH locations simultaneously
43
+ - Primary (for reads): `<workspace>/.rrce-workflow/`
44
+ - Secondary (auto-synced): `~/.rrce-workflow/workspaces/<workspace-name>/`
45
+ - When writing, always write to `{{RRCE_DATA}}` - the system ensures both locations stay in sync
46
+ - Data path: `{{RRCE_DATA}}` (resolves to primary storage based on mode)
44
47
  - Global home: `{{RRCE_HOME}}` (always `~/.rrce-workflow`)
45
48
  - Workspace root: `{{WORKSPACE_ROOT}}` (auto-detected or via `$RRCE_WORKSPACE`)
46
49
  - Workspace name: `{{WORKSPACE_NAME}}` (from config or directory name)
@@ -32,8 +32,11 @@ Path Resolution
32
32
  - Storage mode: Determined by `.rrce-workflow.yaml` → global config → default (`global`)
33
33
  - `global`: Data in `~/.rrce-workflow/workspaces/<workspace-name>/`
34
34
  - `workspace`: Data in `<workspace>/.rrce-workflow/`
35
- - `both`: Dual storage with sync
36
- - Data path: `{{RRCE_DATA}}` (resolves based on storage mode)
35
+ - `both`: **Dual storage** - data stored in BOTH locations simultaneously
36
+ - Primary (for reads): `<workspace>/.rrce-workflow/`
37
+ - Secondary (auto-synced): `~/.rrce-workflow/workspaces/<workspace-name>/`
38
+ - When writing, always write to `{{RRCE_DATA}}` - the system ensures both locations stay in sync
39
+ - Data path: `{{RRCE_DATA}}` (resolves to primary storage based on mode)
37
40
  - Global home: `{{RRCE_HOME}}` (always `~/.rrce-workflow`)
38
41
  - Workspace root: `{{WORKSPACE_ROOT}}` (auto-detected or via `$RRCE_WORKSPACE`)
39
42
  - Workspace name: `{{WORKSPACE_NAME}}` (from config or directory name)
@@ -1,3 +1,13 @@
1
+ <!--
2
+ RRCE Template Variables:
3
+ - {{RRCE_DATA}}: Primary storage path (resolves based on storage mode in .rrce-workflow.yaml)
4
+ - global: ~/.rrce-workflow/workspaces/<workspace-name>/
5
+ - workspace: <workspace>/.rrce-workflow/
6
+ - both: <workspace>/.rrce-workflow/ (primary, auto-synced to global)
7
+ - {{RRCE_HOME}}: Always ~/.rrce-workflow
8
+ - {{WORKSPACE_ROOT}}: Workspace root directory
9
+ - {{WORKSPACE_NAME}}: Workspace name from config or directory name
10
+ -->
1
11
  # Handover Note – {{task_title}}
2
12
 
3
13
  - Task ID: `{{task_id}}`
@@ -1,3 +1,13 @@
1
+ <!--
2
+ RRCE Template Variables:
3
+ - {{RRCE_DATA}}: Primary storage path (resolves based on storage mode in .rrce-workflow.yaml)
4
+ - global: ~/.rrce-workflow/workspaces/<workspace-name>/
5
+ - workspace: <workspace>/.rrce-workflow/
6
+ - both: <workspace>/.rrce-workflow/ (primary, auto-synced to global)
7
+ - {{RRCE_HOME}}: Always ~/.rrce-workflow
8
+ - {{WORKSPACE_ROOT}}: Workspace root directory
9
+ - {{WORKSPACE_NAME}}: Workspace name from config or directory name
10
+ -->
1
11
  # Execution Log – {{task_title}}
2
12
 
3
13
  - Task ID: `{{task_id}}`
@@ -1,3 +1,13 @@
1
+ <!--
2
+ RRCE Template Variables:
3
+ - {{RRCE_DATA}}: Primary storage path (resolves based on storage mode in .rrce-workflow.yaml)
4
+ - global: ~/.rrce-workflow/workspaces/<workspace-name>/
5
+ - workspace: <workspace>/.rrce-workflow/
6
+ - both: <workspace>/.rrce-workflow/ (primary, auto-synced to global)
7
+ - {{RRCE_HOME}}: Always ~/.rrce-workflow
8
+ - {{WORKSPACE_ROOT}}: Workspace root directory
9
+ - {{WORKSPACE_NAME}}: Workspace name from config or directory name
10
+ -->
1
11
  # Project Context – {{project_name}}
2
12
 
3
13
  - Initialized: `{{date}}`
@@ -1,3 +1,13 @@
1
+ <!--
2
+ RRCE Template Variables:
3
+ - {{RRCE_DATA}}: Primary storage path (resolves based on storage mode in .rrce-workflow.yaml)
4
+ - global: ~/.rrce-workflow/workspaces/<workspace-name>/
5
+ - workspace: <workspace>/.rrce-workflow/
6
+ - both: <workspace>/.rrce-workflow/ (primary, auto-synced to global)
7
+ - {{RRCE_HOME}}: Always ~/.rrce-workflow
8
+ - {{WORKSPACE_ROOT}}: Workspace root directory
9
+ - {{WORKSPACE_NAME}}: Workspace name from config or directory name
10
+ -->
1
11
  # Execution Plan – {{task_title}}
2
12
 
3
13
  - Task ID: `{{task_id}}`
@@ -1,3 +1,13 @@
1
+ <!--
2
+ RRCE Template Variables:
3
+ - {{RRCE_DATA}}: Primary storage path (resolves based on storage mode in .rrce-workflow.yaml)
4
+ - global: ~/.rrce-workflow/workspaces/<workspace-name>/
5
+ - workspace: <workspace>/.rrce-workflow/
6
+ - both: <workspace>/.rrce-workflow/ (primary, auto-synced to global)
7
+ - {{RRCE_HOME}}: Always ~/.rrce-workflow
8
+ - {{WORKSPACE_ROOT}}: Workspace root directory
9
+ - {{WORKSPACE_NAME}}: Workspace name from config or directory name
10
+ -->
1
11
  # Research Brief – {{task_title}}
2
12
 
3
13
  - Task ID: `{{task_id}}`
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env bun
2
- import "../src/index.tsx";
2
+ import "../src/index.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rrce-workflow",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "RRCE-Workflow TUI - Agentic code workflow generator for AI-assisted development",
5
5
  "author": "RRCE Team",
6
6
  "license": "MIT",
@@ -3,9 +3,9 @@ import pc from 'picocolors';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
5
  import { getGitUser } from '../lib/git';
6
- import { detectWorkspaceRoot, getWorkspaceName, resolveDataPath, ensureDir, getAgentPromptPath } from '../lib/paths';
6
+ import { detectWorkspaceRoot, getWorkspaceName, resolveAllDataPaths, ensureDir, getAgentPromptPath, syncMetadataToAll, copyDirToAllStoragePaths, listGlobalProjects, getGlobalProjectKnowledgePath, getRRCEHome } from '../lib/paths';
7
7
  import type { StorageMode } from '../types/prompt';
8
- import { loadPromptsFromDir, getAgentCorePromptsDir } from '../lib/prompts';
8
+ import { loadPromptsFromDir, getAgentCorePromptsDir, getAgentCoreDir } from '../lib/prompts';
9
9
 
10
10
  import type { ParsedPrompt } from '../types/prompt';
11
11
 
@@ -28,6 +28,38 @@ Workspace: ${pc.bold(workspaceName)}`,
28
28
  'Context'
29
29
  );
30
30
 
31
+ // Check for existing projects in global storage
32
+ const existingProjects = listGlobalProjects(workspaceName);
33
+
34
+ // Check if already configured
35
+ const configFilePath = path.join(workspacePath, '.rrce-workflow.yaml');
36
+ const isAlreadyConfigured = fs.existsSync(configFilePath);
37
+
38
+ // If already configured and there are other projects, show menu
39
+ if (isAlreadyConfigured && existingProjects.length > 0) {
40
+ const action = await select({
41
+ message: 'This workspace is already configured. What would you like to do?',
42
+ options: [
43
+ { value: 'link', label: 'Link other project knowledge', hint: `${existingProjects.length} projects available` },
44
+ { value: 'reconfigure', label: 'Reconfigure from scratch' },
45
+ { value: 'exit', label: 'Exit' },
46
+ ],
47
+ });
48
+
49
+ if (isCancel(action) || action === 'exit') {
50
+ outro('Exited.');
51
+ process.exit(0);
52
+ }
53
+
54
+ if (action === 'link') {
55
+ // Link-only flow
56
+ await runLinkProjectsFlow(workspacePath, workspaceName, existingProjects);
57
+ return;
58
+ }
59
+ // Otherwise continue to full reconfigure flow
60
+ }
61
+
62
+ // Full setup flow
31
63
  const config = await group(
32
64
  {
33
65
  storageMode: () =>
@@ -49,6 +81,21 @@ Workspace: ${pc.bold(workspaceName)}`,
49
81
  ],
50
82
  required: false,
51
83
  }),
84
+ linkedProjects: () => {
85
+ // Only show if there are other projects to link
86
+ if (existingProjects.length === 0) {
87
+ return Promise.resolve([]);
88
+ }
89
+ return multiselect({
90
+ message: 'Link knowledge from other projects?',
91
+ options: existingProjects.map(name => ({
92
+ value: name,
93
+ label: name,
94
+ hint: `~/.rrce-workflow/workspaces/${name}/knowledge`
95
+ })),
96
+ required: false,
97
+ });
98
+ },
52
99
  confirm: () =>
53
100
  confirm({
54
101
  message: 'Create configuration?',
@@ -71,14 +118,39 @@ Workspace: ${pc.bold(workspaceName)}`,
71
118
  s.start('Generating configuration');
72
119
 
73
120
  try {
74
- // Create config file
75
- const dataPath = resolveDataPath(config.storageMode as StorageMode, workspaceName, workspacePath);
76
- ensureDir(dataPath);
121
+ // Create data directories in all storage locations
122
+ const dataPaths = resolveAllDataPaths(config.storageMode as StorageMode, workspaceName, workspacePath);
123
+
124
+ for (const dataPath of dataPaths) {
125
+ ensureDir(dataPath);
126
+ // Create agent metadata subdirectories
127
+ ensureDir(path.join(dataPath, 'knowledge'));
128
+ ensureDir(path.join(dataPath, 'refs'));
129
+ ensureDir(path.join(dataPath, 'tasks'));
130
+ ensureDir(path.join(dataPath, 'templates'));
131
+ ensureDir(path.join(dataPath, 'prompts'));
132
+ }
133
+
134
+ // Get the agent-core directory path
135
+ const agentCoreDir = getAgentCoreDir();
136
+
137
+ // Sync metadata (knowledge, refs, tasks) from agent-core to all storage locations
138
+ syncMetadataToAll(agentCoreDir, dataPaths);
139
+
140
+ // Also copy templates to all storage locations
141
+ copyDirToAllStoragePaths(path.join(agentCoreDir, 'templates'), 'templates', dataPaths);
77
142
 
78
143
  // Load prompts
79
144
  const prompts = loadPromptsFromDir(getAgentCorePromptsDir());
80
145
 
81
- // Copy prompts
146
+ // Copy prompts to all storage locations (for cross-project access)
147
+ for (const dataPath of dataPaths) {
148
+ const promptsDir = path.join(dataPath, 'prompts');
149
+ ensureDir(promptsDir);
150
+ copyPromptsToDir(prompts, promptsDir, '.md');
151
+ }
152
+
153
+ // Copy prompts to tool-specific locations (for IDE integration)
82
154
  const selectedTools = config.tools as string[];
83
155
 
84
156
  if (selectedTools.includes('copilot')) {
@@ -94,8 +166,9 @@ Workspace: ${pc.bold(workspaceName)}`,
94
166
  }
95
167
 
96
168
  // Create workspace config
169
+ const linkedProjects = config.linkedProjects as string[];
97
170
  const workspaceConfigPath = path.join(workspacePath, '.rrce-workflow.yaml');
98
- const configContent = `# RRCE-Workflow Configuration
171
+ let configContent = `# RRCE-Workflow Configuration
99
172
  version: 1
100
173
 
101
174
  storage:
@@ -103,12 +176,50 @@ storage:
103
176
 
104
177
  project:
105
178
  name: "${workspaceName}"
179
+
180
+ tools:
181
+ copilot: ${selectedTools.includes('copilot')}
182
+ antigravity: ${selectedTools.includes('antigravity')}
106
183
  `;
184
+
185
+ // Add linked projects if any
186
+ if (linkedProjects.length > 0) {
187
+ configContent += `\nlinked_projects:\n`;
188
+ linkedProjects.forEach(name => {
189
+ configContent += ` - ${name}\n`;
190
+ });
191
+ }
192
+
107
193
  fs.writeFileSync(workspaceConfigPath, configContent);
108
194
 
195
+ // Generate VSCode workspace file if using copilot or has linked projects
196
+ if (selectedTools.includes('copilot') || linkedProjects.length > 0) {
197
+ generateVSCodeWorkspace(workspacePath, workspaceName, linkedProjects);
198
+ }
199
+
109
200
  s.stop('Configuration generated');
110
201
 
111
- outro(pc.green(`✓ Setup complete! You can now run "rrce-workflow select" to start using agents.`));
202
+ // Show summary
203
+ const summary = [
204
+ `Storage: ${config.storageMode === 'both' ? 'global + workspace' : config.storageMode}`,
205
+ ];
206
+
207
+ if (dataPaths.length > 0) {
208
+ summary.push(`Data paths:`);
209
+ dataPaths.forEach(p => summary.push(` - ${p}`));
210
+ }
211
+
212
+ if (selectedTools.length > 0) {
213
+ summary.push(`Tools: ${selectedTools.join(', ')}`);
214
+ }
215
+
216
+ if (linkedProjects.length > 0) {
217
+ summary.push(`Linked projects: ${linkedProjects.join(', ')}`);
218
+ }
219
+
220
+ note(summary.join('\n'), 'Setup Summary');
221
+
222
+ outro(pc.green(`✓ Setup complete! Your agents are ready to use.`));
112
223
 
113
224
  } catch (error) {
114
225
  s.stop('Error occurred');
@@ -128,3 +239,142 @@ function copyPromptsToDir(prompts: ParsedPrompt[], targetDir: string, extension:
128
239
  fs.writeFileSync(targetPath, content);
129
240
  }
130
241
  }
242
+
243
+ interface VSCodeWorkspaceFolder {
244
+ path: string;
245
+ name?: string;
246
+ }
247
+
248
+ interface VSCodeWorkspace {
249
+ folders: VSCodeWorkspaceFolder[];
250
+ settings?: Record<string, unknown>;
251
+ }
252
+
253
+ /**
254
+ * Generate or update VSCode workspace file with linked project knowledge folders
255
+ */
256
+ function generateVSCodeWorkspace(workspacePath: string, workspaceName: string, linkedProjects: string[]) {
257
+ const workspaceFilePath = path.join(workspacePath, `${workspaceName}.code-workspace`);
258
+
259
+ let workspace: VSCodeWorkspace;
260
+
261
+ // Check if workspace file already exists
262
+ if (fs.existsSync(workspaceFilePath)) {
263
+ try {
264
+ const content = fs.readFileSync(workspaceFilePath, 'utf-8');
265
+ workspace = JSON.parse(content);
266
+ } catch {
267
+ // If parse fails, create new
268
+ workspace = { folders: [] };
269
+ }
270
+ } else {
271
+ workspace = { folders: [] };
272
+ }
273
+
274
+ // Ensure main workspace folder is first
275
+ const mainFolder: VSCodeWorkspaceFolder = { path: '.' };
276
+ const existingMainIndex = workspace.folders.findIndex(f => f.path === '.');
277
+ if (existingMainIndex === -1) {
278
+ workspace.folders.unshift(mainFolder);
279
+ }
280
+
281
+ // Add linked project knowledge folders
282
+ const rrceHome = getRRCEHome();
283
+ for (const projectName of linkedProjects) {
284
+ const knowledgePath = path.join(rrceHome, 'workspaces', projectName, 'knowledge');
285
+ const folderEntry: VSCodeWorkspaceFolder = {
286
+ path: knowledgePath,
287
+ name: `📚 ${projectName} (knowledge)`
288
+ };
289
+
290
+ // Check if already exists
291
+ const existingIndex = workspace.folders.findIndex(f => f.path === knowledgePath);
292
+ if (existingIndex === -1) {
293
+ workspace.folders.push(folderEntry);
294
+ }
295
+ }
296
+
297
+ // Write workspace file
298
+ fs.writeFileSync(workspaceFilePath, JSON.stringify(workspace, null, 2));
299
+ }
300
+
301
+ /**
302
+ * Run the link-only flow for adding other project knowledge to an existing workspace
303
+ */
304
+ async function runLinkProjectsFlow(workspacePath: string, workspaceName: string, existingProjects: string[]) {
305
+ const linkedProjects = await multiselect({
306
+ message: 'Select projects to link:',
307
+ options: existingProjects.map(name => ({
308
+ value: name,
309
+ label: name,
310
+ hint: `~/.rrce-workflow/workspaces/${name}/knowledge`
311
+ })),
312
+ required: true,
313
+ });
314
+
315
+ if (isCancel(linkedProjects)) {
316
+ cancel('Cancelled.');
317
+ process.exit(0);
318
+ }
319
+
320
+ const selectedProjects = linkedProjects as string[];
321
+
322
+ if (selectedProjects.length === 0) {
323
+ outro('No projects selected.');
324
+ return;
325
+ }
326
+
327
+ const s = spinner();
328
+ s.start('Linking projects');
329
+
330
+ // Update .rrce-workflow.yaml with linked projects
331
+ const configFilePath = path.join(workspacePath, '.rrce-workflow.yaml');
332
+ let configContent = fs.readFileSync(configFilePath, 'utf-8');
333
+
334
+ // Check if linked_projects section exists
335
+ if (configContent.includes('linked_projects:')) {
336
+ // Append to existing section - find and update
337
+ const lines = configContent.split('\n');
338
+ const linkedIndex = lines.findIndex(l => l.trim() === 'linked_projects:');
339
+ if (linkedIndex !== -1) {
340
+ // Find where to insert new projects (after existing ones)
341
+ let insertIndex = linkedIndex + 1;
342
+ while (insertIndex < lines.length && lines[insertIndex]?.startsWith(' - ')) {
343
+ insertIndex++;
344
+ }
345
+ // Add new projects that aren't already there
346
+ for (const name of selectedProjects) {
347
+ if (!configContent.includes(` - ${name}`)) {
348
+ lines.splice(insertIndex, 0, ` - ${name}`);
349
+ insertIndex++;
350
+ }
351
+ }
352
+ configContent = lines.join('\n');
353
+ }
354
+ } else {
355
+ // Add new linked_projects section
356
+ configContent += `\nlinked_projects:\n`;
357
+ selectedProjects.forEach(name => {
358
+ configContent += ` - ${name}\n`;
359
+ });
360
+ }
361
+
362
+ fs.writeFileSync(configFilePath, configContent);
363
+
364
+ // Update VSCode workspace file
365
+ generateVSCodeWorkspace(workspacePath, workspaceName, selectedProjects);
366
+
367
+ s.stop('Projects linked');
368
+
369
+ // Show summary
370
+ const summary = [
371
+ `Linked projects:`,
372
+ ...selectedProjects.map(p => ` ✓ ${p}`),
373
+ ``,
374
+ `VSCode workspace file updated: ${workspaceName}.code-workspace`,
375
+ ];
376
+
377
+ note(summary.join('\n'), 'Link Summary');
378
+
379
+ outro(pc.green('✓ Projects linked successfully!'));
380
+ }
package/src/lib/paths.ts CHANGED
@@ -38,7 +38,8 @@ export function getWorkspaceName(workspaceRoot: string): string {
38
38
  }
39
39
 
40
40
  /**
41
- * Resolve data path based on storage mode
41
+ * Resolve primary data path based on storage mode
42
+ * Note: For 'both' mode, use resolveAllDataPaths() to get all paths
42
43
  */
43
44
  export function resolveDataPath(mode: StorageMode, workspaceName: string, workspaceRoot: string): string {
44
45
  switch (mode) {
@@ -54,6 +55,29 @@ export function resolveDataPath(mode: StorageMode, workspaceName: string, worksp
54
55
  }
55
56
  }
56
57
 
58
+ /**
59
+ * Resolve ALL data paths based on storage mode
60
+ * Returns array of paths where data should be stored:
61
+ * - 'global': [~/.rrce-workflow/workspaces/<name>]
62
+ * - 'workspace': [<workspace>/.rrce-workflow]
63
+ * - 'both': [<workspace>/.rrce-workflow, ~/.rrce-workflow/workspaces/<name>]
64
+ */
65
+ export function resolveAllDataPaths(mode: StorageMode, workspaceName: string, workspaceRoot: string): string[] {
66
+ const globalPath = path.join(RRCE_HOME, 'workspaces', workspaceName);
67
+ const workspacePath = path.join(workspaceRoot, '.rrce-workflow');
68
+
69
+ switch (mode) {
70
+ case 'global':
71
+ return [globalPath];
72
+ case 'workspace':
73
+ return [workspacePath];
74
+ case 'both':
75
+ return [workspacePath, globalPath];
76
+ default:
77
+ return [globalPath];
78
+ }
79
+ }
80
+
57
81
  /**
58
82
  * Get RRCE home directory
59
83
  */
@@ -61,6 +85,35 @@ export function getRRCEHome(): string {
61
85
  return RRCE_HOME;
62
86
  }
63
87
 
88
+ /**
89
+ * List all projects in global storage
90
+ * @param excludeWorkspace - Workspace name to exclude from the list (typically current workspace)
91
+ * @returns Array of project names found in ~/.rrce-workflow/workspaces/
92
+ */
93
+ export function listGlobalProjects(excludeWorkspace?: string): string[] {
94
+ const workspacesDir = path.join(RRCE_HOME, 'workspaces');
95
+
96
+ if (!fs.existsSync(workspacesDir)) {
97
+ return [];
98
+ }
99
+
100
+ try {
101
+ const entries = fs.readdirSync(workspacesDir, { withFileTypes: true });
102
+ return entries
103
+ .filter(entry => entry.isDirectory() && entry.name !== excludeWorkspace)
104
+ .map(entry => entry.name);
105
+ } catch {
106
+ return [];
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Get the knowledge path for a global project
112
+ */
113
+ export function getGlobalProjectKnowledgePath(projectName: string): string {
114
+ return path.join(RRCE_HOME, 'workspaces', projectName, 'knowledge');
115
+ }
116
+
64
117
  /**
65
118
  * Ensure directory exists
66
119
  */
@@ -80,3 +133,71 @@ export function getAgentPromptPath(workspaceRoot: string, tool: 'copilot' | 'ant
80
133
  return path.join(workspaceRoot, '.agent', 'workflows');
81
134
  }
82
135
  }
136
+
137
+ /**
138
+ * Copy a file to all storage paths
139
+ * @param sourceFile - Absolute path to source file
140
+ * @param relativePath - Relative path within the data directory (e.g., 'knowledge/context.md')
141
+ * @param dataPaths - Array of data paths from resolveAllDataPaths()
142
+ */
143
+ export function copyToAllStoragePaths(sourceFile: string, relativePath: string, dataPaths: string[]): void {
144
+ const content = fs.readFileSync(sourceFile);
145
+
146
+ for (const dataPath of dataPaths) {
147
+ const targetPath = path.join(dataPath, relativePath);
148
+ ensureDir(path.dirname(targetPath));
149
+ fs.writeFileSync(targetPath, content);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Write content to a file in all storage paths
155
+ * @param content - Content to write
156
+ * @param relativePath - Relative path within the data directory
157
+ * @param dataPaths - Array of data paths from resolveAllDataPaths()
158
+ */
159
+ export function writeToAllStoragePaths(content: string | Buffer, relativePath: string, dataPaths: string[]): void {
160
+ for (const dataPath of dataPaths) {
161
+ const targetPath = path.join(dataPath, relativePath);
162
+ ensureDir(path.dirname(targetPath));
163
+ fs.writeFileSync(targetPath, content);
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Copy a directory recursively to all storage paths
169
+ * @param sourceDir - Absolute path to source directory
170
+ * @param relativeDir - Relative directory path within the data directory
171
+ * @param dataPaths - Array of data paths from resolveAllDataPaths()
172
+ */
173
+ export function copyDirToAllStoragePaths(sourceDir: string, relativeDir: string, dataPaths: string[]): void {
174
+ if (!fs.existsSync(sourceDir)) {
175
+ return;
176
+ }
177
+
178
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
179
+
180
+ for (const entry of entries) {
181
+ const sourcePath = path.join(sourceDir, entry.name);
182
+ const relativePath = path.join(relativeDir, entry.name);
183
+
184
+ if (entry.isDirectory()) {
185
+ copyDirToAllStoragePaths(sourcePath, relativePath, dataPaths);
186
+ } else {
187
+ copyToAllStoragePaths(sourcePath, relativePath, dataPaths);
188
+ }
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Sync metadata subdirectories (knowledge, refs, tasks) to all storage paths
194
+ * Copies from agent-core to all configured storage locations
195
+ */
196
+ export function syncMetadataToAll(agentCorePath: string, dataPaths: string[]): void {
197
+ const metadataDirs = ['knowledge', 'refs', 'tasks'];
198
+
199
+ for (const dir of metadataDirs) {
200
+ const sourceDir = path.join(agentCorePath, dir);
201
+ copyDirToAllStoragePaths(sourceDir, dir, dataPaths);
202
+ }
203
+ }
@@ -50,10 +50,17 @@ export function loadPromptsFromDir(dirPath: string): ParsedPrompt[] {
50
50
  return prompts;
51
51
  }
52
52
 
53
+ /**
54
+ * Get the agent-core root directory
55
+ */
56
+ export function getAgentCoreDir(): string {
57
+ // Relative to the package root
58
+ return path.join(import.meta.dir, '..', '..', 'agent-core');
59
+ }
60
+
53
61
  /**
54
62
  * Get the agent-core prompts directory
55
63
  */
56
64
  export function getAgentCorePromptsDir(): string {
57
- // Relative to the package root
58
- return path.join(import.meta.dir, '..', '..', 'agent-core', 'prompts');
65
+ return path.join(getAgentCoreDir(), 'prompts');
59
66
  }