@open-code-review/cli 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.
- package/README.md +65 -0
- package/dist/index.js +1052 -0
- package/dist/package.json +42 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# @open-code-review/cli
|
|
2
|
+
|
|
3
|
+
CLI for Open Code Review - Multi-environment setup and progress tracking.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Via npx (no install)
|
|
9
|
+
npx @open-code-review/cli init
|
|
10
|
+
|
|
11
|
+
# Via pnpm dlx
|
|
12
|
+
pnpm dlx @open-code-review/cli init
|
|
13
|
+
|
|
14
|
+
# Global install
|
|
15
|
+
npm install -g @open-code-review/cli
|
|
16
|
+
ocr init
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
### `ocr init`
|
|
22
|
+
|
|
23
|
+
Set up OCR for your AI coding environments.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Interactive mode - select tools via checkbox
|
|
27
|
+
ocr init
|
|
28
|
+
|
|
29
|
+
# Non-interactive - specify tools
|
|
30
|
+
ocr init --tools claude,windsurf,cursor
|
|
31
|
+
|
|
32
|
+
# Install for all supported tools
|
|
33
|
+
ocr init --tools all
|
|
34
|
+
|
|
35
|
+
# Skip AGENTS.md/CLAUDE.md injection
|
|
36
|
+
ocr init --no-inject
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### `ocr progress`
|
|
40
|
+
|
|
41
|
+
Watch real-time progress of an active code review session.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Auto-detect current session
|
|
45
|
+
ocr progress
|
|
46
|
+
|
|
47
|
+
# Specify session
|
|
48
|
+
ocr progress --session 2025-01-26-feature-auth
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Supported AI Tools
|
|
52
|
+
|
|
53
|
+
| Tool | Config Directory |
|
|
54
|
+
|------|------------------|
|
|
55
|
+
| Claude Code | `.claude/` |
|
|
56
|
+
| Windsurf | `.windsurf/` |
|
|
57
|
+
| Cursor | `.cursor/` |
|
|
58
|
+
| GitHub Copilot | `.github/` |
|
|
59
|
+
| Cline | `.cline/` |
|
|
60
|
+
| Continue | `.continue/` |
|
|
61
|
+
| And more... | See `ocr init --help` |
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
Apache-2.0
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,1052 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
|
|
4
|
+
// packages/cli/src/index.ts
|
|
5
|
+
import { Command as Command4 } from "commander";
|
|
6
|
+
|
|
7
|
+
// packages/cli/src/commands/init.ts
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { checkbox } from "@inquirer/prompts";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import ora from "ora";
|
|
12
|
+
|
|
13
|
+
// packages/cli/src/lib/config.ts
|
|
14
|
+
var AI_TOOLS = [
|
|
15
|
+
{
|
|
16
|
+
id: "amazon-q",
|
|
17
|
+
name: "Amazon Q Developer",
|
|
18
|
+
configDir: ".aws/amazonq",
|
|
19
|
+
commandsDir: ".aws/amazonq/commands",
|
|
20
|
+
skillsDir: ".aws/amazonq/skills",
|
|
21
|
+
commandStrategy: "subdirectory"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "augment",
|
|
25
|
+
name: "Augment (Auggie)",
|
|
26
|
+
configDir: ".augment",
|
|
27
|
+
commandsDir: ".augment/commands",
|
|
28
|
+
skillsDir: ".augment/skills",
|
|
29
|
+
commandStrategy: "subdirectory"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "claude",
|
|
33
|
+
name: "Claude Code",
|
|
34
|
+
configDir: ".claude",
|
|
35
|
+
commandsDir: ".claude/commands",
|
|
36
|
+
skillsDir: ".claude/skills",
|
|
37
|
+
commandStrategy: "subdirectory"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "cline",
|
|
41
|
+
name: "Cline",
|
|
42
|
+
configDir: ".cline",
|
|
43
|
+
commandsDir: ".cline/commands",
|
|
44
|
+
skillsDir: ".cline/skills",
|
|
45
|
+
commandStrategy: "subdirectory"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "codex",
|
|
49
|
+
name: "Codex",
|
|
50
|
+
configDir: ".codex",
|
|
51
|
+
commandsDir: ".codex/commands",
|
|
52
|
+
skillsDir: ".codex/skills",
|
|
53
|
+
commandStrategy: "subdirectory"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "continue",
|
|
57
|
+
name: "Continue",
|
|
58
|
+
configDir: ".continue",
|
|
59
|
+
commandsDir: ".continue/commands",
|
|
60
|
+
skillsDir: ".continue/skills",
|
|
61
|
+
commandStrategy: "subdirectory"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "cursor",
|
|
65
|
+
name: "Cursor",
|
|
66
|
+
configDir: ".cursor",
|
|
67
|
+
commandsDir: ".cursor/commands",
|
|
68
|
+
skillsDir: ".cursor/skills",
|
|
69
|
+
commandStrategy: "subdirectory"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "gemini",
|
|
73
|
+
name: "Gemini CLI",
|
|
74
|
+
configDir: ".gemini",
|
|
75
|
+
commandsDir: ".gemini/commands",
|
|
76
|
+
skillsDir: ".gemini/skills",
|
|
77
|
+
commandStrategy: "subdirectory"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "github-copilot",
|
|
81
|
+
name: "GitHub Copilot",
|
|
82
|
+
configDir: ".github",
|
|
83
|
+
commandsDir: ".github/commands",
|
|
84
|
+
skillsDir: ".github/skills",
|
|
85
|
+
commandStrategy: "subdirectory"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "kilo-code",
|
|
89
|
+
name: "Kilo Code",
|
|
90
|
+
configDir: ".kilocode",
|
|
91
|
+
commandsDir: ".kilocode/commands",
|
|
92
|
+
skillsDir: ".kilocode/skills",
|
|
93
|
+
commandStrategy: "subdirectory"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "opencode",
|
|
97
|
+
name: "OpenCode",
|
|
98
|
+
configDir: ".opencode",
|
|
99
|
+
commandsDir: ".opencode/commands",
|
|
100
|
+
skillsDir: ".opencode/skills",
|
|
101
|
+
commandStrategy: "subdirectory"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "qoder",
|
|
105
|
+
name: "Qoder",
|
|
106
|
+
configDir: ".qoder",
|
|
107
|
+
commandsDir: ".qoder/commands",
|
|
108
|
+
skillsDir: ".qoder/skills",
|
|
109
|
+
commandStrategy: "subdirectory"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "roo-code",
|
|
113
|
+
name: "RooCode",
|
|
114
|
+
configDir: ".roo",
|
|
115
|
+
commandsDir: ".roo/commands",
|
|
116
|
+
skillsDir: ".roo/skills",
|
|
117
|
+
commandStrategy: "subdirectory"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "windsurf",
|
|
121
|
+
name: "Windsurf",
|
|
122
|
+
configDir: ".windsurf",
|
|
123
|
+
commandsDir: ".windsurf/workflows",
|
|
124
|
+
skillsDir: ".windsurf/skills",
|
|
125
|
+
commandStrategy: "flat-prefixed"
|
|
126
|
+
}
|
|
127
|
+
];
|
|
128
|
+
function getToolIds() {
|
|
129
|
+
return AI_TOOLS.map((tool) => tool.id);
|
|
130
|
+
}
|
|
131
|
+
function parseToolsArg(toolsArg) {
|
|
132
|
+
if (toolsArg === "all") {
|
|
133
|
+
return getToolIds();
|
|
134
|
+
}
|
|
135
|
+
const requestedIds = toolsArg.split(",").map((s) => s.trim().toLowerCase());
|
|
136
|
+
const validIds = getToolIds();
|
|
137
|
+
const result = [];
|
|
138
|
+
for (const id of requestedIds) {
|
|
139
|
+
if (validIds.includes(id)) {
|
|
140
|
+
result.push(id);
|
|
141
|
+
} else {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Invalid tool ID: "${id}". Valid options: ${validIds.join(", ")}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// packages/cli/src/lib/installer.ts
|
|
151
|
+
import {
|
|
152
|
+
existsSync,
|
|
153
|
+
mkdirSync,
|
|
154
|
+
cpSync,
|
|
155
|
+
writeFileSync,
|
|
156
|
+
readdirSync,
|
|
157
|
+
readFileSync,
|
|
158
|
+
unlinkSync
|
|
159
|
+
} from "node:fs";
|
|
160
|
+
import { join, dirname } from "node:path";
|
|
161
|
+
import { createRequire } from "node:module";
|
|
162
|
+
var require2 = createRequire(import.meta.url);
|
|
163
|
+
function ensureDir(dir) {
|
|
164
|
+
if (!existsSync(dir)) {
|
|
165
|
+
mkdirSync(dir, { recursive: true });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function getAgentsPackagePath() {
|
|
169
|
+
try {
|
|
170
|
+
const agentsPath = require2.resolve("@open-code-review/agents/package.json");
|
|
171
|
+
return dirname(agentsPath);
|
|
172
|
+
} catch {
|
|
173
|
+
const localPath = join(process.cwd(), "packages", "agents");
|
|
174
|
+
if (existsSync(localPath)) {
|
|
175
|
+
return localPath;
|
|
176
|
+
}
|
|
177
|
+
throw new Error(
|
|
178
|
+
"Could not find @open-code-review/agents package. Run from OCR repo or install the package."
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function copyDirSafe(src, dest) {
|
|
183
|
+
try {
|
|
184
|
+
ensureDir(dirname(dest));
|
|
185
|
+
cpSync(src, dest, { recursive: true, force: true });
|
|
186
|
+
return true;
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function copyFileSafe(src, dest) {
|
|
192
|
+
try {
|
|
193
|
+
ensureDir(dirname(dest));
|
|
194
|
+
const content = readFileSync(src);
|
|
195
|
+
writeFileSync(dest, content);
|
|
196
|
+
return true;
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function generateCommandReference(commandName, description) {
|
|
202
|
+
const baseName = commandName.replace(/\.md$/, "").replace(/^ocr-/, "");
|
|
203
|
+
return `---
|
|
204
|
+
description: ${description}
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
# OCR: ${baseName.charAt(0).toUpperCase() + baseName.slice(1)}
|
|
208
|
+
|
|
209
|
+
This command is managed by Open Code Review.
|
|
210
|
+
|
|
211
|
+
**Execute this command by reading and following:** \`.ocr/commands/${baseName}.md\`
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
function extractDescription(content) {
|
|
215
|
+
const match = content.match(/^---[\s\S]*?description:\s*(.+?)\n/m);
|
|
216
|
+
return match?.[1]?.trim() ?? "OCR command";
|
|
217
|
+
}
|
|
218
|
+
function installCommandsForTool(tool, commandsSource, targetDir) {
|
|
219
|
+
const toolCommandsDir = join(targetDir, tool.commandsDir);
|
|
220
|
+
const centralCommandsDir = join(targetDir, ".ocr", "commands");
|
|
221
|
+
ensureDir(toolCommandsDir);
|
|
222
|
+
ensureDir(centralCommandsDir);
|
|
223
|
+
try {
|
|
224
|
+
const commandFiles = readdirSync(commandsSource).filter(
|
|
225
|
+
(f) => f.endsWith(".md")
|
|
226
|
+
);
|
|
227
|
+
for (const file of commandFiles) {
|
|
228
|
+
const srcPath = join(commandsSource, file);
|
|
229
|
+
const normalizedName = file.replace(/^ocr-/, "");
|
|
230
|
+
const centralPath = join(centralCommandsDir, normalizedName);
|
|
231
|
+
if (!copyFileSafe(srcPath, centralPath)) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (tool.commandStrategy === "subdirectory") {
|
|
236
|
+
const ocrSubdir = join(toolCommandsDir, "ocr");
|
|
237
|
+
ensureDir(ocrSubdir);
|
|
238
|
+
for (const file of commandFiles) {
|
|
239
|
+
const srcPath = join(commandsSource, file);
|
|
240
|
+
const content = readFileSync(srcPath, "utf-8");
|
|
241
|
+
const description = extractDescription(content);
|
|
242
|
+
const normalizedName = file.replace(/^ocr-/, "");
|
|
243
|
+
const refContent = generateCommandReference(
|
|
244
|
+
normalizedName,
|
|
245
|
+
description
|
|
246
|
+
);
|
|
247
|
+
const destPath = join(ocrSubdir, normalizedName);
|
|
248
|
+
writeFileSync(destPath, refContent);
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
for (const file of commandFiles) {
|
|
252
|
+
const srcPath = join(commandsSource, file);
|
|
253
|
+
const content = readFileSync(srcPath, "utf-8");
|
|
254
|
+
const description = extractDescription(content);
|
|
255
|
+
const normalizedName = file.replace(/^ocr-/, "");
|
|
256
|
+
const destName = `ocr-${normalizedName}`;
|
|
257
|
+
const refContent = generateCommandReference(
|
|
258
|
+
normalizedName,
|
|
259
|
+
description
|
|
260
|
+
);
|
|
261
|
+
const destPath = join(toolCommandsDir, destName);
|
|
262
|
+
writeFileSync(destPath, refContent);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return true;
|
|
266
|
+
} catch {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function installForTool(tool, targetDir) {
|
|
271
|
+
const agentsPath = getAgentsPackagePath();
|
|
272
|
+
const ocrSkillsSource = join(agentsPath, "skills", "ocr");
|
|
273
|
+
const commandsSource = join(agentsPath, "commands");
|
|
274
|
+
const ocrDir = join(targetDir, ".ocr");
|
|
275
|
+
const ocrSkillsDest = join(ocrDir, "skills");
|
|
276
|
+
ensureDir(ocrDir);
|
|
277
|
+
ensureDir(join(ocrDir, "sessions"));
|
|
278
|
+
const gitignoreContent = `# OCR session files
|
|
279
|
+
sessions/
|
|
280
|
+
`;
|
|
281
|
+
const gitignorePath = join(ocrDir, ".gitignore");
|
|
282
|
+
if (!existsSync(gitignorePath)) {
|
|
283
|
+
writeFileSync(gitignorePath, gitignoreContent);
|
|
284
|
+
}
|
|
285
|
+
const configPath = join(ocrDir, "config.yaml");
|
|
286
|
+
let existingConfig = null;
|
|
287
|
+
if (existsSync(configPath)) {
|
|
288
|
+
try {
|
|
289
|
+
existingConfig = readFileSync(configPath);
|
|
290
|
+
} catch {
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const skillsOk = copyDirSafe(ocrSkillsSource, ocrSkillsDest);
|
|
294
|
+
if (!skillsOk) {
|
|
295
|
+
return {
|
|
296
|
+
tool,
|
|
297
|
+
success: false,
|
|
298
|
+
error: "Failed to install OCR skills to .ocr/"
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
const configSource = join(ocrSkillsSource, "assets", "config.yaml");
|
|
302
|
+
if (existingConfig) {
|
|
303
|
+
try {
|
|
304
|
+
writeFileSync(configPath, existingConfig);
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
} else if (existsSync(configSource)) {
|
|
308
|
+
copyFileSafe(configSource, configPath);
|
|
309
|
+
}
|
|
310
|
+
const duplicateConfig = join(ocrSkillsDest, "assets", "config.yaml");
|
|
311
|
+
if (existsSync(duplicateConfig)) {
|
|
312
|
+
try {
|
|
313
|
+
unlinkSync(duplicateConfig);
|
|
314
|
+
} catch {
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const commandsOk = installCommandsForTool(tool, commandsSource, targetDir);
|
|
318
|
+
if (!commandsOk) {
|
|
319
|
+
return {
|
|
320
|
+
tool,
|
|
321
|
+
success: false,
|
|
322
|
+
error: `Failed to install OCR commands to ${tool.commandsDir}`
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
tool,
|
|
327
|
+
success: true
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function detectInstalledTools(targetDir, tools) {
|
|
331
|
+
return tools.filter((tool) => {
|
|
332
|
+
const configPath = join(targetDir, tool.configDir);
|
|
333
|
+
return existsSync(configPath);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// packages/cli/src/lib/injector.ts
|
|
338
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
339
|
+
import { join as join2 } from "node:path";
|
|
340
|
+
var START_MARKER = "<!-- OCR:START -->";
|
|
341
|
+
var END_MARKER = "<!-- OCR:END -->";
|
|
342
|
+
var OCR_INSTRUCTION_BLOCK = `${START_MARKER}
|
|
343
|
+
# Open Code Review Instructions
|
|
344
|
+
|
|
345
|
+
These instructions are for AI assistants handling code review in this project.
|
|
346
|
+
|
|
347
|
+
Always open \`.ocr/skills/SKILL.md\` when the request:
|
|
348
|
+
- Asks for code review, PR review, or feedback on changes
|
|
349
|
+
- Mentions "review my code" or similar phrases
|
|
350
|
+
- Wants multi-perspective analysis of code quality
|
|
351
|
+
|
|
352
|
+
Use \`.ocr/skills/SKILL.md\` to learn:
|
|
353
|
+
- How to run the 8-phase review workflow
|
|
354
|
+
- Available reviewer personas and their focus areas
|
|
355
|
+
- Session management and output format
|
|
356
|
+
|
|
357
|
+
Keep this managed block so 'ocr init' can refresh the instructions.
|
|
358
|
+
|
|
359
|
+
${END_MARKER}`;
|
|
360
|
+
function injectOcrInstructions(filePath) {
|
|
361
|
+
try {
|
|
362
|
+
let content = existsSync2(filePath) ? readFileSync2(filePath, "utf-8") : "";
|
|
363
|
+
const regex = new RegExp(
|
|
364
|
+
`${escapeRegex(START_MARKER)}[\\s\\S]*?${escapeRegex(END_MARKER)}\\n?`,
|
|
365
|
+
"g"
|
|
366
|
+
);
|
|
367
|
+
content = content.replace(regex, "");
|
|
368
|
+
content = content.trim();
|
|
369
|
+
if (content.length > 0) {
|
|
370
|
+
content += "\n\n";
|
|
371
|
+
}
|
|
372
|
+
content += OCR_INSTRUCTION_BLOCK + "\n";
|
|
373
|
+
writeFileSync2(filePath, content);
|
|
374
|
+
return true;
|
|
375
|
+
} catch {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function escapeRegex(str) {
|
|
380
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
381
|
+
}
|
|
382
|
+
function injectIntoProjectFiles(targetDir) {
|
|
383
|
+
const agentsMdPath = join2(targetDir, "AGENTS.md");
|
|
384
|
+
const claudeMdPath = join2(targetDir, "CLAUDE.md");
|
|
385
|
+
const agentsMd = injectOcrInstructions(agentsMdPath);
|
|
386
|
+
const claudeMd = injectOcrInstructions(claudeMdPath);
|
|
387
|
+
return { agentsMd, claudeMd };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// packages/cli/src/commands/init.ts
|
|
391
|
+
var initCommand = new Command("init").description("Set up OCR for AI coding environments").option("-t, --tools <tools>", 'Comma-separated tool IDs or "all"').option("--no-inject", "Skip injecting instructions into AGENTS.md/CLAUDE.md").action(async (options) => {
|
|
392
|
+
console.log();
|
|
393
|
+
console.log(chalk.bold.cyan(" Open Code Review"));
|
|
394
|
+
console.log(chalk.dim(" AI-powered multi-agent code review"));
|
|
395
|
+
console.log();
|
|
396
|
+
const targetDir = process.cwd();
|
|
397
|
+
let selectedTools;
|
|
398
|
+
if (options.tools) {
|
|
399
|
+
try {
|
|
400
|
+
const toolIds = parseToolsArg(options.tools);
|
|
401
|
+
selectedTools = toolIds.map((id) => AI_TOOLS.find((t) => t.id === id)).filter((t) => t !== void 0);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.error(
|
|
404
|
+
chalk.red(
|
|
405
|
+
`Error: ${error instanceof Error ? error.message : "Invalid tools argument"}`
|
|
406
|
+
)
|
|
407
|
+
);
|
|
408
|
+
console.log();
|
|
409
|
+
console.log(
|
|
410
|
+
chalk.dim(`Valid tool IDs: ${AI_TOOLS.map((t) => t.id).join(", ")}`)
|
|
411
|
+
);
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
const installedTools = detectInstalledTools(targetDir, AI_TOOLS);
|
|
416
|
+
const choices = AI_TOOLS.map((tool) => {
|
|
417
|
+
const isInstalled = installedTools.some((t) => t.id === tool.id);
|
|
418
|
+
return {
|
|
419
|
+
name: isInstalled ? `${tool.name} ${chalk.dim("(detected)")}` : tool.name,
|
|
420
|
+
value: tool.id,
|
|
421
|
+
checked: isInstalled
|
|
422
|
+
};
|
|
423
|
+
});
|
|
424
|
+
try {
|
|
425
|
+
const selectedIds = await checkbox({
|
|
426
|
+
message: "Select AI tools to configure",
|
|
427
|
+
choices,
|
|
428
|
+
pageSize: 15
|
|
429
|
+
});
|
|
430
|
+
if (selectedIds.length === 0) {
|
|
431
|
+
console.log(chalk.yellow("No tools selected. Exiting."));
|
|
432
|
+
process.exit(0);
|
|
433
|
+
}
|
|
434
|
+
selectedTools = selectedIds.map((id) => AI_TOOLS.find((t) => t.id === id)).filter((t) => t !== void 0);
|
|
435
|
+
} catch {
|
|
436
|
+
console.log(chalk.yellow("\nOperation cancelled."));
|
|
437
|
+
process.exit(0);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
console.log();
|
|
441
|
+
const spinner = ora("Installing OCR...").start();
|
|
442
|
+
const results = [];
|
|
443
|
+
for (const tool of selectedTools) {
|
|
444
|
+
spinner.text = `Installing for ${tool.name}...`;
|
|
445
|
+
const result = installForTool(tool, targetDir);
|
|
446
|
+
results.push(result);
|
|
447
|
+
}
|
|
448
|
+
spinner.stop();
|
|
449
|
+
const successful = results.filter((r) => r.success);
|
|
450
|
+
const failed = results.filter((r) => !r.success);
|
|
451
|
+
if (successful.length > 0) {
|
|
452
|
+
console.log(chalk.green("\u2713 OCR installed successfully"));
|
|
453
|
+
console.log();
|
|
454
|
+
for (const result of successful) {
|
|
455
|
+
console.log(` ${chalk.green("\u2713")} ${result.tool.name}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (failed.length > 0) {
|
|
459
|
+
console.log();
|
|
460
|
+
console.log(chalk.red("\u2717 Some installations failed:"));
|
|
461
|
+
for (const result of failed) {
|
|
462
|
+
console.log(` ${chalk.red("\u2717")} ${result.tool.name}: ${result.error}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (options.inject && successful.length > 0) {
|
|
466
|
+
console.log();
|
|
467
|
+
const injectSpinner = ora(
|
|
468
|
+
"Injecting OCR instructions into project files..."
|
|
469
|
+
).start();
|
|
470
|
+
const injectResults = injectIntoProjectFiles(targetDir);
|
|
471
|
+
injectSpinner.stop();
|
|
472
|
+
if (injectResults.agentsMd || injectResults.claudeMd) {
|
|
473
|
+
console.log(chalk.green("\u2713 OCR instructions injected"));
|
|
474
|
+
if (injectResults.agentsMd) {
|
|
475
|
+
console.log(` ${chalk.green("\u2713")} AGENTS.md`);
|
|
476
|
+
}
|
|
477
|
+
if (injectResults.claudeMd) {
|
|
478
|
+
console.log(` ${chalk.green("\u2713")} CLAUDE.md`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
console.log();
|
|
483
|
+
console.log(chalk.bold("Next steps:"));
|
|
484
|
+
console.log();
|
|
485
|
+
console.log(
|
|
486
|
+
` ${chalk.cyan("1.")} Review ${chalk.yellow(".ocr/config.yaml")}`
|
|
487
|
+
);
|
|
488
|
+
console.log(
|
|
489
|
+
chalk.dim(
|
|
490
|
+
" Add project context, review rules, and customize discovery settings."
|
|
491
|
+
)
|
|
492
|
+
);
|
|
493
|
+
console.log();
|
|
494
|
+
console.log(
|
|
495
|
+
` ${chalk.cyan("2.")} Run ${chalk.yellow("/ocr-review")} to start a code review session.`
|
|
496
|
+
);
|
|
497
|
+
console.log();
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// packages/cli/src/commands/progress.ts
|
|
501
|
+
import { Command as Command2 } from "commander";
|
|
502
|
+
import chalk3 from "chalk";
|
|
503
|
+
import { watch } from "chokidar";
|
|
504
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
505
|
+
import { join as join4, basename } from "node:path";
|
|
506
|
+
import logUpdate from "log-update";
|
|
507
|
+
|
|
508
|
+
// packages/cli/src/lib/guards.ts
|
|
509
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "node:fs";
|
|
510
|
+
import { join as join3 } from "node:path";
|
|
511
|
+
import chalk2 from "chalk";
|
|
512
|
+
function checkOcrSetup(targetDir) {
|
|
513
|
+
const ocrDir = join3(targetDir, ".ocr");
|
|
514
|
+
const skillsDir = join3(ocrDir, "skills");
|
|
515
|
+
const sessionsDir = join3(ocrDir, "sessions");
|
|
516
|
+
const hasOcrDir = existsSync3(ocrDir);
|
|
517
|
+
const hasSkills = existsSync3(skillsDir);
|
|
518
|
+
const hasSessions = existsSync3(sessionsDir);
|
|
519
|
+
return {
|
|
520
|
+
valid: hasOcrDir && hasSkills,
|
|
521
|
+
ocrDir,
|
|
522
|
+
skillsDir,
|
|
523
|
+
sessionsDir,
|
|
524
|
+
hasSkills,
|
|
525
|
+
hasSessions
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
function requireOcrSetup(targetDir) {
|
|
529
|
+
const status = checkOcrSetup(targetDir);
|
|
530
|
+
if (!status.valid) {
|
|
531
|
+
console.log();
|
|
532
|
+
console.log(chalk2.red.bold(" \u2717 OCR is not set up in this directory"));
|
|
533
|
+
console.log();
|
|
534
|
+
if (!existsSync3(status.ocrDir)) {
|
|
535
|
+
console.log(chalk2.dim(" The .ocr directory was not found."));
|
|
536
|
+
} else if (!status.hasSkills) {
|
|
537
|
+
console.log(chalk2.dim(" The .ocr/skills directory is missing."));
|
|
538
|
+
console.log(chalk2.dim(" OCR may have been partially installed."));
|
|
539
|
+
}
|
|
540
|
+
console.log();
|
|
541
|
+
console.log(chalk2.dim(" To set up OCR, run:"));
|
|
542
|
+
console.log();
|
|
543
|
+
console.log(chalk2.white(" ocr init"));
|
|
544
|
+
console.log();
|
|
545
|
+
console.log(chalk2.dim(" Or with npx:"));
|
|
546
|
+
console.log();
|
|
547
|
+
console.log(chalk2.white(" npx @open-code-review/cli init"));
|
|
548
|
+
console.log();
|
|
549
|
+
process.exit(1);
|
|
550
|
+
}
|
|
551
|
+
return status;
|
|
552
|
+
}
|
|
553
|
+
function ensureSessionsDir(targetDir) {
|
|
554
|
+
const sessionsDir = join3(targetDir, ".ocr", "sessions");
|
|
555
|
+
if (!existsSync3(sessionsDir)) {
|
|
556
|
+
mkdirSync2(sessionsDir, { recursive: true });
|
|
557
|
+
}
|
|
558
|
+
return sessionsDir;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// packages/cli/src/commands/progress.ts
|
|
562
|
+
var TOTAL_PHASES = 8;
|
|
563
|
+
function findLatestSession(sessionsDir) {
|
|
564
|
+
if (!existsSync4(sessionsDir)) {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
const sessions = readdirSync2(sessionsDir).filter((name) => {
|
|
568
|
+
const sessionPath = join4(sessionsDir, name);
|
|
569
|
+
return statSync(sessionPath).isDirectory();
|
|
570
|
+
}).sort().reverse();
|
|
571
|
+
return sessions[0] ?? null;
|
|
572
|
+
}
|
|
573
|
+
function countFindings(filePath) {
|
|
574
|
+
if (!existsSync4(filePath)) {
|
|
575
|
+
return 0;
|
|
576
|
+
}
|
|
577
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
578
|
+
const findingMatches = content.match(/^##\s+(Finding|Issue|Suggestion)/gm);
|
|
579
|
+
return findingMatches?.length ?? 0;
|
|
580
|
+
}
|
|
581
|
+
function formatReviewerName(filename) {
|
|
582
|
+
const base = filename.replace(".md", "");
|
|
583
|
+
const match = base.match(/^(.+)-(\d+)$/);
|
|
584
|
+
if (match && match[1] && match[2]) {
|
|
585
|
+
const name = match[1].charAt(0).toUpperCase() + match[1].slice(1);
|
|
586
|
+
return `${name} #${match[2]}`;
|
|
587
|
+
}
|
|
588
|
+
return base.charAt(0).toUpperCase() + base.slice(1);
|
|
589
|
+
}
|
|
590
|
+
function parseSessionState(sessionPath, preservedStartTime) {
|
|
591
|
+
const session = basename(sessionPath);
|
|
592
|
+
const statePath = join4(sessionPath, "state.json");
|
|
593
|
+
if (!existsSync4(statePath)) {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
const stateContent = readFileSync3(statePath, "utf-8");
|
|
598
|
+
const state = JSON.parse(stateContent);
|
|
599
|
+
return parseFromStateJson(session, state, sessionPath, preservedStartTime);
|
|
600
|
+
} catch {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function parseFromStateJson(session, state, sessionPath, preservedStartTime) {
|
|
605
|
+
const startTime = preservedStartTime ?? (state.started_at ? new Date(state.started_at).getTime() : Date.now());
|
|
606
|
+
const completed = new Set(state.completed_phases);
|
|
607
|
+
const reviewsDir = join4(sessionPath, "reviews");
|
|
608
|
+
const reviewers = [];
|
|
609
|
+
if (existsSync4(reviewsDir)) {
|
|
610
|
+
const reviewFiles = readdirSync2(reviewsDir).filter(
|
|
611
|
+
(f) => f.endsWith(".md")
|
|
612
|
+
);
|
|
613
|
+
for (const file of reviewFiles) {
|
|
614
|
+
const reviewPath = join4(reviewsDir, file);
|
|
615
|
+
const findings = countFindings(reviewPath);
|
|
616
|
+
reviewers.push({
|
|
617
|
+
name: file.replace(".md", ""),
|
|
618
|
+
displayName: formatReviewerName(file),
|
|
619
|
+
status: "complete",
|
|
620
|
+
findings
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
session,
|
|
626
|
+
phase: state.current_phase,
|
|
627
|
+
phaseNumber: state.phase_number,
|
|
628
|
+
totalPhases: TOTAL_PHASES,
|
|
629
|
+
contextComplete: completed.has("context"),
|
|
630
|
+
requirementsComplete: completed.has("requirements"),
|
|
631
|
+
analysisComplete: completed.has("analysis"),
|
|
632
|
+
reviewsComplete: completed.has("reviews"),
|
|
633
|
+
aggregationComplete: completed.has("aggregation"),
|
|
634
|
+
discourseComplete: completed.has("discourse"),
|
|
635
|
+
synthesisComplete: completed.has("synthesis"),
|
|
636
|
+
reviewers,
|
|
637
|
+
startTime,
|
|
638
|
+
complete: state.current_phase === "complete"
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function formatDuration(ms) {
|
|
642
|
+
const seconds = Math.floor(ms / 1e3);
|
|
643
|
+
const minutes = Math.floor(seconds / 60);
|
|
644
|
+
const secs = seconds % 60;
|
|
645
|
+
return `${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
|
|
646
|
+
}
|
|
647
|
+
var PHASE_INFO = [
|
|
648
|
+
{ key: "context", label: "Context Discovery", icon: "\u{1F4CB}" },
|
|
649
|
+
{ key: "requirements", label: "Requirements Gathering", icon: "\u{1F4DD}" },
|
|
650
|
+
{ key: "analysis", label: "Tech Lead Analysis", icon: "\u{1F50D}" },
|
|
651
|
+
{ key: "reviews", label: "Parallel Reviews", icon: "\u{1F465}" },
|
|
652
|
+
{ key: "aggregation", label: "Aggregate Findings", icon: "\u{1F4CA}" },
|
|
653
|
+
{ key: "discourse", label: "Reviewer Discourse", icon: "\u{1F4AC}" },
|
|
654
|
+
{ key: "synthesis", label: "Final Synthesis", icon: "\u2728" },
|
|
655
|
+
{ key: "complete", label: "Review Complete", icon: "\u{1F389}" }
|
|
656
|
+
];
|
|
657
|
+
function getPhaseIcon(phaseKey, isComplete, isCurrent) {
|
|
658
|
+
if (isComplete) return chalk3.green("\u2713");
|
|
659
|
+
if (isCurrent) return chalk3.yellow("\u25CF");
|
|
660
|
+
return chalk3.dim("\u25CB");
|
|
661
|
+
}
|
|
662
|
+
function renderProgressBar(current, total) {
|
|
663
|
+
const width = 20;
|
|
664
|
+
const filled = Math.round(current / total * width);
|
|
665
|
+
const empty = width - filled;
|
|
666
|
+
const bar = chalk3.green("\u2588".repeat(filled)) + chalk3.dim("\u2591".repeat(empty));
|
|
667
|
+
const percent = Math.round(current / total * 100);
|
|
668
|
+
return `${bar} ${percent}%`;
|
|
669
|
+
}
|
|
670
|
+
function renderProgress(state) {
|
|
671
|
+
const lines = [];
|
|
672
|
+
const log = (line = "") => lines.push(line);
|
|
673
|
+
const title = "Open Code Review - Live Progress";
|
|
674
|
+
const boxWidth = title.length + 4;
|
|
675
|
+
const border = "\u2500".repeat(boxWidth);
|
|
676
|
+
log(chalk3.bold.cyan(` \u250C${border}\u2510`));
|
|
677
|
+
log(
|
|
678
|
+
chalk3.bold.cyan(" \u2502") + chalk3.bold(` ${title} `) + chalk3.bold.cyan("\u2502")
|
|
679
|
+
);
|
|
680
|
+
log(chalk3.bold.cyan(` \u2514${border}\u2518`));
|
|
681
|
+
log();
|
|
682
|
+
const elapsed = Date.now() - state.startTime;
|
|
683
|
+
log(chalk3.dim(` Session: `) + chalk3.white(state.session));
|
|
684
|
+
log(chalk3.dim(` Elapsed: `) + chalk3.white(formatDuration(elapsed)));
|
|
685
|
+
log();
|
|
686
|
+
const progressPhases = state.complete ? 8 : state.phaseNumber;
|
|
687
|
+
log(` ${renderProgressBar(progressPhases, 8)}`);
|
|
688
|
+
log();
|
|
689
|
+
const currentPhase = PHASE_INFO.find((p) => p.key === state.phase);
|
|
690
|
+
if (currentPhase && !state.complete) {
|
|
691
|
+
log(
|
|
692
|
+
chalk3.bold(` ${currentPhase.icon} `) + chalk3.bold.yellow(currentPhase.label) + chalk3.yellow(" in progress...")
|
|
693
|
+
);
|
|
694
|
+
log();
|
|
695
|
+
}
|
|
696
|
+
log(chalk3.dim(" \u2500\u2500\u2500 Workflow Phases \u2500\u2500\u2500"));
|
|
697
|
+
log();
|
|
698
|
+
const phaseCompletion = {
|
|
699
|
+
waiting: false,
|
|
700
|
+
context: state.contextComplete,
|
|
701
|
+
requirements: state.requirementsComplete,
|
|
702
|
+
analysis: state.analysisComplete,
|
|
703
|
+
reviews: state.reviewsComplete,
|
|
704
|
+
aggregation: state.aggregationComplete,
|
|
705
|
+
discourse: state.discourseComplete,
|
|
706
|
+
synthesis: state.synthesisComplete,
|
|
707
|
+
complete: state.complete
|
|
708
|
+
};
|
|
709
|
+
for (const phase of PHASE_INFO) {
|
|
710
|
+
const isComplete = phaseCompletion[phase.key];
|
|
711
|
+
const isCurrent = state.phase === phase.key && !state.complete;
|
|
712
|
+
const icon = getPhaseIcon(phase.key, isComplete, isCurrent);
|
|
713
|
+
let label = phase.label;
|
|
714
|
+
if (isCurrent) {
|
|
715
|
+
label = chalk3.yellow(label);
|
|
716
|
+
} else if (isComplete) {
|
|
717
|
+
label = chalk3.white(label);
|
|
718
|
+
} else {
|
|
719
|
+
label = chalk3.dim(label);
|
|
720
|
+
}
|
|
721
|
+
log(` ${icon} ${label}`);
|
|
722
|
+
if (phase.key === "reviews" && state.reviewers.length > 0) {
|
|
723
|
+
for (const reviewer of state.reviewers) {
|
|
724
|
+
const reviewerIcon = reviewer.status === "complete" ? chalk3.green("\u2713") : chalk3.dim("\u25CB");
|
|
725
|
+
const findings = reviewer.findings > 0 ? chalk3.cyan(
|
|
726
|
+
` \u2192 ${reviewer.findings} finding${reviewer.findings > 1 ? "s" : ""}`
|
|
727
|
+
) : "";
|
|
728
|
+
log(
|
|
729
|
+
chalk3.dim(` \u2514\u2500 `) + `${reviewerIcon} ${chalk3.dim(reviewer.displayName)}${findings}`
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
log();
|
|
735
|
+
if (state.complete) {
|
|
736
|
+
const totalFindings = state.reviewers.reduce(
|
|
737
|
+
(sum, r) => sum + r.findings,
|
|
738
|
+
0
|
|
739
|
+
);
|
|
740
|
+
log(chalk3.green.bold(" \u2705 Review Complete!"));
|
|
741
|
+
if (totalFindings > 0) {
|
|
742
|
+
log(
|
|
743
|
+
chalk3.dim(
|
|
744
|
+
` ${totalFindings} total finding${totalFindings > 1 ? "s" : ""} identified`
|
|
745
|
+
)
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
log();
|
|
749
|
+
log(
|
|
750
|
+
chalk3.dim(" Results saved to: ") + chalk3.white(`.ocr/sessions/${state.session}/final.md`)
|
|
751
|
+
);
|
|
752
|
+
} else {
|
|
753
|
+
log(chalk3.dim(" Press Ctrl+C to exit"));
|
|
754
|
+
}
|
|
755
|
+
log();
|
|
756
|
+
logUpdate(lines.join("\n"));
|
|
757
|
+
}
|
|
758
|
+
function renderWaiting() {
|
|
759
|
+
const lines = [];
|
|
760
|
+
const log = (line = "") => lines.push(line);
|
|
761
|
+
const title = "Open Code Review - Live Progress";
|
|
762
|
+
const boxWidth = title.length + 4;
|
|
763
|
+
const border = "\u2500".repeat(boxWidth);
|
|
764
|
+
log(chalk3.bold.cyan(` \u250C${border}\u2510`));
|
|
765
|
+
log(
|
|
766
|
+
chalk3.bold.cyan(" \u2502") + chalk3.bold(` ${title} `) + chalk3.bold.cyan("\u2502")
|
|
767
|
+
);
|
|
768
|
+
log(chalk3.bold.cyan(` \u2514${border}\u2518`));
|
|
769
|
+
log();
|
|
770
|
+
log(chalk3.dim(" Session: ") + chalk3.yellow("Waiting for session..."));
|
|
771
|
+
log();
|
|
772
|
+
const bar = chalk3.dim("\u2591".repeat(20));
|
|
773
|
+
log(` ${bar} 0%`);
|
|
774
|
+
log();
|
|
775
|
+
log(chalk3.yellow(" \u23F3 Waiting for a code review to begin..."));
|
|
776
|
+
log();
|
|
777
|
+
log(chalk3.dim(" \u2500\u2500\u2500 How to Start \u2500\u2500\u2500"));
|
|
778
|
+
log();
|
|
779
|
+
log(
|
|
780
|
+
chalk3.dim(" Run ") + chalk3.white("/ocr-review") + chalk3.dim(" in your AI assistant to begin")
|
|
781
|
+
);
|
|
782
|
+
log(chalk3.dim(" This display will update automatically"));
|
|
783
|
+
log();
|
|
784
|
+
log(chalk3.dim(" Press Ctrl+C to exit"));
|
|
785
|
+
log();
|
|
786
|
+
logUpdate(lines.join("\n"));
|
|
787
|
+
}
|
|
788
|
+
var progressCommand = new Command2("progress").description("Watch real-time progress of a code review session").option("-s, --session <name>", "Specify session name").action(async (options) => {
|
|
789
|
+
const targetDir = process.cwd();
|
|
790
|
+
requireOcrSetup(targetDir);
|
|
791
|
+
const sessionsDir = ensureSessionsDir(targetDir);
|
|
792
|
+
const ocrDir = join4(targetDir, ".ocr");
|
|
793
|
+
if (options.session) {
|
|
794
|
+
const sessionPath = join4(sessionsDir, options.session);
|
|
795
|
+
if (!existsSync4(sessionPath)) {
|
|
796
|
+
console.log(chalk3.red(`Session not found: ${options.session}`));
|
|
797
|
+
process.exit(1);
|
|
798
|
+
}
|
|
799
|
+
let state = parseSessionState(sessionPath);
|
|
800
|
+
if (!state) {
|
|
801
|
+
console.log(
|
|
802
|
+
chalk3.red(
|
|
803
|
+
`Session ${options.session} has no state.json - cannot track progress`
|
|
804
|
+
)
|
|
805
|
+
);
|
|
806
|
+
console.log(
|
|
807
|
+
chalk3.dim(
|
|
808
|
+
`The orchestrating agent must create state.json for progress tracking.`
|
|
809
|
+
)
|
|
810
|
+
);
|
|
811
|
+
process.exit(1);
|
|
812
|
+
}
|
|
813
|
+
let preservedStartTime2 = state.startTime;
|
|
814
|
+
renderProgress(state);
|
|
815
|
+
const timerInterval2 = setInterval(() => {
|
|
816
|
+
const newState = parseSessionState(sessionPath, preservedStartTime2);
|
|
817
|
+
if (newState) {
|
|
818
|
+
state = newState;
|
|
819
|
+
renderProgress(state);
|
|
820
|
+
}
|
|
821
|
+
}, 1e3);
|
|
822
|
+
const watcher = watch(sessionPath, {
|
|
823
|
+
persistent: true,
|
|
824
|
+
ignoreInitial: true,
|
|
825
|
+
depth: 2
|
|
826
|
+
});
|
|
827
|
+
watcher.on("all", () => {
|
|
828
|
+
const newState = parseSessionState(sessionPath, preservedStartTime2);
|
|
829
|
+
if (newState) {
|
|
830
|
+
state = newState;
|
|
831
|
+
renderProgress(state);
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
process.on("SIGINT", () => {
|
|
835
|
+
clearInterval(timerInterval2);
|
|
836
|
+
watcher.close();
|
|
837
|
+
logUpdate.done();
|
|
838
|
+
process.exit(0);
|
|
839
|
+
});
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
let currentSession = findLatestSession(sessionsDir);
|
|
843
|
+
let currentSessionPath = currentSession ? join4(sessionsDir, currentSession) : null;
|
|
844
|
+
let sessionWatcher = null;
|
|
845
|
+
let preservedStartTime;
|
|
846
|
+
const updateDisplay = () => {
|
|
847
|
+
if (currentSessionPath && existsSync4(currentSessionPath)) {
|
|
848
|
+
const state = parseSessionState(currentSessionPath, preservedStartTime);
|
|
849
|
+
if (state) {
|
|
850
|
+
if (!preservedStartTime) {
|
|
851
|
+
preservedStartTime = state.startTime;
|
|
852
|
+
}
|
|
853
|
+
renderProgress(state);
|
|
854
|
+
} else {
|
|
855
|
+
renderWaiting();
|
|
856
|
+
}
|
|
857
|
+
} else {
|
|
858
|
+
preservedStartTime = void 0;
|
|
859
|
+
renderWaiting();
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
const watchSession = (sessionPath) => {
|
|
863
|
+
if (sessionWatcher) {
|
|
864
|
+
sessionWatcher.close();
|
|
865
|
+
}
|
|
866
|
+
sessionWatcher = watch(sessionPath, {
|
|
867
|
+
persistent: true,
|
|
868
|
+
ignoreInitial: true,
|
|
869
|
+
depth: 2
|
|
870
|
+
});
|
|
871
|
+
sessionWatcher.on("all", updateDisplay);
|
|
872
|
+
};
|
|
873
|
+
updateDisplay();
|
|
874
|
+
if (currentSessionPath) {
|
|
875
|
+
watchSession(currentSessionPath);
|
|
876
|
+
}
|
|
877
|
+
const timerInterval = setInterval(updateDisplay, 1e3);
|
|
878
|
+
const watchDir = existsSync4(ocrDir) ? ocrDir : targetDir;
|
|
879
|
+
const dirWatcher = watch(watchDir, {
|
|
880
|
+
persistent: true,
|
|
881
|
+
ignoreInitial: true,
|
|
882
|
+
depth: 3
|
|
883
|
+
});
|
|
884
|
+
dirWatcher.on("addDir", (dirPath) => {
|
|
885
|
+
const parentDir = join4(dirPath, "..");
|
|
886
|
+
const isDirectChild = parentDir.endsWith("sessions") || parentDir.endsWith(join4(".ocr", "sessions"));
|
|
887
|
+
if (isDirectChild && !dirPath.endsWith("sessions")) {
|
|
888
|
+
const newSession = basename(dirPath);
|
|
889
|
+
currentSession = newSession;
|
|
890
|
+
currentSessionPath = dirPath;
|
|
891
|
+
preservedStartTime = void 0;
|
|
892
|
+
watchSession(dirPath);
|
|
893
|
+
updateDisplay();
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
dirWatcher.on("add", updateDisplay);
|
|
897
|
+
dirWatcher.on("change", updateDisplay);
|
|
898
|
+
process.on("SIGINT", () => {
|
|
899
|
+
clearInterval(timerInterval);
|
|
900
|
+
dirWatcher.close();
|
|
901
|
+
if (sessionWatcher) sessionWatcher.close();
|
|
902
|
+
logUpdate.done();
|
|
903
|
+
process.exit(0);
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
// packages/cli/src/commands/update.ts
|
|
908
|
+
import { Command as Command3 } from "commander";
|
|
909
|
+
import chalk4 from "chalk";
|
|
910
|
+
import ora2 from "ora";
|
|
911
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
912
|
+
import { join as join5 } from "node:path";
|
|
913
|
+
function detectConfiguredTools(targetDir) {
|
|
914
|
+
return AI_TOOLS.filter((tool) => {
|
|
915
|
+
if (tool.commandStrategy === "subdirectory") {
|
|
916
|
+
const ocrDir = join5(targetDir, tool.commandsDir, "ocr");
|
|
917
|
+
return existsSync5(ocrDir);
|
|
918
|
+
} else {
|
|
919
|
+
const reviewCmd = join5(targetDir, tool.commandsDir, "ocr-review.md");
|
|
920
|
+
return existsSync5(reviewCmd);
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
var updateCommand = new Command3("update").description("Update OCR assets after package upgrade").option("--commands", "Update only commands/workflows").option(
|
|
925
|
+
"--skills",
|
|
926
|
+
"Update only skills (includes templates, references, assets)"
|
|
927
|
+
).option("--inject", "Update only AGENTS.md/CLAUDE.md injection").option("--dry-run", "Preview changes without modifying files").action(async (options) => {
|
|
928
|
+
const targetDir = process.cwd();
|
|
929
|
+
requireOcrSetup(targetDir);
|
|
930
|
+
console.log();
|
|
931
|
+
console.log(chalk4.bold.cyan(" Open Code Review - Update"));
|
|
932
|
+
console.log();
|
|
933
|
+
const configuredTools = detectConfiguredTools(targetDir);
|
|
934
|
+
const installedTools = detectInstalledTools(targetDir, AI_TOOLS);
|
|
935
|
+
const toolsToUpdate = AI_TOOLS.filter(
|
|
936
|
+
(tool) => configuredTools.some((t) => t.id === tool.id) || installedTools.some((t) => t.id === tool.id)
|
|
937
|
+
);
|
|
938
|
+
if (toolsToUpdate.length === 0) {
|
|
939
|
+
console.log(chalk4.yellow(" No configured AI tools found."));
|
|
940
|
+
console.log(chalk4.dim(" Run `ocr init` to set up OCR first."));
|
|
941
|
+
console.log();
|
|
942
|
+
process.exit(1);
|
|
943
|
+
}
|
|
944
|
+
const hasSpecificFlag = options.commands || options.skills || options.inject;
|
|
945
|
+
const updateCommands = options.commands || !hasSpecificFlag;
|
|
946
|
+
const updateSkills = options.skills || !hasSpecificFlag;
|
|
947
|
+
const updateInject = options.inject || !hasSpecificFlag;
|
|
948
|
+
if (options.dryRun) {
|
|
949
|
+
console.log(chalk4.yellow(" Dry run mode - no files will be modified"));
|
|
950
|
+
console.log();
|
|
951
|
+
}
|
|
952
|
+
console.log(chalk4.dim(" Detected tools:"));
|
|
953
|
+
for (const tool of toolsToUpdate) {
|
|
954
|
+
console.log(` \u2022 ${tool.name}`);
|
|
955
|
+
}
|
|
956
|
+
console.log();
|
|
957
|
+
if (updateCommands || updateSkills) {
|
|
958
|
+
if (options.dryRun) {
|
|
959
|
+
console.log(chalk4.dim(" Would update:"));
|
|
960
|
+
console.log(chalk4.dim(" \u2022 .ocr/skills/SKILL.md (main skill)"));
|
|
961
|
+
console.log(
|
|
962
|
+
chalk4.dim(" \u2022 .ocr/skills/references/ (workflow, reviewers)")
|
|
963
|
+
);
|
|
964
|
+
console.log(chalk4.dim(" \u2022 .ocr/skills/assets/reviewer-template.md"));
|
|
965
|
+
console.log(
|
|
966
|
+
chalk4.dim(" \u2022 .ocr/config.yaml (preserved if customized)")
|
|
967
|
+
);
|
|
968
|
+
for (const tool of toolsToUpdate) {
|
|
969
|
+
if (tool.commandStrategy === "subdirectory") {
|
|
970
|
+
console.log(chalk4.dim(` \u2022 ${tool.commandsDir}/ocr/ (commands)`));
|
|
971
|
+
} else {
|
|
972
|
+
console.log(
|
|
973
|
+
chalk4.dim(` \u2022 ${tool.commandsDir}/ocr-*.md (commands)`)
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
console.log();
|
|
978
|
+
} else {
|
|
979
|
+
const spinner = ora2("Updating OCR commands and skills...").start();
|
|
980
|
+
const results = [];
|
|
981
|
+
for (const tool of toolsToUpdate) {
|
|
982
|
+
spinner.text = `Updating ${tool.name}...`;
|
|
983
|
+
const result = installForTool(tool, targetDir);
|
|
984
|
+
results.push(result);
|
|
985
|
+
}
|
|
986
|
+
spinner.stop();
|
|
987
|
+
const successful = results.filter((r) => r.success);
|
|
988
|
+
const failed = results.filter((r) => !r.success);
|
|
989
|
+
if (successful.length > 0) {
|
|
990
|
+
console.log(chalk4.green(" \u2713 Commands and skills updated"));
|
|
991
|
+
console.log(
|
|
992
|
+
chalk4.dim(" Including: SKILL.md, references/, assets/")
|
|
993
|
+
);
|
|
994
|
+
for (const result of successful) {
|
|
995
|
+
console.log(` ${chalk4.green("\u2713")} ${result.tool.name}`);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (failed.length > 0) {
|
|
999
|
+
console.log();
|
|
1000
|
+
console.log(chalk4.red(" \u2717 Some updates failed:"));
|
|
1001
|
+
for (const result of failed) {
|
|
1002
|
+
console.log(
|
|
1003
|
+
` ${chalk4.red("\u2717")} ${result.tool.name}: ${result.error}`
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
console.log();
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
if (updateInject) {
|
|
1011
|
+
if (options.dryRun) {
|
|
1012
|
+
console.log(chalk4.dim(" Would update:"));
|
|
1013
|
+
if (existsSync5(join5(targetDir, "AGENTS.md"))) {
|
|
1014
|
+
console.log(chalk4.dim(" \u2022 AGENTS.md (OCR managed block)"));
|
|
1015
|
+
}
|
|
1016
|
+
if (existsSync5(join5(targetDir, "CLAUDE.md"))) {
|
|
1017
|
+
console.log(chalk4.dim(" \u2022 CLAUDE.md (OCR managed block)"));
|
|
1018
|
+
}
|
|
1019
|
+
console.log();
|
|
1020
|
+
} else {
|
|
1021
|
+
const spinner = ora2("Updating AGENTS.md/CLAUDE.md...").start();
|
|
1022
|
+
const injectResults = injectIntoProjectFiles(targetDir);
|
|
1023
|
+
spinner.stop();
|
|
1024
|
+
if (injectResults.agentsMd || injectResults.claudeMd) {
|
|
1025
|
+
console.log(chalk4.green(" \u2713 Instructions updated"));
|
|
1026
|
+
if (injectResults.agentsMd) {
|
|
1027
|
+
console.log(` ${chalk4.green("\u2713")} AGENTS.md`);
|
|
1028
|
+
}
|
|
1029
|
+
if (injectResults.claudeMd) {
|
|
1030
|
+
console.log(` ${chalk4.green("\u2713")} CLAUDE.md`);
|
|
1031
|
+
}
|
|
1032
|
+
} else {
|
|
1033
|
+
console.log(chalk4.dim(" No instruction files to update"));
|
|
1034
|
+
}
|
|
1035
|
+
console.log();
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (options.dryRun) {
|
|
1039
|
+
console.log(chalk4.dim(" Run without --dry-run to apply changes."));
|
|
1040
|
+
} else {
|
|
1041
|
+
console.log(chalk4.green(" \u2713 Update complete"));
|
|
1042
|
+
}
|
|
1043
|
+
console.log();
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
// packages/cli/src/index.ts
|
|
1047
|
+
var program = new Command4();
|
|
1048
|
+
program.name("ocr").description("Open Code Review - AI-powered multi-agent code review").version("1.0.0");
|
|
1049
|
+
program.addCommand(initCommand);
|
|
1050
|
+
program.addCommand(progressCommand);
|
|
1051
|
+
program.addCommand(updateCommand);
|
|
1052
|
+
program.parse();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@open-code-review/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI for Open Code Review - Multi-environment setup and progress tracking",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ocr": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"code-review",
|
|
14
|
+
"ai",
|
|
15
|
+
"cli",
|
|
16
|
+
"claude",
|
|
17
|
+
"cursor",
|
|
18
|
+
"windsurf"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/spencermarx/open-code-review",
|
|
23
|
+
"directory": "packages/cli"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "Spencer Marx",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20.0.0"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@inquirer/prompts": "^7.2.0",
|
|
32
|
+
"@open-code-review/agents": "workspace:*",
|
|
33
|
+
"chalk": "^5.4.1",
|
|
34
|
+
"chokidar": "^4.0.3",
|
|
35
|
+
"commander": "^13.0.0",
|
|
36
|
+
"log-update": "^7.0.2",
|
|
37
|
+
"ora": "^8.1.1"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@open-code-review/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI for Open Code Review - Multi-environment setup and progress tracking",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ocr": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"code-review",
|
|
14
|
+
"ai",
|
|
15
|
+
"cli",
|
|
16
|
+
"claude",
|
|
17
|
+
"cursor",
|
|
18
|
+
"windsurf"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/spencermarx/open-code-review",
|
|
23
|
+
"directory": "packages/cli"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "Spencer Marx",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20.0.0"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@inquirer/prompts": "^7.2.0",
|
|
32
|
+
"chalk": "^5.4.1",
|
|
33
|
+
"chokidar": "^4.0.3",
|
|
34
|
+
"commander": "^13.0.0",
|
|
35
|
+
"log-update": "^7.0.2",
|
|
36
|
+
"ora": "^8.1.1",
|
|
37
|
+
"@open-code-review/agents": "1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|