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.
- package/bin/rrce-workflow.js +3 -33
- package/dist/commands/selector.d.ts +1 -0
- package/dist/commands/selector.js +29 -0
- package/dist/commands/wizard/index.d.ts +1 -0
- package/dist/commands/wizard/index.js +86 -0
- package/dist/commands/wizard/link-flow.d.ts +5 -0
- package/dist/commands/wizard/link-flow.js +97 -0
- package/dist/commands/wizard/setup-flow.d.ts +4 -0
- package/dist/commands/wizard/setup-flow.js +262 -0
- package/dist/commands/wizard/sync-flow.d.ts +4 -0
- package/dist/commands/wizard/sync-flow.js +67 -0
- package/dist/commands/wizard/update-flow.d.ts +4 -0
- package/dist/commands/wizard/update-flow.js +85 -0
- package/dist/commands/wizard/utils.d.ts +9 -0
- package/dist/commands/wizard/utils.js +33 -0
- package/dist/commands/wizard/vscode.d.ts +15 -0
- package/dist/commands/wizard/vscode.js +148 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1181 -0
- package/dist/lib/autocomplete-prompt.d.ts +14 -0
- package/dist/lib/autocomplete-prompt.js +167 -0
- package/dist/lib/detection.d.ts +44 -0
- package/dist/lib/detection.js +185 -0
- package/dist/lib/git.d.ts +12 -0
- package/dist/lib/git.js +37 -0
- package/dist/lib/paths.d.ts +108 -0
- package/dist/lib/paths.js +296 -0
- package/dist/lib/prompts.d.ts +18 -0
- package/dist/lib/prompts.js +62 -0
- package/dist/types/prompt.d.ts +54 -0
- package/dist/types/prompt.js +20 -0
- package/package.json +10 -7
- package/src/commands/selector.ts +0 -42
- package/src/commands/wizard/index.ts +0 -114
- package/src/commands/wizard/link-flow.ts +0 -118
- package/src/commands/wizard/setup-flow.ts +0 -347
- package/src/commands/wizard/sync-flow.ts +0 -93
- package/src/commands/wizard/update-flow.ts +0 -124
- package/src/commands/wizard/utils.ts +0 -38
- package/src/commands/wizard/vscode.ts +0 -197
- package/src/index.ts +0 -11
- package/src/lib/autocomplete-prompt.ts +0 -190
- package/src/lib/detection.ts +0 -235
- package/src/lib/git.ts +0 -37
- package/src/lib/paths.ts +0 -332
- package/src/lib/prompts.ts +0 -73
- 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 };
|
package/src/lib/detection.ts
DELETED
|
@@ -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
|
-
}
|