rrce-workflow 0.2.14 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/bin/rrce-workflow.js +3 -33
  2. package/dist/commands/selector.d.ts +1 -0
  3. package/dist/commands/selector.js +29 -0
  4. package/dist/commands/wizard/index.d.ts +1 -0
  5. package/dist/commands/wizard/index.js +86 -0
  6. package/dist/commands/wizard/link-flow.d.ts +5 -0
  7. package/dist/commands/wizard/link-flow.js +97 -0
  8. package/dist/commands/wizard/setup-flow.d.ts +4 -0
  9. package/dist/commands/wizard/setup-flow.js +262 -0
  10. package/dist/commands/wizard/sync-flow.d.ts +4 -0
  11. package/dist/commands/wizard/sync-flow.js +67 -0
  12. package/dist/commands/wizard/update-flow.d.ts +4 -0
  13. package/dist/commands/wizard/update-flow.js +85 -0
  14. package/dist/commands/wizard/utils.d.ts +9 -0
  15. package/dist/commands/wizard/utils.js +33 -0
  16. package/dist/commands/wizard/vscode.d.ts +15 -0
  17. package/dist/commands/wizard/vscode.js +148 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +1181 -0
  20. package/dist/lib/autocomplete-prompt.d.ts +14 -0
  21. package/dist/lib/autocomplete-prompt.js +167 -0
  22. package/dist/lib/detection.d.ts +44 -0
  23. package/dist/lib/detection.js +185 -0
  24. package/dist/lib/git.d.ts +12 -0
  25. package/dist/lib/git.js +37 -0
  26. package/dist/lib/paths.d.ts +108 -0
  27. package/dist/lib/paths.js +296 -0
  28. package/dist/lib/prompts.d.ts +18 -0
  29. package/dist/lib/prompts.js +62 -0
  30. package/dist/types/prompt.d.ts +54 -0
  31. package/dist/types/prompt.js +20 -0
  32. package/package.json +10 -7
  33. package/src/commands/selector.ts +0 -42
  34. package/src/commands/wizard/index.ts +0 -114
  35. package/src/commands/wizard/link-flow.ts +0 -118
  36. package/src/commands/wizard/setup-flow.ts +0 -347
  37. package/src/commands/wizard/sync-flow.ts +0 -93
  38. package/src/commands/wizard/update-flow.ts +0 -124
  39. package/src/commands/wizard/utils.ts +0 -38
  40. package/src/commands/wizard/vscode.ts +0 -197
  41. package/src/index.ts +0 -11
  42. package/src/lib/autocomplete-prompt.ts +0 -190
  43. package/src/lib/detection.ts +0 -235
  44. package/src/lib/git.ts +0 -37
  45. package/src/lib/paths.ts +0 -332
  46. package/src/lib/prompts.ts +0 -73
  47. package/src/types/prompt.ts +0 -54
@@ -1,38 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import { ensureDir } from '../../lib/paths';
4
- import type { ParsedPrompt } from '../../types/prompt';
5
-
6
- /**
7
- * Copy parsed prompts to a target directory with specified extension
8
- */
9
- export function copyPromptsToDir(prompts: ParsedPrompt[], targetDir: string, extension: string) {
10
- for (const prompt of prompts) {
11
- const baseName = path.basename(prompt.filePath, '.md');
12
- const targetName = baseName + extension;
13
- const targetPath = path.join(targetDir, targetName);
14
-
15
- // Read the full content including frontmatter
16
- const content = fs.readFileSync(prompt.filePath, 'utf-8');
17
- fs.writeFileSync(targetPath, content);
18
- }
19
- }
20
-
21
- /**
22
- * Recursively copy a directory
23
- */
24
- export function copyDirRecursive(src: string, dest: string) {
25
- const entries = fs.readdirSync(src, { withFileTypes: true });
26
-
27
- for (const entry of entries) {
28
- const srcPath = path.join(src, entry.name);
29
- const destPath = path.join(dest, entry.name);
30
-
31
- if (entry.isDirectory()) {
32
- ensureDir(destPath);
33
- copyDirRecursive(srcPath, destPath);
34
- } else {
35
- fs.copyFileSync(srcPath, destPath);
36
- }
37
- }
38
- }
@@ -1,197 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import { getRRCEHome } from '../../lib/paths';
4
- import { type DetectedProject, getProjectFolders } from '../../lib/detection';
5
-
6
- interface VSCodeWorkspaceFolder {
7
- path: string;
8
- name?: string;
9
- }
10
-
11
- interface VSCodeWorkspace {
12
- folders: VSCodeWorkspaceFolder[];
13
- settings?: Record<string, unknown>;
14
- }
15
-
16
- // Reference folder group prefix - used to visually group linked folders
17
- const REFERENCE_GROUP_PREFIX = '📁 References';
18
-
19
- /**
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
27
- */
28
- export function generateVSCodeWorkspace(
29
- workspacePath: string,
30
- workspaceName: string,
31
- linkedProjects: string[] | DetectedProject[],
32
- customGlobalPath?: string
33
- ) {
34
- const workspaceFilePath = path.join(workspacePath, `${workspaceName}.code-workspace`);
35
-
36
- let workspace: VSCodeWorkspace;
37
-
38
- // Check if workspace file already exists
39
- if (fs.existsSync(workspaceFilePath)) {
40
- try {
41
- const content = fs.readFileSync(workspaceFilePath, 'utf-8');
42
- workspace = JSON.parse(content);
43
- } catch {
44
- // If parse fails, create new
45
- workspace = { folders: [], settings: {} };
46
- }
47
- } else {
48
- workspace = { folders: [], settings: {} };
49
- }
50
-
51
- // Initialize settings if not present
52
- if (!workspace.settings) {
53
- workspace.settings = {};
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
- }
76
-
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
- }
132
- }
133
- }
134
-
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
157
- fs.writeFileSync(workspaceFilePath, JSON.stringify(workspace, null, 2));
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
- }
package/src/index.ts DELETED
@@ -1,11 +0,0 @@
1
- import { runWizard } from './commands/wizard/index';
2
- import { runSelector } from './commands/selector';
3
-
4
- // Get command from args
5
- const command = process.argv[2];
6
-
7
- if (!command || command === 'wizard') {
8
- runWizard();
9
- } else {
10
- runSelector();
11
- }
@@ -1,190 +0,0 @@
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 };
@@ -1,235 +0,0 @@
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
- }