get-research-done 1.1.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.
- package/LICENSE +21 -0
- package/README.md +560 -0
- package/agents/grd-architect.md +789 -0
- package/agents/grd-codebase-mapper.md +738 -0
- package/agents/grd-critic.md +1065 -0
- package/agents/grd-debugger.md +1203 -0
- package/agents/grd-evaluator.md +948 -0
- package/agents/grd-executor.md +784 -0
- package/agents/grd-explorer.md +2063 -0
- package/agents/grd-graduator.md +484 -0
- package/agents/grd-integration-checker.md +423 -0
- package/agents/grd-phase-researcher.md +641 -0
- package/agents/grd-plan-checker.md +745 -0
- package/agents/grd-planner.md +1386 -0
- package/agents/grd-project-researcher.md +865 -0
- package/agents/grd-research-synthesizer.md +256 -0
- package/agents/grd-researcher.md +2361 -0
- package/agents/grd-roadmapper.md +605 -0
- package/agents/grd-verifier.md +778 -0
- package/bin/install.js +1294 -0
- package/commands/grd/add-phase.md +207 -0
- package/commands/grd/add-todo.md +193 -0
- package/commands/grd/architect.md +283 -0
- package/commands/grd/audit-milestone.md +277 -0
- package/commands/grd/check-todos.md +228 -0
- package/commands/grd/complete-milestone.md +136 -0
- package/commands/grd/debug.md +169 -0
- package/commands/grd/discuss-phase.md +86 -0
- package/commands/grd/evaluate.md +1095 -0
- package/commands/grd/execute-phase.md +339 -0
- package/commands/grd/explore.md +258 -0
- package/commands/grd/graduate.md +323 -0
- package/commands/grd/help.md +482 -0
- package/commands/grd/insert-phase.md +227 -0
- package/commands/grd/insights.md +231 -0
- package/commands/grd/join-discord.md +18 -0
- package/commands/grd/list-phase-assumptions.md +50 -0
- package/commands/grd/map-codebase.md +71 -0
- package/commands/grd/new-milestone.md +721 -0
- package/commands/grd/new-project.md +1008 -0
- package/commands/grd/pause-work.md +134 -0
- package/commands/grd/plan-milestone-gaps.md +295 -0
- package/commands/grd/plan-phase.md +525 -0
- package/commands/grd/progress.md +364 -0
- package/commands/grd/quick-explore.md +236 -0
- package/commands/grd/quick.md +309 -0
- package/commands/grd/remove-phase.md +349 -0
- package/commands/grd/research-phase.md +200 -0
- package/commands/grd/research.md +681 -0
- package/commands/grd/resume-work.md +40 -0
- package/commands/grd/set-profile.md +106 -0
- package/commands/grd/settings.md +136 -0
- package/commands/grd/update.md +172 -0
- package/commands/grd/verify-work.md +219 -0
- package/get-research-done/config/default.json +15 -0
- package/get-research-done/references/checkpoints.md +1078 -0
- package/get-research-done/references/continuation-format.md +249 -0
- package/get-research-done/references/git-integration.md +254 -0
- package/get-research-done/references/model-profiles.md +73 -0
- package/get-research-done/references/planning-config.md +94 -0
- package/get-research-done/references/questioning.md +141 -0
- package/get-research-done/references/tdd.md +263 -0
- package/get-research-done/references/ui-brand.md +160 -0
- package/get-research-done/references/verification-patterns.md +612 -0
- package/get-research-done/templates/DEBUG.md +159 -0
- package/get-research-done/templates/UAT.md +247 -0
- package/get-research-done/templates/archive-reason.md +195 -0
- package/get-research-done/templates/codebase/architecture.md +255 -0
- package/get-research-done/templates/codebase/concerns.md +310 -0
- package/get-research-done/templates/codebase/conventions.md +307 -0
- package/get-research-done/templates/codebase/integrations.md +280 -0
- package/get-research-done/templates/codebase/stack.md +186 -0
- package/get-research-done/templates/codebase/structure.md +285 -0
- package/get-research-done/templates/codebase/testing.md +480 -0
- package/get-research-done/templates/config.json +35 -0
- package/get-research-done/templates/context.md +283 -0
- package/get-research-done/templates/continue-here.md +78 -0
- package/get-research-done/templates/critic-log.md +288 -0
- package/get-research-done/templates/data-report.md +173 -0
- package/get-research-done/templates/debug-subagent-prompt.md +91 -0
- package/get-research-done/templates/decision-log.md +58 -0
- package/get-research-done/templates/decision.md +138 -0
- package/get-research-done/templates/discovery.md +146 -0
- package/get-research-done/templates/experiment-readme.md +104 -0
- package/get-research-done/templates/graduated-script.md +180 -0
- package/get-research-done/templates/iteration-summary.md +234 -0
- package/get-research-done/templates/milestone-archive.md +123 -0
- package/get-research-done/templates/milestone.md +115 -0
- package/get-research-done/templates/objective.md +271 -0
- package/get-research-done/templates/phase-prompt.md +567 -0
- package/get-research-done/templates/planner-subagent-prompt.md +117 -0
- package/get-research-done/templates/project.md +184 -0
- package/get-research-done/templates/requirements.md +231 -0
- package/get-research-done/templates/research-project/ARCHITECTURE.md +204 -0
- package/get-research-done/templates/research-project/FEATURES.md +147 -0
- package/get-research-done/templates/research-project/PITFALLS.md +200 -0
- package/get-research-done/templates/research-project/STACK.md +120 -0
- package/get-research-done/templates/research-project/SUMMARY.md +170 -0
- package/get-research-done/templates/research.md +529 -0
- package/get-research-done/templates/roadmap.md +202 -0
- package/get-research-done/templates/scorecard.json +113 -0
- package/get-research-done/templates/state.md +287 -0
- package/get-research-done/templates/summary.md +246 -0
- package/get-research-done/templates/user-setup.md +311 -0
- package/get-research-done/templates/verification-report.md +322 -0
- package/get-research-done/workflows/complete-milestone.md +756 -0
- package/get-research-done/workflows/diagnose-issues.md +231 -0
- package/get-research-done/workflows/discovery-phase.md +289 -0
- package/get-research-done/workflows/discuss-phase.md +433 -0
- package/get-research-done/workflows/execute-phase.md +657 -0
- package/get-research-done/workflows/execute-plan.md +1844 -0
- package/get-research-done/workflows/list-phase-assumptions.md +178 -0
- package/get-research-done/workflows/map-codebase.md +322 -0
- package/get-research-done/workflows/resume-project.md +307 -0
- package/get-research-done/workflows/transition.md +556 -0
- package/get-research-done/workflows/verify-phase.md +628 -0
- package/get-research-done/workflows/verify-work.md +596 -0
- package/hooks/dist/grd-check-update.js +61 -0
- package/hooks/dist/grd-statusline.js +84 -0
- package/package.json +47 -0
- package/scripts/audit-help-commands.sh +115 -0
- package/scripts/build-hooks.js +42 -0
- package/scripts/verify-all-commands.sh +246 -0
- package/scripts/verify-architect-warning.sh +35 -0
- package/scripts/verify-insights-mode.sh +40 -0
- package/scripts/verify-quick-mode.sh +20 -0
- package/scripts/verify-revise-data-routing.sh +139 -0
package/bin/install.js
ADDED
|
@@ -0,0 +1,1294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
|
|
8
|
+
// Colors
|
|
9
|
+
const cyan = '\x1b[36m';
|
|
10
|
+
const green = '\x1b[32m';
|
|
11
|
+
const yellow = '\x1b[33m';
|
|
12
|
+
const dim = '\x1b[2m';
|
|
13
|
+
const reset = '\x1b[0m';
|
|
14
|
+
|
|
15
|
+
// Get version from package.json
|
|
16
|
+
const pkg = require('../package.json');
|
|
17
|
+
|
|
18
|
+
// Parse args
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
const hasGlobal = args.includes('--global') || args.includes('-g');
|
|
21
|
+
const hasLocal = args.includes('--local') || args.includes('-l');
|
|
22
|
+
const hasOpencode = args.includes('--opencode');
|
|
23
|
+
const hasClaude = args.includes('--claude');
|
|
24
|
+
const hasBoth = args.includes('--both');
|
|
25
|
+
const hasUninstall = args.includes('--uninstall') || args.includes('-u');
|
|
26
|
+
|
|
27
|
+
// Runtime selection - can be set by flags or interactive prompt
|
|
28
|
+
let selectedRuntimes = [];
|
|
29
|
+
if (hasBoth) {
|
|
30
|
+
selectedRuntimes = ['claude', 'opencode'];
|
|
31
|
+
} else if (hasOpencode) {
|
|
32
|
+
selectedRuntimes = ['opencode'];
|
|
33
|
+
} else if (hasClaude) {
|
|
34
|
+
selectedRuntimes = ['claude'];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Helper to get directory name for a runtime (used for local/project installs)
|
|
38
|
+
function getDirName(runtime) {
|
|
39
|
+
return runtime === 'opencode' ? '.opencode' : '.claude';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the global config directory for OpenCode
|
|
44
|
+
* OpenCode follows XDG Base Directory spec and uses ~/.config/opencode/
|
|
45
|
+
* Priority: OPENCODE_CONFIG_DIR > dirname(OPENCODE_CONFIG) > XDG_CONFIG_HOME/opencode > ~/.config/opencode
|
|
46
|
+
*/
|
|
47
|
+
function getOpencodeGlobalDir() {
|
|
48
|
+
// 1. Explicit OPENCODE_CONFIG_DIR env var
|
|
49
|
+
if (process.env.OPENCODE_CONFIG_DIR) {
|
|
50
|
+
return expandTilde(process.env.OPENCODE_CONFIG_DIR);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. OPENCODE_CONFIG env var (use its directory)
|
|
54
|
+
if (process.env.OPENCODE_CONFIG) {
|
|
55
|
+
return path.dirname(expandTilde(process.env.OPENCODE_CONFIG));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 3. XDG_CONFIG_HOME/opencode
|
|
59
|
+
if (process.env.XDG_CONFIG_HOME) {
|
|
60
|
+
return path.join(expandTilde(process.env.XDG_CONFIG_HOME), 'opencode');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 4. Default: ~/.config/opencode (XDG default)
|
|
64
|
+
return path.join(os.homedir(), '.config', 'opencode');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the global config directory for a runtime
|
|
69
|
+
* @param {string} runtime - 'claude' or 'opencode'
|
|
70
|
+
* @param {string|null} explicitDir - Explicit directory from --config-dir flag
|
|
71
|
+
*/
|
|
72
|
+
function getGlobalDir(runtime, explicitDir = null) {
|
|
73
|
+
if (runtime === 'opencode') {
|
|
74
|
+
// For OpenCode, --config-dir overrides env vars
|
|
75
|
+
if (explicitDir) {
|
|
76
|
+
return expandTilde(explicitDir);
|
|
77
|
+
}
|
|
78
|
+
return getOpencodeGlobalDir();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude
|
|
82
|
+
if (explicitDir) {
|
|
83
|
+
return expandTilde(explicitDir);
|
|
84
|
+
}
|
|
85
|
+
if (process.env.CLAUDE_CONFIG_DIR) {
|
|
86
|
+
return expandTilde(process.env.CLAUDE_CONFIG_DIR);
|
|
87
|
+
}
|
|
88
|
+
return path.join(os.homedir(), '.claude');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const banner = `
|
|
92
|
+
${cyan} ██████╗ ██████╗ ██████╗
|
|
93
|
+
██╔════╝ ██╔══██╗██╔══██╗
|
|
94
|
+
██║ ███╗██████╔╝██║ ██║
|
|
95
|
+
██║ ██║██╔══██╗██║ ██║
|
|
96
|
+
╚██████╔╝██║ ██║██████╔╝
|
|
97
|
+
╚═════╝ ╚═╝ ╚═╝╚═════╝${reset}
|
|
98
|
+
|
|
99
|
+
Get Research Done ${dim}v${pkg.version}${reset}
|
|
100
|
+
A recursive, agentic framework for ML research
|
|
101
|
+
with hypothesis-driven experimentation for Claude Code by Ulmentflam.
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
// Parse --config-dir argument
|
|
105
|
+
function parseConfigDirArg() {
|
|
106
|
+
const configDirIndex = args.findIndex(arg => arg === '--config-dir' || arg === '-c');
|
|
107
|
+
if (configDirIndex !== -1) {
|
|
108
|
+
const nextArg = args[configDirIndex + 1];
|
|
109
|
+
// Error if --config-dir is provided without a value or next arg is another flag
|
|
110
|
+
if (!nextArg || nextArg.startsWith('-')) {
|
|
111
|
+
console.error(` ${yellow}--config-dir requires a path argument${reset}`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
return nextArg;
|
|
115
|
+
}
|
|
116
|
+
// Also handle --config-dir=value format
|
|
117
|
+
const configDirArg = args.find(arg => arg.startsWith('--config-dir=') || arg.startsWith('-c='));
|
|
118
|
+
if (configDirArg) {
|
|
119
|
+
const value = configDirArg.split('=')[1];
|
|
120
|
+
if (!value) {
|
|
121
|
+
console.error(` ${yellow}--config-dir requires a non-empty path${reset}`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const explicitConfigDir = parseConfigDirArg();
|
|
129
|
+
const hasHelp = args.includes('--help') || args.includes('-h');
|
|
130
|
+
const forceStatusline = args.includes('--force-statusline');
|
|
131
|
+
|
|
132
|
+
console.log(banner);
|
|
133
|
+
|
|
134
|
+
// Show help if requested
|
|
135
|
+
if (hasHelp) {
|
|
136
|
+
console.log(` ${yellow}Usage:${reset} npx get-research-done [options]
|
|
137
|
+
|
|
138
|
+
${yellow}Options:${reset}
|
|
139
|
+
${cyan}-g, --global${reset} Install globally (to config directory)
|
|
140
|
+
${cyan}-l, --local${reset} Install locally (to current directory)
|
|
141
|
+
${cyan}--claude${reset} Install for Claude Code only
|
|
142
|
+
${cyan}--opencode${reset} Install for OpenCode only
|
|
143
|
+
${cyan}--both${reset} Install for both Claude Code and OpenCode
|
|
144
|
+
${cyan}-u, --uninstall${reset} Uninstall GRD (remove all GRD files)
|
|
145
|
+
${cyan}-c, --config-dir <path>${reset} Specify custom config directory
|
|
146
|
+
${cyan}-h, --help${reset} Show this help message
|
|
147
|
+
${cyan}--force-statusline${reset} Replace existing statusline config
|
|
148
|
+
|
|
149
|
+
${yellow}Examples:${reset}
|
|
150
|
+
${dim}# Interactive install (prompts for runtime and location)${reset}
|
|
151
|
+
npx get-research-done
|
|
152
|
+
|
|
153
|
+
${dim}# Install for Claude Code globally${reset}
|
|
154
|
+
npx get-research-done --claude --global
|
|
155
|
+
|
|
156
|
+
${dim}# Install for OpenCode globally${reset}
|
|
157
|
+
npx get-research-done --opencode --global
|
|
158
|
+
|
|
159
|
+
${dim}# Install for both runtimes globally${reset}
|
|
160
|
+
npx get-research-done --both --global
|
|
161
|
+
|
|
162
|
+
${dim}# Install to custom config directory${reset}
|
|
163
|
+
npx get-research-done --claude --global --config-dir ~/.claude-bc
|
|
164
|
+
|
|
165
|
+
${dim}# Install to current project only${reset}
|
|
166
|
+
npx get-research-done --claude --local
|
|
167
|
+
|
|
168
|
+
${dim}# Uninstall GRD from Claude Code globally${reset}
|
|
169
|
+
npx get-research-done --claude --global --uninstall
|
|
170
|
+
|
|
171
|
+
${dim}# Uninstall GRD from current project${reset}
|
|
172
|
+
npx get-research-done --claude --local --uninstall
|
|
173
|
+
|
|
174
|
+
${yellow}Notes:${reset}
|
|
175
|
+
The --config-dir option is useful when you have multiple Claude Code
|
|
176
|
+
configurations (e.g., for different subscriptions). It takes priority
|
|
177
|
+
over the CLAUDE_CONFIG_DIR environment variable.
|
|
178
|
+
`);
|
|
179
|
+
process.exit(0);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Expand ~ to home directory (shell doesn't expand in env vars passed to node)
|
|
184
|
+
*/
|
|
185
|
+
function expandTilde(filePath) {
|
|
186
|
+
if (filePath && filePath.startsWith('~/')) {
|
|
187
|
+
return path.join(os.homedir(), filePath.slice(2));
|
|
188
|
+
}
|
|
189
|
+
return filePath;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Build a hook command path using forward slashes for cross-platform compatibility.
|
|
194
|
+
* On Windows, $HOME is not expanded by cmd.exe/PowerShell, so we use the actual path.
|
|
195
|
+
*/
|
|
196
|
+
function buildHookCommand(claudeDir, hookName) {
|
|
197
|
+
// Use forward slashes for Node.js compatibility on all platforms
|
|
198
|
+
const hooksPath = claudeDir.replace(/\\/g, '/') + '/hooks/' + hookName;
|
|
199
|
+
return `node "${hooksPath}"`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Read and parse settings.json, returning empty object if it doesn't exist
|
|
204
|
+
*/
|
|
205
|
+
function readSettings(settingsPath) {
|
|
206
|
+
if (fs.existsSync(settingsPath)) {
|
|
207
|
+
try {
|
|
208
|
+
return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
209
|
+
} catch (e) {
|
|
210
|
+
return {};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return {};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Write settings.json with proper formatting
|
|
218
|
+
*/
|
|
219
|
+
function writeSettings(settingsPath, settings) {
|
|
220
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Convert Claude Code frontmatter to opencode format
|
|
225
|
+
* - Converts 'allowed-tools:' array to 'permission:' object
|
|
226
|
+
* @param {string} content - Markdown file content with YAML frontmatter
|
|
227
|
+
* @returns {string} - Content with converted frontmatter
|
|
228
|
+
*/
|
|
229
|
+
// Color name to hex mapping for opencode compatibility
|
|
230
|
+
const colorNameToHex = {
|
|
231
|
+
cyan: '#00FFFF',
|
|
232
|
+
red: '#FF0000',
|
|
233
|
+
green: '#00FF00',
|
|
234
|
+
blue: '#0000FF',
|
|
235
|
+
yellow: '#FFFF00',
|
|
236
|
+
magenta: '#FF00FF',
|
|
237
|
+
orange: '#FFA500',
|
|
238
|
+
purple: '#800080',
|
|
239
|
+
pink: '#FFC0CB',
|
|
240
|
+
white: '#FFFFFF',
|
|
241
|
+
black: '#000000',
|
|
242
|
+
gray: '#808080',
|
|
243
|
+
grey: '#808080',
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Tool name mapping from Claude Code to OpenCode
|
|
247
|
+
// OpenCode uses lowercase tool names; special mappings for renamed tools
|
|
248
|
+
const claudeToOpencodeTools = {
|
|
249
|
+
AskUserQuestion: 'question',
|
|
250
|
+
SlashCommand: 'skill',
|
|
251
|
+
TodoWrite: 'todowrite',
|
|
252
|
+
WebFetch: 'webfetch',
|
|
253
|
+
WebSearch: 'websearch', // Plugin/MCP - keep for compatibility
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Convert a Claude Code tool name to OpenCode format
|
|
258
|
+
* - Applies special mappings (AskUserQuestion -> question, etc.)
|
|
259
|
+
* - Converts to lowercase (except MCP tools which keep their format)
|
|
260
|
+
*/
|
|
261
|
+
function convertToolName(claudeTool) {
|
|
262
|
+
// Check for special mapping first
|
|
263
|
+
if (claudeToOpencodeTools[claudeTool]) {
|
|
264
|
+
return claudeToOpencodeTools[claudeTool];
|
|
265
|
+
}
|
|
266
|
+
// MCP tools (mcp__*) keep their format
|
|
267
|
+
if (claudeTool.startsWith('mcp__')) {
|
|
268
|
+
return claudeTool;
|
|
269
|
+
}
|
|
270
|
+
// Default: convert to lowercase
|
|
271
|
+
return claudeTool.toLowerCase();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function convertClaudeToOpencodeFrontmatter(content) {
|
|
275
|
+
// Replace tool name references in content (applies to all files)
|
|
276
|
+
let convertedContent = content;
|
|
277
|
+
convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
|
|
278
|
+
convertedContent = convertedContent.replace(/\bSlashCommand\b/g, 'skill');
|
|
279
|
+
convertedContent = convertedContent.replace(/\bTodoWrite\b/g, 'todowrite');
|
|
280
|
+
// Replace /grd:command with /grd-command for opencode (flat command structure)
|
|
281
|
+
convertedContent = convertedContent.replace(/\/grd:/g, '/grd-');
|
|
282
|
+
// Replace ~/.claude with ~/.config/opencode (OpenCode's correct config location)
|
|
283
|
+
convertedContent = convertedContent.replace(/~\/\.claude\b/g, '~/.config/opencode');
|
|
284
|
+
|
|
285
|
+
// Check if content has frontmatter
|
|
286
|
+
if (!convertedContent.startsWith('---')) {
|
|
287
|
+
return convertedContent;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Find the end of frontmatter
|
|
291
|
+
const endIndex = convertedContent.indexOf('---', 3);
|
|
292
|
+
if (endIndex === -1) {
|
|
293
|
+
return convertedContent;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const frontmatter = convertedContent.substring(3, endIndex).trim();
|
|
297
|
+
const body = convertedContent.substring(endIndex + 3);
|
|
298
|
+
|
|
299
|
+
// Parse frontmatter line by line (simple YAML parsing)
|
|
300
|
+
const lines = frontmatter.split('\n');
|
|
301
|
+
const newLines = [];
|
|
302
|
+
let inAllowedTools = false;
|
|
303
|
+
const allowedTools = [];
|
|
304
|
+
|
|
305
|
+
for (const line of lines) {
|
|
306
|
+
const trimmed = line.trim();
|
|
307
|
+
|
|
308
|
+
// Detect start of allowed-tools array
|
|
309
|
+
if (trimmed.startsWith('allowed-tools:')) {
|
|
310
|
+
inAllowedTools = true;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Detect inline tools: field (comma-separated string)
|
|
315
|
+
if (trimmed.startsWith('tools:')) {
|
|
316
|
+
const toolsValue = trimmed.substring(6).trim();
|
|
317
|
+
if (toolsValue) {
|
|
318
|
+
// Parse comma-separated tools
|
|
319
|
+
const tools = toolsValue.split(',').map(t => t.trim()).filter(t => t);
|
|
320
|
+
allowedTools.push(...tools);
|
|
321
|
+
}
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Remove name: field - opencode uses filename for command name
|
|
326
|
+
if (trimmed.startsWith('name:')) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Convert color names to hex for opencode
|
|
331
|
+
if (trimmed.startsWith('color:')) {
|
|
332
|
+
const colorValue = trimmed.substring(6).trim().toLowerCase();
|
|
333
|
+
const hexColor = colorNameToHex[colorValue];
|
|
334
|
+
if (hexColor) {
|
|
335
|
+
newLines.push(`color: "${hexColor}"`);
|
|
336
|
+
} else if (colorValue.startsWith('#')) {
|
|
337
|
+
// Already hex, keep as is
|
|
338
|
+
newLines.push(line);
|
|
339
|
+
}
|
|
340
|
+
// Skip unknown color names
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Collect allowed-tools items
|
|
345
|
+
if (inAllowedTools) {
|
|
346
|
+
if (trimmed.startsWith('- ')) {
|
|
347
|
+
allowedTools.push(trimmed.substring(2).trim());
|
|
348
|
+
continue;
|
|
349
|
+
} else if (trimmed && !trimmed.startsWith('-')) {
|
|
350
|
+
// End of array, new field started
|
|
351
|
+
inAllowedTools = false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Keep other fields (including name: which opencode ignores)
|
|
356
|
+
if (!inAllowedTools) {
|
|
357
|
+
newLines.push(line);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Add tools object if we had allowed-tools or tools
|
|
362
|
+
if (allowedTools.length > 0) {
|
|
363
|
+
newLines.push('tools:');
|
|
364
|
+
for (const tool of allowedTools) {
|
|
365
|
+
newLines.push(` ${convertToolName(tool)}: true`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Rebuild frontmatter (body already has tool names converted)
|
|
370
|
+
const newFrontmatter = newLines.join('\n').trim();
|
|
371
|
+
return `---\n${newFrontmatter}\n---${body}`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Copy commands to a flat structure for OpenCode
|
|
376
|
+
* OpenCode expects: command/grd-help.md (invoked as /grd-help)
|
|
377
|
+
* Source structure: commands/grd/help.md
|
|
378
|
+
*
|
|
379
|
+
* @param {string} srcDir - Source directory (e.g., commands/grd/)
|
|
380
|
+
* @param {string} destDir - Destination directory (e.g., command/)
|
|
381
|
+
* @param {string} prefix - Prefix for filenames (e.g., 'grd')
|
|
382
|
+
* @param {string} pathPrefix - Path prefix for file references
|
|
383
|
+
* @param {string} runtime - Target runtime ('claude' or 'opencode')
|
|
384
|
+
*/
|
|
385
|
+
function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) {
|
|
386
|
+
if (!fs.existsSync(srcDir)) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Remove old grd-*.md files before copying new ones
|
|
391
|
+
if (fs.existsSync(destDir)) {
|
|
392
|
+
for (const file of fs.readdirSync(destDir)) {
|
|
393
|
+
if (file.startsWith(`${prefix}-`) && file.endsWith('.md')) {
|
|
394
|
+
fs.unlinkSync(path.join(destDir, file));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
402
|
+
|
|
403
|
+
for (const entry of entries) {
|
|
404
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
405
|
+
|
|
406
|
+
if (entry.isDirectory()) {
|
|
407
|
+
// Recurse into subdirectories, adding to prefix
|
|
408
|
+
// e.g., commands/grd/debug/start.md -> command/grd-debug-start.md
|
|
409
|
+
copyFlattenedCommands(srcPath, destDir, `${prefix}-${entry.name}`, pathPrefix, runtime);
|
|
410
|
+
} else if (entry.name.endsWith('.md')) {
|
|
411
|
+
// Flatten: help.md -> grd-help.md
|
|
412
|
+
const baseName = entry.name.replace('.md', '');
|
|
413
|
+
const destName = `${prefix}-${baseName}.md`;
|
|
414
|
+
const destPath = path.join(destDir, destName);
|
|
415
|
+
|
|
416
|
+
// Read, transform, and write
|
|
417
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
418
|
+
// Replace path references
|
|
419
|
+
const claudeDirRegex = /~\/\.claude\//g;
|
|
420
|
+
const opencodeDirRegex = /~\/\.opencode\//g;
|
|
421
|
+
content = content.replace(claudeDirRegex, pathPrefix);
|
|
422
|
+
content = content.replace(opencodeDirRegex, pathPrefix);
|
|
423
|
+
// Convert frontmatter for opencode compatibility
|
|
424
|
+
content = convertClaudeToOpencodeFrontmatter(content);
|
|
425
|
+
|
|
426
|
+
fs.writeFileSync(destPath, content);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Recursively copy directory, replacing paths in .md files
|
|
433
|
+
* Deletes existing destDir first to remove orphaned files from previous versions
|
|
434
|
+
* @param {string} srcDir - Source directory
|
|
435
|
+
* @param {string} destDir - Destination directory
|
|
436
|
+
* @param {string} pathPrefix - Path prefix for file references
|
|
437
|
+
* @param {string} runtime - Target runtime ('claude' or 'opencode')
|
|
438
|
+
*/
|
|
439
|
+
function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime) {
|
|
440
|
+
const isOpencode = runtime === 'opencode';
|
|
441
|
+
const dirName = getDirName(runtime);
|
|
442
|
+
|
|
443
|
+
// Clean install: remove existing destination to prevent orphaned files
|
|
444
|
+
if (fs.existsSync(destDir)) {
|
|
445
|
+
fs.rmSync(destDir, { recursive: true });
|
|
446
|
+
}
|
|
447
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
448
|
+
|
|
449
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
450
|
+
|
|
451
|
+
for (const entry of entries) {
|
|
452
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
453
|
+
const destPath = path.join(destDir, entry.name);
|
|
454
|
+
|
|
455
|
+
if (entry.isDirectory()) {
|
|
456
|
+
copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime);
|
|
457
|
+
} else if (entry.name.endsWith('.md')) {
|
|
458
|
+
// Replace ~/.claude/ with the appropriate prefix in Markdown files
|
|
459
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
460
|
+
const claudeDirRegex = new RegExp(`~/${dirName.replace('.', '\\.')}/`, 'g');
|
|
461
|
+
content = content.replace(claudeDirRegex, pathPrefix);
|
|
462
|
+
// Convert frontmatter for opencode compatibility
|
|
463
|
+
if (isOpencode) {
|
|
464
|
+
content = convertClaudeToOpencodeFrontmatter(content);
|
|
465
|
+
}
|
|
466
|
+
fs.writeFileSync(destPath, content);
|
|
467
|
+
} else {
|
|
468
|
+
fs.copyFileSync(srcPath, destPath);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Clean up orphaned files from previous GRD versions
|
|
475
|
+
*/
|
|
476
|
+
function cleanupOrphanedFiles(claudeDir) {
|
|
477
|
+
const orphanedFiles = [
|
|
478
|
+
'hooks/gsd-notify.sh', // Removed in v1.6.x (legacy GSD)
|
|
479
|
+
'hooks/statusline.js', // Renamed to grd-statusline.js
|
|
480
|
+
];
|
|
481
|
+
|
|
482
|
+
for (const relPath of orphanedFiles) {
|
|
483
|
+
const fullPath = path.join(claudeDir, relPath);
|
|
484
|
+
if (fs.existsSync(fullPath)) {
|
|
485
|
+
fs.unlinkSync(fullPath);
|
|
486
|
+
console.log(` ${green}✓${reset} Removed orphaned ${relPath}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Clean up orphaned hook registrations from settings.json
|
|
493
|
+
*/
|
|
494
|
+
function cleanupOrphanedHooks(settings) {
|
|
495
|
+
const orphanedHookPatterns = [
|
|
496
|
+
'gsd-notify.sh', // Removed (legacy GSD)
|
|
497
|
+
'hooks/statusline.js', // Renamed to grd-statusline.js
|
|
498
|
+
'gsd-intel-index.js', // Removed (legacy GSD)
|
|
499
|
+
'gsd-intel-session.js', // Removed (legacy GSD)
|
|
500
|
+
'gsd-intel-prune.js', // Removed (legacy GSD)
|
|
501
|
+
'gsd-statusline.js', // Legacy GSD hook
|
|
502
|
+
'gsd-check-update.js', // Legacy GSD hook
|
|
503
|
+
];
|
|
504
|
+
|
|
505
|
+
let cleaned = false;
|
|
506
|
+
|
|
507
|
+
// Check all hook event types (Stop, SessionStart, etc.)
|
|
508
|
+
if (settings.hooks) {
|
|
509
|
+
for (const eventType of Object.keys(settings.hooks)) {
|
|
510
|
+
const hookEntries = settings.hooks[eventType];
|
|
511
|
+
if (Array.isArray(hookEntries)) {
|
|
512
|
+
// Filter out entries that contain orphaned hooks
|
|
513
|
+
const filtered = hookEntries.filter(entry => {
|
|
514
|
+
if (entry.hooks && Array.isArray(entry.hooks)) {
|
|
515
|
+
// Check if any hook in this entry matches orphaned patterns
|
|
516
|
+
const hasOrphaned = entry.hooks.some(h =>
|
|
517
|
+
h.command && orphanedHookPatterns.some(pattern => h.command.includes(pattern))
|
|
518
|
+
);
|
|
519
|
+
if (hasOrphaned) {
|
|
520
|
+
cleaned = true;
|
|
521
|
+
return false; // Remove this entry
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return true; // Keep this entry
|
|
525
|
+
});
|
|
526
|
+
settings.hooks[eventType] = filtered;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (cleaned) {
|
|
532
|
+
console.log(` ${green}✓${reset} Removed orphaned hook registrations`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return settings;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Uninstall GRD from the specified directory for a specific runtime
|
|
540
|
+
* Removes only GRD-specific files/directories, preserves user content
|
|
541
|
+
* @param {boolean} isGlobal - Whether to uninstall from global or local
|
|
542
|
+
* @param {string} runtime - Target runtime ('claude' or 'opencode')
|
|
543
|
+
*/
|
|
544
|
+
function uninstall(isGlobal, runtime = 'claude') {
|
|
545
|
+
const isOpencode = runtime === 'opencode';
|
|
546
|
+
const dirName = getDirName(runtime);
|
|
547
|
+
|
|
548
|
+
// Get the target directory based on runtime and install type
|
|
549
|
+
const targetDir = isGlobal
|
|
550
|
+
? getGlobalDir(runtime, explicitConfigDir)
|
|
551
|
+
: path.join(process.cwd(), dirName);
|
|
552
|
+
|
|
553
|
+
const locationLabel = isGlobal
|
|
554
|
+
? targetDir.replace(os.homedir(), '~')
|
|
555
|
+
: targetDir.replace(process.cwd(), '.');
|
|
556
|
+
|
|
557
|
+
const runtimeLabel = isOpencode ? 'OpenCode' : 'Claude Code';
|
|
558
|
+
console.log(` Uninstalling GRD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
|
|
559
|
+
|
|
560
|
+
// Check if target directory exists
|
|
561
|
+
if (!fs.existsSync(targetDir)) {
|
|
562
|
+
console.log(` ${yellow}⚠${reset} Directory does not exist: ${locationLabel}`);
|
|
563
|
+
console.log(` Nothing to uninstall.\n`);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
let removedCount = 0;
|
|
568
|
+
|
|
569
|
+
// 1. Remove GRD commands directory
|
|
570
|
+
if (isOpencode) {
|
|
571
|
+
// OpenCode: remove command/grd-*.md files
|
|
572
|
+
const commandDir = path.join(targetDir, 'command');
|
|
573
|
+
if (fs.existsSync(commandDir)) {
|
|
574
|
+
const files = fs.readdirSync(commandDir);
|
|
575
|
+
for (const file of files) {
|
|
576
|
+
if (file.startsWith('grd-') && file.endsWith('.md')) {
|
|
577
|
+
fs.unlinkSync(path.join(commandDir, file));
|
|
578
|
+
removedCount++;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
console.log(` ${green}✓${reset} Removed GRD commands from command/`);
|
|
582
|
+
}
|
|
583
|
+
} else {
|
|
584
|
+
// Claude Code: remove commands/grd/ directory
|
|
585
|
+
const grdCommandsDir = path.join(targetDir, 'commands', 'grd');
|
|
586
|
+
if (fs.existsSync(grdCommandsDir)) {
|
|
587
|
+
fs.rmSync(grdCommandsDir, { recursive: true });
|
|
588
|
+
removedCount++;
|
|
589
|
+
console.log(` ${green}✓${reset} Removed commands/grd/`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 2. Remove get-research-done directory
|
|
594
|
+
const grdDir = path.join(targetDir, 'get-research-done');
|
|
595
|
+
if (fs.existsSync(grdDir)) {
|
|
596
|
+
fs.rmSync(grdDir, { recursive: true });
|
|
597
|
+
removedCount++;
|
|
598
|
+
console.log(` ${green}✓${reset} Removed get-research-done/`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// 3. Remove GRD agents (grd-*.md files only)
|
|
602
|
+
const agentsDir = path.join(targetDir, 'agents');
|
|
603
|
+
if (fs.existsSync(agentsDir)) {
|
|
604
|
+
const files = fs.readdirSync(agentsDir);
|
|
605
|
+
let agentCount = 0;
|
|
606
|
+
for (const file of files) {
|
|
607
|
+
if (file.startsWith('grd-') && file.endsWith('.md')) {
|
|
608
|
+
fs.unlinkSync(path.join(agentsDir, file));
|
|
609
|
+
agentCount++;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (agentCount > 0) {
|
|
613
|
+
removedCount++;
|
|
614
|
+
console.log(` ${green}✓${reset} Removed ${agentCount} GRD agents`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// 4. Remove GRD hooks
|
|
619
|
+
const hooksDir = path.join(targetDir, 'hooks');
|
|
620
|
+
if (fs.existsSync(hooksDir)) {
|
|
621
|
+
const grdHooks = ['grd-statusline.js', 'grd-check-update.js', 'grd-check-update.sh'];
|
|
622
|
+
let hookCount = 0;
|
|
623
|
+
for (const hook of grdHooks) {
|
|
624
|
+
const hookPath = path.join(hooksDir, hook);
|
|
625
|
+
if (fs.existsSync(hookPath)) {
|
|
626
|
+
fs.unlinkSync(hookPath);
|
|
627
|
+
hookCount++;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (hookCount > 0) {
|
|
631
|
+
removedCount++;
|
|
632
|
+
console.log(` ${green}✓${reset} Removed ${hookCount} GRD hooks`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// 5. Clean up settings.json (remove GRD hooks and statusline)
|
|
637
|
+
const settingsPath = path.join(targetDir, 'settings.json');
|
|
638
|
+
if (fs.existsSync(settingsPath)) {
|
|
639
|
+
let settings = readSettings(settingsPath);
|
|
640
|
+
let settingsModified = false;
|
|
641
|
+
|
|
642
|
+
// Remove GRD statusline if it references our hook
|
|
643
|
+
if (settings.statusLine && settings.statusLine.command &&
|
|
644
|
+
settings.statusLine.command.includes('grd-statusline')) {
|
|
645
|
+
delete settings.statusLine;
|
|
646
|
+
settingsModified = true;
|
|
647
|
+
console.log(` ${green}✓${reset} Removed GRD statusline from settings`);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Remove GRD hooks from SessionStart
|
|
651
|
+
if (settings.hooks && settings.hooks.SessionStart) {
|
|
652
|
+
const before = settings.hooks.SessionStart.length;
|
|
653
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(entry => {
|
|
654
|
+
if (entry.hooks && Array.isArray(entry.hooks)) {
|
|
655
|
+
// Filter out GRD hooks
|
|
656
|
+
const hasGsdHook = entry.hooks.some(h =>
|
|
657
|
+
h.command && (h.command.includes('grd-check-update') || h.command.includes('grd-statusline'))
|
|
658
|
+
);
|
|
659
|
+
return !hasGsdHook;
|
|
660
|
+
}
|
|
661
|
+
return true;
|
|
662
|
+
});
|
|
663
|
+
if (settings.hooks.SessionStart.length < before) {
|
|
664
|
+
settingsModified = true;
|
|
665
|
+
console.log(` ${green}✓${reset} Removed GRD hooks from settings`);
|
|
666
|
+
}
|
|
667
|
+
// Clean up empty array
|
|
668
|
+
if (settings.hooks.SessionStart.length === 0) {
|
|
669
|
+
delete settings.hooks.SessionStart;
|
|
670
|
+
}
|
|
671
|
+
// Clean up empty hooks object
|
|
672
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
673
|
+
delete settings.hooks;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (settingsModified) {
|
|
678
|
+
writeSettings(settingsPath, settings);
|
|
679
|
+
removedCount++;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// 6. For OpenCode, clean up permissions from opencode.json
|
|
684
|
+
if (isOpencode) {
|
|
685
|
+
const opencodeConfigDir = getOpencodeGlobalDir();
|
|
686
|
+
const configPath = path.join(opencodeConfigDir, 'opencode.json');
|
|
687
|
+
if (fs.existsSync(configPath)) {
|
|
688
|
+
try {
|
|
689
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
690
|
+
let modified = false;
|
|
691
|
+
|
|
692
|
+
// Remove GRD permission entries
|
|
693
|
+
if (config.permission) {
|
|
694
|
+
for (const permType of ['read', 'external_directory']) {
|
|
695
|
+
if (config.permission[permType]) {
|
|
696
|
+
const keys = Object.keys(config.permission[permType]);
|
|
697
|
+
for (const key of keys) {
|
|
698
|
+
if (key.includes('get-research-done')) {
|
|
699
|
+
delete config.permission[permType][key];
|
|
700
|
+
modified = true;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
// Clean up empty objects
|
|
704
|
+
if (Object.keys(config.permission[permType]).length === 0) {
|
|
705
|
+
delete config.permission[permType];
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (Object.keys(config.permission).length === 0) {
|
|
710
|
+
delete config.permission;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (modified) {
|
|
715
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
716
|
+
removedCount++;
|
|
717
|
+
console.log(` ${green}✓${reset} Removed GRD permissions from opencode.json`);
|
|
718
|
+
}
|
|
719
|
+
} catch (e) {
|
|
720
|
+
// Ignore JSON parse errors
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (removedCount === 0) {
|
|
726
|
+
console.log(` ${yellow}⚠${reset} No GRD files found to remove.`);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
console.log(`
|
|
730
|
+
${green}Done!${reset} GRD has been uninstalled from ${runtimeLabel}.
|
|
731
|
+
Your other files and settings have been preserved.
|
|
732
|
+
`);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Configure OpenCode permissions to allow reading GRD reference docs
|
|
737
|
+
* This prevents permission prompts when GRD accesses the get-research-done directory
|
|
738
|
+
*/
|
|
739
|
+
function configureOpencodePermissions() {
|
|
740
|
+
// OpenCode config file is at ~/.config/opencode/opencode.json
|
|
741
|
+
const opencodeConfigDir = getOpencodeGlobalDir();
|
|
742
|
+
const configPath = path.join(opencodeConfigDir, 'opencode.json');
|
|
743
|
+
|
|
744
|
+
// Ensure config directory exists
|
|
745
|
+
fs.mkdirSync(opencodeConfigDir, { recursive: true });
|
|
746
|
+
|
|
747
|
+
// Read existing config or create empty object
|
|
748
|
+
let config = {};
|
|
749
|
+
if (fs.existsSync(configPath)) {
|
|
750
|
+
try {
|
|
751
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
752
|
+
} catch (e) {
|
|
753
|
+
// Invalid JSON - start fresh but warn user
|
|
754
|
+
console.log(` ${yellow}⚠${reset} opencode.json had invalid JSON, recreating`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Ensure permission structure exists
|
|
759
|
+
if (!config.permission) {
|
|
760
|
+
config.permission = {};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Build the GRD path using the actual config directory
|
|
764
|
+
// Use ~ shorthand if it's in the default location, otherwise use full path
|
|
765
|
+
const defaultConfigDir = path.join(os.homedir(), '.config', 'opencode');
|
|
766
|
+
const grdPath = opencodeConfigDir === defaultConfigDir
|
|
767
|
+
? '~/.config/opencode/get-research-done/*'
|
|
768
|
+
: `${opencodeConfigDir}/get-research-done/*`;
|
|
769
|
+
|
|
770
|
+
let modified = false;
|
|
771
|
+
|
|
772
|
+
// Configure read permission
|
|
773
|
+
if (!config.permission.read || typeof config.permission.read !== 'object') {
|
|
774
|
+
config.permission.read = {};
|
|
775
|
+
}
|
|
776
|
+
if (config.permission.read[grdPath] !== 'allow') {
|
|
777
|
+
config.permission.read[grdPath] = 'allow';
|
|
778
|
+
modified = true;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Configure external_directory permission (the safety guard for paths outside project)
|
|
782
|
+
if (!config.permission.external_directory || typeof config.permission.external_directory !== 'object') {
|
|
783
|
+
config.permission.external_directory = {};
|
|
784
|
+
}
|
|
785
|
+
if (config.permission.external_directory[grdPath] !== 'allow') {
|
|
786
|
+
config.permission.external_directory[grdPath] = 'allow';
|
|
787
|
+
modified = true;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (!modified) {
|
|
791
|
+
return; // Already configured
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Write config back
|
|
795
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
796
|
+
console.log(` ${green}✓${reset} Configured read permission for GRD docs`);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Verify a directory exists and contains files
|
|
801
|
+
*/
|
|
802
|
+
function verifyInstalled(dirPath, description) {
|
|
803
|
+
if (!fs.existsSync(dirPath)) {
|
|
804
|
+
console.error(` ${yellow}✗${reset} Failed to install ${description}: directory not created`);
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
try {
|
|
808
|
+
const entries = fs.readdirSync(dirPath);
|
|
809
|
+
if (entries.length === 0) {
|
|
810
|
+
console.error(` ${yellow}✗${reset} Failed to install ${description}: directory is empty`);
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
} catch (e) {
|
|
814
|
+
console.error(` ${yellow}✗${reset} Failed to install ${description}: ${e.message}`);
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Verify a file exists
|
|
822
|
+
*/
|
|
823
|
+
function verifyFileInstalled(filePath, description) {
|
|
824
|
+
if (!fs.existsSync(filePath)) {
|
|
825
|
+
console.error(` ${yellow}✗${reset} Failed to install ${description}: file not created`);
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Install to the specified directory for a specific runtime
|
|
833
|
+
* @param {boolean} isGlobal - Whether to install globally or locally
|
|
834
|
+
* @param {string} runtime - Target runtime ('claude' or 'opencode')
|
|
835
|
+
*/
|
|
836
|
+
function install(isGlobal, runtime = 'claude') {
|
|
837
|
+
const isOpencode = runtime === 'opencode';
|
|
838
|
+
const dirName = getDirName(runtime); // .opencode or .claude (for local installs)
|
|
839
|
+
const src = path.join(__dirname, '..');
|
|
840
|
+
|
|
841
|
+
// Get the target directory based on runtime and install type
|
|
842
|
+
const targetDir = isGlobal
|
|
843
|
+
? getGlobalDir(runtime, explicitConfigDir)
|
|
844
|
+
: path.join(process.cwd(), dirName);
|
|
845
|
+
|
|
846
|
+
const locationLabel = isGlobal
|
|
847
|
+
? targetDir.replace(os.homedir(), '~')
|
|
848
|
+
: targetDir.replace(process.cwd(), '.');
|
|
849
|
+
|
|
850
|
+
// Path prefix for file references in markdown content
|
|
851
|
+
// For global installs: use full path (necessary when config dir is customized)
|
|
852
|
+
// For local installs: use relative ./.opencode/ or ./.claude/
|
|
853
|
+
const pathPrefix = isGlobal
|
|
854
|
+
? `${targetDir}/`
|
|
855
|
+
: `./${dirName}/`;
|
|
856
|
+
|
|
857
|
+
const runtimeLabel = isOpencode ? 'OpenCode' : 'Claude Code';
|
|
858
|
+
console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
|
|
859
|
+
|
|
860
|
+
// Track installation failures
|
|
861
|
+
const failures = [];
|
|
862
|
+
|
|
863
|
+
// Clean up orphaned files from previous versions
|
|
864
|
+
cleanupOrphanedFiles(targetDir);
|
|
865
|
+
|
|
866
|
+
// OpenCode uses 'command/' (singular) with flat structure: command/grd-help.md
|
|
867
|
+
// Claude Code uses 'commands/' (plural) with nested structure: commands/grd/help.md
|
|
868
|
+
if (isOpencode) {
|
|
869
|
+
// OpenCode: flat structure in command/ directory
|
|
870
|
+
const commandDir = path.join(targetDir, 'command');
|
|
871
|
+
fs.mkdirSync(commandDir, { recursive: true });
|
|
872
|
+
|
|
873
|
+
// Copy commands/grd/*.md as command/grd-*.md (flatten structure)
|
|
874
|
+
const grdSrc = path.join(src, 'commands', 'grd');
|
|
875
|
+
copyFlattenedCommands(grdSrc, commandDir, 'grd', pathPrefix, runtime);
|
|
876
|
+
if (verifyInstalled(commandDir, 'command/grd-*')) {
|
|
877
|
+
const count = fs.readdirSync(commandDir).filter(f => f.startsWith('grd-')).length;
|
|
878
|
+
console.log(` ${green}✓${reset} Installed ${count} commands to command/`);
|
|
879
|
+
} else {
|
|
880
|
+
failures.push('command/grd-*');
|
|
881
|
+
}
|
|
882
|
+
} else {
|
|
883
|
+
// Claude Code: nested structure in commands/ directory
|
|
884
|
+
const commandsDir = path.join(targetDir, 'commands');
|
|
885
|
+
fs.mkdirSync(commandsDir, { recursive: true });
|
|
886
|
+
|
|
887
|
+
const grdSrc = path.join(src, 'commands', 'grd');
|
|
888
|
+
const grdDest = path.join(commandsDir, 'grd');
|
|
889
|
+
copyWithPathReplacement(grdSrc, grdDest, pathPrefix, runtime);
|
|
890
|
+
if (verifyInstalled(grdDest, 'commands/grd')) {
|
|
891
|
+
console.log(` ${green}✓${reset} Installed commands/grd`);
|
|
892
|
+
} else {
|
|
893
|
+
failures.push('commands/grd');
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Copy get-research-done skill with path replacement
|
|
898
|
+
const skillSrc = path.join(src, 'get-research-done');
|
|
899
|
+
const skillDest = path.join(targetDir, 'get-research-done');
|
|
900
|
+
copyWithPathReplacement(skillSrc, skillDest, pathPrefix, runtime);
|
|
901
|
+
if (verifyInstalled(skillDest, 'get-research-done')) {
|
|
902
|
+
console.log(` ${green}✓${reset} Installed get-research-done`);
|
|
903
|
+
} else {
|
|
904
|
+
failures.push('get-research-done');
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Copy agents to agents directory (subagents must be at root level)
|
|
908
|
+
// Only delete grd-*.md files to preserve user's custom agents
|
|
909
|
+
const agentsSrc = path.join(src, 'agents');
|
|
910
|
+
if (fs.existsSync(agentsSrc)) {
|
|
911
|
+
const agentsDest = path.join(targetDir, 'agents');
|
|
912
|
+
fs.mkdirSync(agentsDest, { recursive: true });
|
|
913
|
+
|
|
914
|
+
// Remove old GRD agents (grd-*.md) before copying new ones
|
|
915
|
+
if (fs.existsSync(agentsDest)) {
|
|
916
|
+
for (const file of fs.readdirSync(agentsDest)) {
|
|
917
|
+
if (file.startsWith('grd-') && file.endsWith('.md')) {
|
|
918
|
+
fs.unlinkSync(path.join(agentsDest, file));
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Copy new agents (don't use copyWithPathReplacement which would wipe the folder)
|
|
924
|
+
const agentEntries = fs.readdirSync(agentsSrc, { withFileTypes: true });
|
|
925
|
+
for (const entry of agentEntries) {
|
|
926
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
927
|
+
let content = fs.readFileSync(path.join(agentsSrc, entry.name), 'utf8');
|
|
928
|
+
const dirRegex = new RegExp(`~/${dirName.replace('.', '\\.')}/`, 'g');
|
|
929
|
+
content = content.replace(dirRegex, pathPrefix);
|
|
930
|
+
// Convert frontmatter for opencode compatibility
|
|
931
|
+
if (isOpencode) {
|
|
932
|
+
content = convertClaudeToOpencodeFrontmatter(content);
|
|
933
|
+
}
|
|
934
|
+
fs.writeFileSync(path.join(agentsDest, entry.name), content);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (verifyInstalled(agentsDest, 'agents')) {
|
|
938
|
+
console.log(` ${green}✓${reset} Installed agents`);
|
|
939
|
+
} else {
|
|
940
|
+
failures.push('agents');
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Copy CHANGELOG.md
|
|
945
|
+
const changelogSrc = path.join(src, 'CHANGELOG.md');
|
|
946
|
+
const changelogDest = path.join(targetDir, 'get-research-done', 'CHANGELOG.md');
|
|
947
|
+
if (fs.existsSync(changelogSrc)) {
|
|
948
|
+
fs.copyFileSync(changelogSrc, changelogDest);
|
|
949
|
+
if (verifyFileInstalled(changelogDest, 'CHANGELOG.md')) {
|
|
950
|
+
console.log(` ${green}✓${reset} Installed CHANGELOG.md`);
|
|
951
|
+
} else {
|
|
952
|
+
failures.push('CHANGELOG.md');
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Write VERSION file for whats-new command
|
|
957
|
+
const versionDest = path.join(targetDir, 'get-research-done', 'VERSION');
|
|
958
|
+
fs.writeFileSync(versionDest, pkg.version);
|
|
959
|
+
if (verifyFileInstalled(versionDest, 'VERSION')) {
|
|
960
|
+
console.log(` ${green}✓${reset} Wrote VERSION (${pkg.version})`);
|
|
961
|
+
} else {
|
|
962
|
+
failures.push('VERSION');
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Copy hooks from dist/ (bundled with dependencies)
|
|
966
|
+
const hooksSrc = path.join(src, 'hooks', 'dist');
|
|
967
|
+
if (fs.existsSync(hooksSrc)) {
|
|
968
|
+
const hooksDest = path.join(targetDir, 'hooks');
|
|
969
|
+
fs.mkdirSync(hooksDest, { recursive: true });
|
|
970
|
+
const hookEntries = fs.readdirSync(hooksSrc);
|
|
971
|
+
for (const entry of hookEntries) {
|
|
972
|
+
const srcFile = path.join(hooksSrc, entry);
|
|
973
|
+
// Only copy files, not directories
|
|
974
|
+
if (fs.statSync(srcFile).isFile()) {
|
|
975
|
+
const destFile = path.join(hooksDest, entry);
|
|
976
|
+
fs.copyFileSync(srcFile, destFile);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (verifyInstalled(hooksDest, 'hooks')) {
|
|
980
|
+
console.log(` ${green}✓${reset} Installed hooks (bundled)`);
|
|
981
|
+
} else {
|
|
982
|
+
failures.push('hooks');
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// If critical components failed, exit with error
|
|
987
|
+
if (failures.length > 0) {
|
|
988
|
+
console.error(`\n ${yellow}Installation incomplete!${reset} Failed: ${failures.join(', ')}`);
|
|
989
|
+
console.error(` Try running directly: node ~/.npm/_npx/*/node_modules/get-research-done/bin/install.js --global\n`);
|
|
990
|
+
process.exit(1);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Configure statusline and hooks in settings.json
|
|
994
|
+
const settingsPath = path.join(targetDir, 'settings.json');
|
|
995
|
+
const settings = cleanupOrphanedHooks(readSettings(settingsPath));
|
|
996
|
+
const statuslineCommand = isGlobal
|
|
997
|
+
? buildHookCommand(targetDir, 'grd-statusline.js')
|
|
998
|
+
: 'node ' + dirName + '/hooks/grd-statusline.js';
|
|
999
|
+
const updateCheckCommand = isGlobal
|
|
1000
|
+
? buildHookCommand(targetDir, 'grd-check-update.js')
|
|
1001
|
+
: 'node ' + dirName + '/hooks/grd-check-update.js';
|
|
1002
|
+
|
|
1003
|
+
// Configure SessionStart hook for update checking (skip for opencode - different hook system)
|
|
1004
|
+
if (!isOpencode) {
|
|
1005
|
+
if (!settings.hooks) {
|
|
1006
|
+
settings.hooks = {};
|
|
1007
|
+
}
|
|
1008
|
+
if (!settings.hooks.SessionStart) {
|
|
1009
|
+
settings.hooks.SessionStart = [];
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Check if GRD update hook already exists
|
|
1013
|
+
const hasGsdUpdateHook = settings.hooks.SessionStart.some(entry =>
|
|
1014
|
+
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('grd-check-update'))
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
if (!hasGsdUpdateHook) {
|
|
1018
|
+
settings.hooks.SessionStart.push({
|
|
1019
|
+
hooks: [
|
|
1020
|
+
{
|
|
1021
|
+
type: 'command',
|
|
1022
|
+
command: updateCheckCommand
|
|
1023
|
+
}
|
|
1024
|
+
]
|
|
1025
|
+
});
|
|
1026
|
+
console.log(` ${green}✓${reset} Configured update check hook`);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
return { settingsPath, settings, statuslineCommand, runtime };
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Apply statusline config, then print completion message
|
|
1035
|
+
* @param {string} settingsPath - Path to settings.json
|
|
1036
|
+
* @param {object} settings - Settings object
|
|
1037
|
+
* @param {string} statuslineCommand - Statusline command
|
|
1038
|
+
* @param {boolean} shouldInstallStatusline - Whether to install statusline
|
|
1039
|
+
* @param {string} runtime - Target runtime ('claude' or 'opencode')
|
|
1040
|
+
*/
|
|
1041
|
+
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude') {
|
|
1042
|
+
const isOpencode = runtime === 'opencode';
|
|
1043
|
+
|
|
1044
|
+
if (shouldInstallStatusline && !isOpencode) {
|
|
1045
|
+
settings.statusLine = {
|
|
1046
|
+
type: 'command',
|
|
1047
|
+
command: statuslineCommand
|
|
1048
|
+
};
|
|
1049
|
+
console.log(` ${green}✓${reset} Configured statusline`);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Always write settings (hooks were already configured in install())
|
|
1053
|
+
writeSettings(settingsPath, settings);
|
|
1054
|
+
|
|
1055
|
+
// Configure OpenCode permissions if needed
|
|
1056
|
+
if (isOpencode) {
|
|
1057
|
+
configureOpencodePermissions();
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const program = isOpencode ? 'OpenCode' : 'Claude Code';
|
|
1061
|
+
const command = isOpencode ? '/grd-help' : '/grd:help';
|
|
1062
|
+
console.log(`
|
|
1063
|
+
${green}Done!${reset} Launch ${program} and run ${cyan}${command}${reset}.
|
|
1064
|
+
|
|
1065
|
+
${cyan}Join the community:${reset} https://discord.gg/5JJgD5svVS
|
|
1066
|
+
`);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Handle statusline configuration with optional prompt
|
|
1071
|
+
*/
|
|
1072
|
+
function handleStatusline(settings, isInteractive, callback) {
|
|
1073
|
+
const hasExisting = settings.statusLine != null;
|
|
1074
|
+
|
|
1075
|
+
// No existing statusline - just install it
|
|
1076
|
+
if (!hasExisting) {
|
|
1077
|
+
callback(true);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Has existing and --force-statusline flag
|
|
1082
|
+
if (forceStatusline) {
|
|
1083
|
+
callback(true);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Has existing, non-interactive mode - skip
|
|
1088
|
+
if (!isInteractive) {
|
|
1089
|
+
console.log(` ${yellow}⚠${reset} Skipping statusline (already configured)`);
|
|
1090
|
+
console.log(` Use ${cyan}--force-statusline${reset} to replace\n`);
|
|
1091
|
+
callback(false);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Has existing, interactive mode - prompt user
|
|
1096
|
+
const existingCmd = settings.statusLine.command || settings.statusLine.url || '(custom)';
|
|
1097
|
+
|
|
1098
|
+
const rl = readline.createInterface({
|
|
1099
|
+
input: process.stdin,
|
|
1100
|
+
output: process.stdout
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
console.log(`
|
|
1104
|
+
${yellow}⚠${reset} Existing statusline detected
|
|
1105
|
+
|
|
1106
|
+
Your current statusline:
|
|
1107
|
+
${dim}command: ${existingCmd}${reset}
|
|
1108
|
+
|
|
1109
|
+
GRD includes a statusline showing:
|
|
1110
|
+
• Model name
|
|
1111
|
+
• Current task (from todo list)
|
|
1112
|
+
• Context window usage (color-coded)
|
|
1113
|
+
|
|
1114
|
+
${cyan}1${reset}) Keep existing
|
|
1115
|
+
${cyan}2${reset}) Replace with GRD statusline
|
|
1116
|
+
`);
|
|
1117
|
+
|
|
1118
|
+
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
|
1119
|
+
rl.close();
|
|
1120
|
+
const choice = answer.trim() || '1';
|
|
1121
|
+
callback(choice === '2');
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Prompt for runtime selection (Claude Code / OpenCode / Both)
|
|
1127
|
+
* @param {function} callback - Called with array of selected runtimes
|
|
1128
|
+
*/
|
|
1129
|
+
function promptRuntime(callback) {
|
|
1130
|
+
const rl = readline.createInterface({
|
|
1131
|
+
input: process.stdin,
|
|
1132
|
+
output: process.stdout
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
let answered = false;
|
|
1136
|
+
|
|
1137
|
+
rl.on('close', () => {
|
|
1138
|
+
if (!answered) {
|
|
1139
|
+
answered = true;
|
|
1140
|
+
console.log(`\n ${yellow}Installation cancelled${reset}\n`);
|
|
1141
|
+
process.exit(0);
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
console.log(` ${yellow}Which runtime(s) would you like to install for?${reset}
|
|
1146
|
+
|
|
1147
|
+
${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset}
|
|
1148
|
+
${cyan}2${reset}) OpenCode ${dim}(~/.config/opencode)${reset} - open source, free models
|
|
1149
|
+
${cyan}3${reset}) Both
|
|
1150
|
+
`);
|
|
1151
|
+
|
|
1152
|
+
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
|
1153
|
+
answered = true;
|
|
1154
|
+
rl.close();
|
|
1155
|
+
const choice = answer.trim() || '1';
|
|
1156
|
+
if (choice === '3') {
|
|
1157
|
+
callback(['claude', 'opencode']);
|
|
1158
|
+
} else if (choice === '2') {
|
|
1159
|
+
callback(['opencode']);
|
|
1160
|
+
} else {
|
|
1161
|
+
callback(['claude']);
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* Prompt for install location
|
|
1168
|
+
* @param {string[]} runtimes - Array of runtimes to install for
|
|
1169
|
+
*/
|
|
1170
|
+
function promptLocation(runtimes) {
|
|
1171
|
+
// Check if stdin is a TTY - if not, fall back to global install
|
|
1172
|
+
// This handles npx execution in environments like WSL2 where stdin may not be properly connected
|
|
1173
|
+
if (!process.stdin.isTTY) {
|
|
1174
|
+
console.log(` ${yellow}Non-interactive terminal detected, defaulting to global install${reset}\n`);
|
|
1175
|
+
installAllRuntimes(runtimes, true, false);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const rl = readline.createInterface({
|
|
1180
|
+
input: process.stdin,
|
|
1181
|
+
output: process.stdout
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
// Track whether we've processed the answer to prevent double-execution
|
|
1185
|
+
let answered = false;
|
|
1186
|
+
|
|
1187
|
+
// Handle readline close event (Ctrl+C, Escape, etc.) - cancel installation
|
|
1188
|
+
rl.on('close', () => {
|
|
1189
|
+
if (!answered) {
|
|
1190
|
+
answered = true;
|
|
1191
|
+
console.log(`\n ${yellow}Installation cancelled${reset}\n`);
|
|
1192
|
+
process.exit(0);
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
// Show paths for selected runtimes
|
|
1197
|
+
const pathExamples = runtimes.map(r => {
|
|
1198
|
+
// Use the proper global directory function for each runtime
|
|
1199
|
+
const globalPath = getGlobalDir(r, explicitConfigDir);
|
|
1200
|
+
return globalPath.replace(os.homedir(), '~');
|
|
1201
|
+
}).join(', ');
|
|
1202
|
+
|
|
1203
|
+
const localExamples = runtimes.map(r => `./${getDirName(r)}`).join(', ');
|
|
1204
|
+
|
|
1205
|
+
console.log(` ${yellow}Where would you like to install?${reset}
|
|
1206
|
+
|
|
1207
|
+
${cyan}1${reset}) Global ${dim}(${pathExamples})${reset} - available in all projects
|
|
1208
|
+
${cyan}2${reset}) Local ${dim}(${localExamples})${reset} - this project only
|
|
1209
|
+
`);
|
|
1210
|
+
|
|
1211
|
+
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
|
1212
|
+
answered = true;
|
|
1213
|
+
rl.close();
|
|
1214
|
+
const choice = answer.trim() || '1';
|
|
1215
|
+
const isGlobal = choice !== '2';
|
|
1216
|
+
installAllRuntimes(runtimes, isGlobal, true);
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* Install GRD for all selected runtimes
|
|
1222
|
+
* @param {string[]} runtimes - Array of runtimes to install for
|
|
1223
|
+
* @param {boolean} isGlobal - Whether to install globally
|
|
1224
|
+
* @param {boolean} isInteractive - Whether running interactively
|
|
1225
|
+
*/
|
|
1226
|
+
function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
1227
|
+
const results = [];
|
|
1228
|
+
|
|
1229
|
+
for (const runtime of runtimes) {
|
|
1230
|
+
const result = install(isGlobal, runtime);
|
|
1231
|
+
results.push(result);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Handle statusline for Claude Code only (OpenCode uses themes)
|
|
1235
|
+
const claudeResult = results.find(r => r.runtime === 'claude');
|
|
1236
|
+
|
|
1237
|
+
if (claudeResult) {
|
|
1238
|
+
handleStatusline(claudeResult.settings, isInteractive, (shouldInstallStatusline) => {
|
|
1239
|
+
finishInstall(claudeResult.settingsPath, claudeResult.settings, claudeResult.statuslineCommand, shouldInstallStatusline, 'claude');
|
|
1240
|
+
|
|
1241
|
+
// Finish OpenCode install if present
|
|
1242
|
+
const opencodeResult = results.find(r => r.runtime === 'opencode');
|
|
1243
|
+
if (opencodeResult) {
|
|
1244
|
+
finishInstall(opencodeResult.settingsPath, opencodeResult.settings, opencodeResult.statuslineCommand, false, 'opencode');
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
} else {
|
|
1248
|
+
// Only OpenCode
|
|
1249
|
+
const opencodeResult = results[0];
|
|
1250
|
+
finishInstall(opencodeResult.settingsPath, opencodeResult.settings, opencodeResult.statuslineCommand, false, 'opencode');
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// Main
|
|
1255
|
+
if (hasGlobal && hasLocal) {
|
|
1256
|
+
console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
|
|
1257
|
+
process.exit(1);
|
|
1258
|
+
} else if (explicitConfigDir && hasLocal) {
|
|
1259
|
+
console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
|
|
1260
|
+
process.exit(1);
|
|
1261
|
+
} else if (hasUninstall) {
|
|
1262
|
+
// Uninstall mode
|
|
1263
|
+
if (!hasGlobal && !hasLocal) {
|
|
1264
|
+
console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
|
|
1265
|
+
console.error(` Example: npx get-research-done --claude --global --uninstall`);
|
|
1266
|
+
process.exit(1);
|
|
1267
|
+
}
|
|
1268
|
+
const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude'];
|
|
1269
|
+
for (const runtime of runtimes) {
|
|
1270
|
+
uninstall(hasGlobal, runtime);
|
|
1271
|
+
}
|
|
1272
|
+
} else if (selectedRuntimes.length > 0) {
|
|
1273
|
+
// Non-interactive: runtime specified via flags
|
|
1274
|
+
if (!hasGlobal && !hasLocal) {
|
|
1275
|
+
// Need location but runtime is specified - prompt for location only
|
|
1276
|
+
promptLocation(selectedRuntimes);
|
|
1277
|
+
} else {
|
|
1278
|
+
// Both runtime and location specified via flags
|
|
1279
|
+
installAllRuntimes(selectedRuntimes, hasGlobal, false);
|
|
1280
|
+
}
|
|
1281
|
+
} else if (hasGlobal || hasLocal) {
|
|
1282
|
+
// Location specified but no runtime - default to Claude Code
|
|
1283
|
+
installAllRuntimes(['claude'], hasGlobal, false);
|
|
1284
|
+
} else {
|
|
1285
|
+
// Fully interactive: prompt for runtime, then location
|
|
1286
|
+
if (!process.stdin.isTTY) {
|
|
1287
|
+
console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
|
|
1288
|
+
installAllRuntimes(['claude'], true, false);
|
|
1289
|
+
} else {
|
|
1290
|
+
promptRuntime((runtimes) => {
|
|
1291
|
+
promptLocation(runtimes);
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
}
|