prr-kit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +226 -0
  3. package/docs/assets/banner.svg +179 -0
  4. package/package.json +60 -0
  5. package/src/core/agents/prr-master.agent.yaml +80 -0
  6. package/src/core/module.yaml +19 -0
  7. package/src/core/tasks/help.md +37 -0
  8. package/src/core/tasks/workflow.xml +22 -0
  9. package/src/core/workflows/party-mode/steps/step-01-load-reviewers.md +68 -0
  10. package/src/core/workflows/party-mode/steps/step-02-discussion.md +125 -0
  11. package/src/core/workflows/party-mode/workflow.md +35 -0
  12. package/src/prr/agents/architecture-reviewer.agent.yaml +45 -0
  13. package/src/prr/agents/general-reviewer.agent.yaml +48 -0
  14. package/src/prr/agents/performance-reviewer.agent.yaml +45 -0
  15. package/src/prr/agents/security-reviewer.agent.yaml +43 -0
  16. package/src/prr/data/review-types.csv +39 -0
  17. package/src/prr/module.yaml +38 -0
  18. package/src/prr/workflows/0-setup/collect-project-context/steps/step-01-scan-configs.md +106 -0
  19. package/src/prr/workflows/0-setup/collect-project-context/steps/step-02-extract-rules.md +131 -0
  20. package/src/prr/workflows/0-setup/collect-project-context/steps/step-03-ask-context.md +194 -0
  21. package/src/prr/workflows/0-setup/collect-project-context/steps/step-04-save-context.md +161 -0
  22. package/src/prr/workflows/0-setup/collect-project-context/workflow.md +58 -0
  23. package/src/prr/workflows/1-discover/select-pr/steps/step-01-fetch.md +68 -0
  24. package/src/prr/workflows/1-discover/select-pr/steps/step-02-list-branches.md +95 -0
  25. package/src/prr/workflows/1-discover/select-pr/steps/step-03-select.md +127 -0
  26. package/src/prr/workflows/1-discover/select-pr/steps/step-04-load-diff.md +79 -0
  27. package/src/prr/workflows/1-discover/select-pr/steps/step-05-confirm.md +76 -0
  28. package/src/prr/workflows/1-discover/select-pr/workflow.md +36 -0
  29. package/src/prr/workflows/2-analyze/describe-pr/steps/step-01-load-context.md +37 -0
  30. package/src/prr/workflows/2-analyze/describe-pr/steps/step-02-classify.md +50 -0
  31. package/src/prr/workflows/2-analyze/describe-pr/steps/step-03-walkthrough.md +41 -0
  32. package/src/prr/workflows/2-analyze/describe-pr/steps/step-04-output.md +50 -0
  33. package/src/prr/workflows/2-analyze/describe-pr/templates/pr-description.template.md +51 -0
  34. package/src/prr/workflows/2-analyze/describe-pr/workflow.md +28 -0
  35. package/src/prr/workflows/3-review/architecture-review/checklist.md +22 -0
  36. package/src/prr/workflows/3-review/architecture-review/instructions.xml +68 -0
  37. package/src/prr/workflows/3-review/architecture-review/workflow.yaml +18 -0
  38. package/src/prr/workflows/3-review/general-review/checklist.md +23 -0
  39. package/src/prr/workflows/3-review/general-review/instructions.xml +68 -0
  40. package/src/prr/workflows/3-review/general-review/workflow.yaml +18 -0
  41. package/src/prr/workflows/3-review/performance-review/checklist.md +22 -0
  42. package/src/prr/workflows/3-review/performance-review/instructions.xml +68 -0
  43. package/src/prr/workflows/3-review/performance-review/workflow.yaml +18 -0
  44. package/src/prr/workflows/3-review/security-review/checklist.md +25 -0
  45. package/src/prr/workflows/3-review/security-review/data/owasp-checklist.csv +19 -0
  46. package/src/prr/workflows/3-review/security-review/instructions.xml +70 -0
  47. package/src/prr/workflows/3-review/security-review/workflow.yaml +19 -0
  48. package/src/prr/workflows/4-improve/improve-code/checklist.md +18 -0
  49. package/src/prr/workflows/4-improve/improve-code/instructions.xml +59 -0
  50. package/src/prr/workflows/4-improve/improve-code/workflow.yaml +18 -0
  51. package/src/prr/workflows/5-ask/ask-code/steps/step-01-load-context.md +37 -0
  52. package/src/prr/workflows/5-ask/ask-code/steps/step-02-answer.md +36 -0
  53. package/src/prr/workflows/5-ask/ask-code/workflow.md +31 -0
  54. package/src/prr/workflows/6-report/generate-report/steps/step-01-collect.md +42 -0
  55. package/src/prr/workflows/6-report/generate-report/steps/step-02-organize.md +38 -0
  56. package/src/prr/workflows/6-report/generate-report/steps/step-03-write.md +44 -0
  57. package/src/prr/workflows/6-report/generate-report/templates/review-report.template.md +78 -0
  58. package/src/prr/workflows/6-report/generate-report/workflow.md +26 -0
  59. package/src/prr/workflows/6-report/post-comments/steps/step-01-format.md +166 -0
  60. package/src/prr/workflows/6-report/post-comments/steps/step-02-post.md +97 -0
  61. package/src/prr/workflows/6-report/post-comments/workflow.md +45 -0
  62. package/src/prr/workflows/quick/workflow.md +244 -0
  63. package/tools/cli/commands/install.js +66 -0
  64. package/tools/cli/commands/status.js +36 -0
  65. package/tools/cli/commands/uninstall.js +38 -0
  66. package/tools/cli/installers/lib/core/config-collector.js +47 -0
  67. package/tools/cli/installers/lib/core/detector.js +46 -0
  68. package/tools/cli/installers/lib/core/installer.js +162 -0
  69. package/tools/cli/installers/lib/core/manifest-generator.js +172 -0
  70. package/tools/cli/installers/lib/core/manifest.js +62 -0
  71. package/tools/cli/installers/lib/ide/_base-ide.js +36 -0
  72. package/tools/cli/installers/lib/ide/_config-driven.js +167 -0
  73. package/tools/cli/installers/lib/ide/manager.js +97 -0
  74. package/tools/cli/installers/lib/ide/platform-codes.yaml +76 -0
  75. package/tools/cli/installers/lib/ide/shared/path-utils.js +11 -0
  76. package/tools/cli/installers/lib/ide/templates/combined/default-agent.md +16 -0
  77. package/tools/cli/installers/lib/ide/templates/combined/default-workflow.md +7 -0
  78. package/tools/cli/installers/lib/ide/templates/combined/windsurf-workflow.md +5 -0
  79. package/tools/cli/lib/agent/compiler.js +123 -0
  80. package/tools/cli/lib/agent/template-engine.js +73 -0
  81. package/tools/cli/lib/cli-utils.js +32 -0
  82. package/tools/cli/lib/prompts.js +15 -0
  83. package/tools/cli/lib/ui.js +132 -0
  84. package/tools/cli/lib/xml-utils.js +24 -0
  85. package/tools/cli/prr-cli.js +36 -0
  86. package/tools/prr-npx-wrapper.js +6 -0
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Manifest Generator — scans installed modules and generates CSV manifests
3
+ */
4
+ const path = require('node:path');
5
+ const fs = require('fs-extra');
6
+ const yaml = require('yaml');
7
+ const { glob } = require('glob');
8
+
9
+ class ManifestGenerator {
10
+ constructor() {
11
+ this.workflows = [];
12
+ this.agents = [];
13
+ this.tasks = [];
14
+ this.prrDir = null;
15
+ }
16
+
17
+ async generateManifests(prrDir, selectedModules, options = {}) {
18
+ const cfgDir = path.join(prrDir, '_config');
19
+ await fs.ensureDir(cfgDir);
20
+
21
+ this.prrDir = prrDir;
22
+
23
+ const allModules = [...new Set(['core', ...selectedModules])];
24
+
25
+ await this.collectWorkflows(allModules);
26
+ await this.collectAgents(allModules);
27
+ await this.collectTasks(allModules);
28
+
29
+ await this.writeWorkflowManifest(cfgDir);
30
+ await this.writeAgentManifest(cfgDir);
31
+ await this.writeTaskManifest(cfgDir);
32
+
33
+ return {
34
+ workflows: this.workflows.length,
35
+ agents: this.agents.length,
36
+ tasks: this.tasks.length,
37
+ };
38
+ }
39
+
40
+ async collectWorkflows(modules) {
41
+ this.workflows = [];
42
+ for (const mod of modules) {
43
+ const workflowsPath = path.join(this.prrDir, mod, 'workflows');
44
+ if (!(await fs.pathExists(workflowsPath))) continue;
45
+
46
+ const files = await glob('**/workflow{.md,.yaml}', { cwd: workflowsPath });
47
+ for (const file of files) {
48
+ const fullPath = path.join(workflowsPath, file);
49
+ const info = await this.parseWorkflowFile(fullPath, mod, file);
50
+ if (info) this.workflows.push(info);
51
+ }
52
+ }
53
+ }
54
+
55
+ async parseWorkflowFile(filePath, moduleName, relPath) {
56
+ try {
57
+ const content = await fs.readFile(filePath, 'utf8');
58
+ let name = path.basename(path.dirname(relPath));
59
+ let description = '';
60
+
61
+ if (filePath.endsWith('.yaml')) {
62
+ const data = yaml.parse(content);
63
+ name = data.name || name;
64
+ description = data.description || '';
65
+ } else {
66
+ // Parse markdown frontmatter
67
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
68
+ if (match) {
69
+ try {
70
+ const fm = yaml.parse(match[1]);
71
+ name = fm.name || name;
72
+ description = fm.description || '';
73
+ } catch { /* ignore */ }
74
+ }
75
+ }
76
+
77
+ return {
78
+ module: moduleName,
79
+ name,
80
+ description,
81
+ path: path.join('_prr', moduleName, 'workflows', relPath).replaceAll('\\', '/'),
82
+ };
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ async collectAgents(modules) {
89
+ this.agents = [];
90
+ for (const mod of modules) {
91
+ const agentsPath = path.join(this.prrDir, mod, 'agents');
92
+ if (!(await fs.pathExists(agentsPath))) continue;
93
+
94
+ const files = await glob('**/*.md', { cwd: agentsPath });
95
+ for (const file of files) {
96
+ const fullPath = path.join(agentsPath, file);
97
+ const info = await this.parseAgentFile(fullPath, mod, file);
98
+ if (info) this.agents.push(info);
99
+ }
100
+ }
101
+ }
102
+
103
+ async parseAgentFile(filePath, moduleName, relPath) {
104
+ try {
105
+ const content = await fs.readFile(filePath, 'utf8');
106
+ // Extract XML agent attributes
107
+ const nameMatch = content.match(/name="([^"]+)"/);
108
+ const titleMatch = content.match(/title="([^"]+)"/);
109
+ const iconMatch = content.match(/icon="([^"]+)"/);
110
+ const capMatch = content.match(/capabilities="([^"]+)"/);
111
+
112
+ return {
113
+ module: moduleName,
114
+ name: nameMatch?.[1] || path.basename(relPath, '.md'),
115
+ title: titleMatch?.[1] || '',
116
+ icon: iconMatch?.[1] || '🔍',
117
+ capabilities: capMatch?.[1] || '',
118
+ path: path.join('_prr', moduleName, 'agents', relPath).replaceAll('\\', '/'),
119
+ };
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+
125
+ async collectTasks(modules) {
126
+ this.tasks = [];
127
+ for (const mod of modules) {
128
+ const tasksPath = path.join(this.prrDir, mod, 'tasks');
129
+ if (!(await fs.pathExists(tasksPath))) continue;
130
+
131
+ const files = await glob('**/*.{md,xml}', { cwd: tasksPath });
132
+ for (const file of files) {
133
+ this.tasks.push({
134
+ module: mod,
135
+ name: path.basename(file, path.extname(file)),
136
+ path: path.join('_prr', mod, 'tasks', file).replaceAll('\\', '/'),
137
+ });
138
+ }
139
+ }
140
+ }
141
+
142
+ cleanForCSV(text) {
143
+ if (!text) return '';
144
+ return text.trim().replaceAll(/\s+/g, ' ').replaceAll('"', '""');
145
+ }
146
+
147
+ async writeWorkflowManifest(cfgDir) {
148
+ const rows = ['module,name,description,path'];
149
+ for (const w of this.workflows) {
150
+ rows.push(`"${this.cleanForCSV(w.module)}","${this.cleanForCSV(w.name)}","${this.cleanForCSV(w.description)}","${this.cleanForCSV(w.path)}"`);
151
+ }
152
+ await fs.writeFile(path.join(cfgDir, 'workflow-manifest.csv'), rows.join('\n') + '\n', 'utf8');
153
+ }
154
+
155
+ async writeAgentManifest(cfgDir) {
156
+ const rows = ['module,name,title,icon,capabilities,path'];
157
+ for (const a of this.agents) {
158
+ rows.push(`"${this.cleanForCSV(a.module)}","${this.cleanForCSV(a.name)}","${this.cleanForCSV(a.title)}","${this.cleanForCSV(a.icon)}","${this.cleanForCSV(a.capabilities)}","${this.cleanForCSV(a.path)}"`);
159
+ }
160
+ await fs.writeFile(path.join(cfgDir, 'agent-manifest.csv'), rows.join('\n') + '\n', 'utf8');
161
+ }
162
+
163
+ async writeTaskManifest(cfgDir) {
164
+ const rows = ['module,name,path'];
165
+ for (const t of this.tasks) {
166
+ rows.push(`"${this.cleanForCSV(t.module)}","${this.cleanForCSV(t.name)}","${this.cleanForCSV(t.path)}"`);
167
+ }
168
+ await fs.writeFile(path.join(cfgDir, 'task-manifest.csv'), rows.join('\n') + '\n', 'utf8');
169
+ }
170
+ }
171
+
172
+ module.exports = { ManifestGenerator };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Manifest — read/write installation manifest.yaml
3
+ */
4
+ const path = require('node:path');
5
+ const fs = require('fs-extra');
6
+ const yaml = require('yaml');
7
+
8
+ const packageJson = require('../../../../../package.json');
9
+
10
+ class Manifest {
11
+ async create(prrDir, data) {
12
+ const manifestPath = path.join(prrDir, '_config', 'manifest.yaml');
13
+ await fs.ensureDir(path.dirname(manifestPath));
14
+
15
+ const version = data.version || packageJson.version;
16
+ const now = data.installDate || new Date().toISOString();
17
+
18
+ const moduleDetails = (data.modules || []).map((name) => ({
19
+ name,
20
+ version,
21
+ installDate: now,
22
+ lastUpdated: now,
23
+ source: 'built-in',
24
+ }));
25
+
26
+ const manifestData = {
27
+ installation: {
28
+ version,
29
+ installDate: now,
30
+ lastUpdated: now,
31
+ },
32
+ modules: moduleDetails,
33
+ ides: data.ides || [],
34
+ };
35
+
36
+ const content = yaml.stringify(manifestData, { indent: 2, lineWidth: 0 });
37
+ await fs.writeFile(manifestPath, content, 'utf8');
38
+ return { success: true, path: manifestPath };
39
+ }
40
+
41
+ async read(prrDir) {
42
+ const manifestPath = path.join(prrDir, '_config', 'manifest.yaml');
43
+ if (!(await fs.pathExists(manifestPath))) return null;
44
+
45
+ try {
46
+ const content = await fs.readFile(manifestPath, 'utf8');
47
+ const data = yaml.parse(content);
48
+ const modules = (data.modules || []).map((m) => (typeof m === 'string' ? m : m.name));
49
+ return {
50
+ version: data.installation?.version,
51
+ installDate: data.installation?.installDate,
52
+ lastUpdated: data.installation?.lastUpdated,
53
+ modules,
54
+ ides: data.ides || [],
55
+ };
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+ }
61
+
62
+ module.exports = { Manifest };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Base IDE Setup — abstract class for IDE handlers
3
+ */
4
+ const fs = require('fs-extra');
5
+ const { PRR_FOLDER_NAME } = require('./shared/path-utils');
6
+
7
+ class BaseIdeSetup {
8
+ constructor(name, displayName, preferred = false) {
9
+ this.name = name;
10
+ this.displayName = displayName;
11
+ this.preferred = preferred;
12
+ this.prrFolderName = PRR_FOLDER_NAME;
13
+ }
14
+
15
+ setPrrFolderName(name) {
16
+ this.prrFolderName = name;
17
+ }
18
+
19
+ async ensureDir(dirPath) {
20
+ await fs.ensureDir(dirPath);
21
+ }
22
+
23
+ async setup(projectDir, prrDir, options = {}) {
24
+ throw new Error(`setup() not implemented for IDE: ${this.name}`);
25
+ }
26
+
27
+ async cleanup(projectDir, options = {}) {
28
+ // Default: no-op; subclasses override to remove IDE-specific files
29
+ }
30
+
31
+ async detect(projectDir) {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ module.exports = { BaseIdeSetup };
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Config-Driven IDE Setup — handles standard IDE installation patterns
3
+ * based on platform-codes.yaml configuration
4
+ */
5
+ const path = require('node:path');
6
+ const fs = require('fs-extra');
7
+ const { BaseIdeSetup } = require('./_base-ide');
8
+ const { glob } = require('glob');
9
+
10
+ class ConfigDrivenIdeSetup extends BaseIdeSetup {
11
+ constructor(platformCode, platformConfig) {
12
+ super(platformCode, platformConfig.name, platformConfig.preferred || false);
13
+ this.platformConfig = platformConfig;
14
+ this.installerConfig = platformConfig.installer || null;
15
+ }
16
+
17
+ async setup(projectDir, prrDir, options = {}) {
18
+ if (!this.installerConfig) return { success: false, reason: 'no-config' };
19
+
20
+ await this.cleanup(projectDir, options);
21
+
22
+ const { target_dir, template_type } = this.installerConfig;
23
+ const targetPath = path.join(projectDir, target_dir);
24
+ await this.ensureDir(targetPath);
25
+
26
+ const results = { agents: 0, workflows: 0 };
27
+ const installedFiles = [];
28
+ const selectedModules = options.selectedModules || ['prr'];
29
+ const allModules = ['core', ...selectedModules.filter((m) => m !== 'core')];
30
+
31
+ // Install agent launchers
32
+ for (const mod of allModules) {
33
+ const agentsDir = path.join(prrDir, mod, 'agents');
34
+ if (!(await fs.pathExists(agentsDir))) continue;
35
+
36
+ const agentFiles = await glob('**/*.md', { cwd: agentsDir });
37
+ for (const agentFile of agentFiles) {
38
+ const agentPath = path.join(agentsDir, agentFile);
39
+ const agentContent = await fs.readFile(agentPath, 'utf8');
40
+
41
+ // Extract name and description from frontmatter
42
+ const nameMatch = agentContent.match(/^name:\s*["']?([^"'\n]+)["']?/m);
43
+ const descMatch = agentContent.match(/^description:\s*["']?([^"'\n]+)["']?/m);
44
+
45
+ // Skip agents marked as internal (no-launcher)
46
+ if (/^no-launcher:\s*true/m.test(agentContent)) continue;
47
+
48
+ const agentName = nameMatch?.[1]?.trim() || path.basename(agentFile, '.md');
49
+ const agentDesc = descMatch?.[1]?.trim() || `${agentName} reviewer`;
50
+ const relAgentPath = `${mod}/agents/${agentFile}`.replaceAll('\\', '/');
51
+
52
+ const launcherContent = this.generateAgentLauncher(agentName, agentDesc, relAgentPath, template_type);
53
+ // Use agent basename directly — e.g. prr-master.md → /prr-master
54
+ const launcherFileName = `${path.basename(agentFile, '.md')}.md`;
55
+ await fs.writeFile(path.join(targetPath, launcherFileName), launcherContent, 'utf8');
56
+ installedFiles.push(launcherFileName);
57
+ results.agents++;
58
+ }
59
+ }
60
+
61
+ // Install workflow launchers
62
+ for (const mod of allModules) {
63
+ const workflowsDir = path.join(prrDir, mod, 'workflows');
64
+ if (!(await fs.pathExists(workflowsDir))) continue;
65
+
66
+ const workflowFiles = await glob('**/workflow{.md,.yaml}', { cwd: workflowsDir });
67
+ for (const wfFile of workflowFiles) {
68
+ const wfPath = path.join(workflowsDir, wfFile);
69
+ const wfContent = await fs.readFile(wfPath, 'utf8');
70
+
71
+ let wfName = path.basename(path.dirname(wfFile));
72
+ let wfDesc = '';
73
+
74
+ if (wfFile.endsWith('.yaml')) {
75
+ const yaml = require('yaml');
76
+ try {
77
+ const data = yaml.parse(wfContent);
78
+ wfName = data.name || wfName;
79
+ wfDesc = data.description || '';
80
+ } catch { /* ignore */ }
81
+ } else {
82
+ const normalized = wfContent.replace(/\r\n/g, '\n');
83
+ const fmMatch = normalized.match(/^---\n([\s\S]*?)\n---/);
84
+ if (fmMatch) {
85
+ try {
86
+ const yaml = require('yaml');
87
+ const fm = yaml.parse(fmMatch[1]);
88
+ wfName = fm.name || wfName;
89
+ wfDesc = fm.description || '';
90
+ } catch { /* ignore */ }
91
+ }
92
+ }
93
+
94
+ const relWfPath = `${mod}/workflows/${wfFile}`.replaceAll('\\', '/');
95
+ const launcherContent = this.generateWorkflowLauncher(wfName, wfDesc, relWfPath, template_type);
96
+
97
+ // Use leaf directory name prefixed with prr- — e.g. prr-select-pr.md → /prr-select-pr
98
+ const dirParts = path.dirname(wfFile).split(path.sep).filter(Boolean);
99
+ const leafDir = dirParts.at(-1) || wfName;
100
+ const launcherFileName = `prr-${leafDir}.md`;
101
+ await fs.writeFile(path.join(targetPath, launcherFileName), launcherContent, 'utf8');
102
+ installedFiles.push(launcherFileName);
103
+ results.workflows++;
104
+ }
105
+ }
106
+
107
+ // Write manifest so cleanup knows exactly which files to remove
108
+ await fs.writeFile(
109
+ path.join(targetPath, '.prr-installed.json'),
110
+ JSON.stringify(installedFiles, null, 2),
111
+ 'utf8'
112
+ );
113
+
114
+ return { success: true, results };
115
+ }
116
+
117
+ generateAgentLauncher(name, description, agentRelPath, templateType) {
118
+ const template = this.loadTemplate(`${templateType}-agent`) || this.loadTemplate('default-agent');
119
+ return template
120
+ .replaceAll('{{name}}', name)
121
+ .replaceAll('{{description}}', description)
122
+ .replaceAll('{{path}}', agentRelPath)
123
+ .replaceAll('{{prrFolderName}}', this.prrFolderName);
124
+ }
125
+
126
+ generateWorkflowLauncher(name, description, wfRelPath, templateType) {
127
+ const template = this.loadTemplate(`${templateType}-workflow`) || this.loadTemplate('default-workflow');
128
+ return template
129
+ .replaceAll('{{name}}', name)
130
+ .replaceAll('{{description}}', description)
131
+ .replaceAll('{{path}}', wfRelPath)
132
+ .replaceAll('{{prrFolderName}}', this.prrFolderName);
133
+ }
134
+
135
+ loadTemplate(templateName) {
136
+ const templatePath = path.join(__dirname, 'templates', 'combined', `${templateName}.md`);
137
+ try {
138
+ return require('fs-extra').readFileSync(templatePath, 'utf8');
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+
144
+ async cleanup(projectDir, options = {}) {
145
+ if (!this.installerConfig?.target_dir) return;
146
+ const targetPath = path.join(projectDir, this.installerConfig.target_dir);
147
+ if (!(await fs.pathExists(targetPath))) return;
148
+
149
+ const manifestPath = path.join(targetPath, '.prr-installed.json');
150
+ if (await fs.pathExists(manifestPath)) {
151
+ // Remove exactly the files recorded during the last install
152
+ const installedFiles = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
153
+ for (const file of installedFiles) {
154
+ await fs.remove(path.join(targetPath, file));
155
+ }
156
+ await fs.remove(manifestPath);
157
+ } else {
158
+ // Fallback for installations before manifest tracking was added
159
+ const files = await glob('prr-*.md', { cwd: targetPath });
160
+ for (const file of files) {
161
+ await fs.remove(path.join(targetPath, file));
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ module.exports = { ConfigDrivenIdeSetup };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * IDE Manager — dynamically loads and manages IDE handlers
3
+ */
4
+ const path = require('node:path');
5
+ const fs = require('fs-extra');
6
+ const yaml = require('yaml');
7
+ const { PRR_FOLDER_NAME } = require('./shared/path-utils');
8
+
9
+ class IdeManager {
10
+ constructor() {
11
+ this.handlers = new Map();
12
+ this._initialized = false;
13
+ this.prrFolderName = PRR_FOLDER_NAME;
14
+ }
15
+
16
+ setPrrFolderName(name) {
17
+ this.prrFolderName = name;
18
+ for (const handler of this.handlers.values()) {
19
+ if (typeof handler.setPrrFolderName === 'function') {
20
+ handler.setPrrFolderName(name);
21
+ }
22
+ }
23
+ }
24
+
25
+ async ensureInitialized() {
26
+ if (!this._initialized) {
27
+ await this.loadHandlers();
28
+ this._initialized = true;
29
+ }
30
+ }
31
+
32
+ async loadHandlers() {
33
+ const platformCodesPath = path.join(__dirname, 'platform-codes.yaml');
34
+ const content = await fs.readFile(platformCodesPath, 'utf8');
35
+ const config = yaml.parse(content);
36
+
37
+ const { ConfigDrivenIdeSetup } = require('./_config-driven');
38
+
39
+ for (const [code, info] of Object.entries(config.platforms || {})) {
40
+ if (!info.installer) continue;
41
+ const handler = new ConfigDrivenIdeSetup(code, info);
42
+ handler.setPrrFolderName(this.prrFolderName);
43
+ this.handlers.set(code, handler);
44
+ }
45
+ }
46
+
47
+ getAvailableIdes() {
48
+ const ides = [];
49
+ for (const [key, handler] of this.handlers) {
50
+ ides.push({
51
+ value: key,
52
+ name: handler.displayName || handler.name || key,
53
+ preferred: handler.preferred || false,
54
+ });
55
+ }
56
+ return ides.sort((a, b) => {
57
+ if (a.preferred && !b.preferred) return -1;
58
+ if (!a.preferred && b.preferred) return 1;
59
+ return a.name.localeCompare(b.name);
60
+ });
61
+ }
62
+
63
+ getPreferredIdes() {
64
+ return this.getAvailableIdes().filter((i) => i.preferred);
65
+ }
66
+
67
+ async setup(ideName, projectDir, prrDir, options = {}) {
68
+ await this.ensureInitialized();
69
+ const handler = this.handlers.get(ideName.toLowerCase());
70
+ if (!handler) return { success: false, ide: ideName, error: 'unsupported IDE' };
71
+
72
+ try {
73
+ const result = await handler.setup(projectDir, prrDir, options);
74
+ let detail = '';
75
+ if (result && result.results) {
76
+ const parts = [];
77
+ if (result.results.agents > 0) parts.push(`${result.results.agents} agents`);
78
+ if (result.results.workflows > 0) parts.push(`${result.results.workflows} workflows`);
79
+ detail = parts.join(', ');
80
+ }
81
+ return { success: true, ide: ideName, detail };
82
+ } catch (error) {
83
+ return { success: false, ide: ideName, error: error.message };
84
+ }
85
+ }
86
+
87
+ async cleanup(projectDir, options = {}) {
88
+ await this.ensureInitialized();
89
+ for (const handler of this.handlers.values()) {
90
+ try {
91
+ await handler.cleanup(projectDir, options);
92
+ } catch { /* ignore */ }
93
+ }
94
+ }
95
+ }
96
+
97
+ module.exports = { IdeManager };
@@ -0,0 +1,76 @@
1
+ # PR Review Platform Codes Configuration
2
+ # IDE/tool registry — defines where to install agent/workflow launchers
3
+
4
+ platforms:
5
+ claude-code:
6
+ name: "Claude Code"
7
+ preferred: true
8
+ category: cli
9
+ description: "Anthropic's official CLI for Claude"
10
+ installer:
11
+ target_dir: .claude/commands
12
+ template_type: default
13
+
14
+ cursor:
15
+ name: "Cursor"
16
+ preferred: true
17
+ category: ide
18
+ description: "AI-first code editor"
19
+ installer:
20
+ target_dir: .cursor/commands
21
+ template_type: default
22
+
23
+ windsurf:
24
+ name: "Windsurf"
25
+ preferred: true
26
+ category: ide
27
+ description: "AI-powered IDE with cascade flows"
28
+ installer:
29
+ target_dir: .windsurf/workflows
30
+ template_type: windsurf
31
+
32
+ cline:
33
+ name: "Cline"
34
+ preferred: false
35
+ category: ide
36
+ description: "AI coding assistant"
37
+ installer:
38
+ target_dir: .clinerules/workflows
39
+ template_type: windsurf
40
+
41
+ roo:
42
+ name: "Roo Cline"
43
+ preferred: false
44
+ category: ide
45
+ description: "Enhanced Cline fork"
46
+ installer:
47
+ target_dir: .roo/commands
48
+ template_type: default
49
+
50
+ gemini:
51
+ name: "Gemini CLI"
52
+ preferred: false
53
+ category: cli
54
+ description: "Google's CLI for Gemini"
55
+ installer:
56
+ target_dir: .gemini/commands
57
+ template_type: default
58
+
59
+ kiro:
60
+ name: "Kiro"
61
+ preferred: false
62
+ category: ide
63
+ description: "Amazon's AI-powered IDE"
64
+ installer:
65
+ target_dir: .kiro/steering
66
+ template_type: default
67
+
68
+ categories:
69
+ ide:
70
+ name: "Integrated Development Environment"
71
+ cli:
72
+ name: "Command Line Interface"
73
+
74
+ conventions:
75
+ code_format: "lowercase-kebab-case"
76
+ name_format: "Title Case"
@@ -0,0 +1,11 @@
1
+ const PRR_FOLDER_NAME = '_prr';
2
+
3
+ function toColonPath(p) {
4
+ return p.replaceAll('/', ':').replaceAll('\\', ':');
5
+ }
6
+
7
+ function toDashPath(p) {
8
+ return p.replaceAll('/', '-').replaceAll('\\', '-').replaceAll('.', '-');
9
+ }
10
+
11
+ module.exports = { PRR_FOLDER_NAME, toColonPath, toDashPath };
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: '{{name}}'
3
+ description: '{{description}}'
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
8
+
9
+ <agent-activation CRITICAL="TRUE">
10
+ 1. LOAD the FULL agent file from {project-root}/_prr/{{path}}
11
+ 2. READ its entire contents - this contains the complete agent persona, menu, and instructions
12
+ 3. FOLLOW every step in the <activation> section precisely
13
+ 4. DISPLAY the welcome/greeting as instructed
14
+ 5. PRESENT the numbered menu
15
+ 6. WAIT for user input before proceeding
16
+ </agent-activation>
@@ -0,0 +1,7 @@
1
+ ---
2
+ name: '{{name}}'
3
+ description: '{{description}}'
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL @{project-root}/{{prrFolderName}}/{{path}}, READ its entire contents and follow its directions exactly!
@@ -0,0 +1,5 @@
1
+ ---
2
+ description: '{{description}}'
3
+ ---
4
+
5
+ IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL @{project-root}/{{prrFolderName}}/{{path}}, READ its entire contents and follow its directions exactly!