@mechanai/deepreview 2.0.1 → 2.0.2
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 +24 -7
- package/package.json +7 -3
- package/src/setup.test.ts +159 -0
- package/src/setup.ts +151 -0
package/README.md
CHANGED
|
@@ -6,15 +6,28 @@ implementation plan.
|
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Run the setup script:
|
|
10
10
|
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
```bash
|
|
12
|
+
bunx @mechanai/deepreview@latest/setup # Global install (~/.config/opencode/)
|
|
13
|
+
bunx @mechanai/deepreview@latest/setup --local # Project-level install (.opencode/)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or with Node.js (v22+):
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx @mechanai/deepreview@latest/setup
|
|
20
|
+
npx @mechanai/deepreview@latest/setup --local
|
|
15
21
|
```
|
|
16
22
|
|
|
17
|
-
|
|
23
|
+
This will:
|
|
24
|
+
|
|
25
|
+
1. Add `@mechanai/deepreview` to the `plugin` array in your `opencode.json` (creates the file if needed)
|
|
26
|
+
2. Symlink agents and commands into the appropriate config directory
|
|
27
|
+
|
|
28
|
+
> [!NOTE]
|
|
29
|
+
> The symlinks are needed because OpenCode does not yet auto-discover
|
|
30
|
+
> agents and commands from installed plugin packages.
|
|
18
31
|
|
|
19
32
|
## Usage
|
|
20
33
|
|
|
@@ -62,12 +75,16 @@ its own context, keeping token usage minimal.
|
|
|
62
75
|
## Requirements
|
|
63
76
|
|
|
64
77
|
- [OpenCode](https://opencode.ai)
|
|
78
|
+
- [Bun](https://bun.sh/) >= 1.2 or [Node.js](https://nodejs.org/) >= 22
|
|
65
79
|
- `git`
|
|
66
80
|
- `gh` CLI (only for PR commands)
|
|
67
81
|
|
|
68
82
|
> [!NOTE]
|
|
69
83
|
> If upgrading from the old `npx @anthropic/deepreview install` workflow, remove
|
|
70
|
-
>
|
|
84
|
+
> the old copied files first (`rm ~/.config/opencode/agents/deepreview*
|
|
85
|
+
~/.config/opencode/commands/deepreview*`), then run the setup script above.
|
|
86
|
+
> The setup script uses symlinks instead of copies, so future upgrades only
|
|
87
|
+
> require re-running the script.
|
|
71
88
|
|
|
72
89
|
## Development
|
|
73
90
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mechanai/deepreview",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "Multi-agent parallel code/spec review for OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"src/",
|
|
12
|
+
"dist/",
|
|
12
13
|
".opencode/"
|
|
13
14
|
],
|
|
14
15
|
"type": "module",
|
|
15
16
|
"main": "./.opencode/plugins/deepreview.ts",
|
|
16
17
|
"exports": {
|
|
17
18
|
".": "./.opencode/plugins/deepreview.ts",
|
|
18
|
-
"./api": "./src/post-review.ts"
|
|
19
|
+
"./api": "./src/post-review.ts",
|
|
20
|
+
"./setup": "./dist/setup.mjs"
|
|
19
21
|
},
|
|
20
22
|
"publishConfig": {
|
|
21
23
|
"access": "public"
|
|
@@ -23,6 +25,7 @@
|
|
|
23
25
|
"dependencies": {
|
|
24
26
|
"gray-matter": "4.0.3",
|
|
25
27
|
"js-yaml": "4.1.0",
|
|
28
|
+
"jsonc-parser": "3.3.1",
|
|
26
29
|
"parse-diff": "0.11.1"
|
|
27
30
|
},
|
|
28
31
|
"devDependencies": {
|
|
@@ -42,6 +45,7 @@
|
|
|
42
45
|
}
|
|
43
46
|
},
|
|
44
47
|
"engines": {
|
|
45
|
-
"bun": ">=1.2.0"
|
|
48
|
+
"bun": ">=1.2.0",
|
|
49
|
+
"node": ">=22.0.0"
|
|
46
50
|
}
|
|
47
51
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
symlinkSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
|
|
13
|
+
const setupScript = path.resolve(import.meta.dirname, "setup.ts");
|
|
14
|
+
|
|
15
|
+
function makeTempDir(): string {
|
|
16
|
+
const dir = path.join(
|
|
17
|
+
os.tmpdir(),
|
|
18
|
+
`deepreview-setup-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
19
|
+
);
|
|
20
|
+
mkdirSync(dir, { recursive: true });
|
|
21
|
+
return dir;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function run(cwd: string, args: string[] = []) {
|
|
25
|
+
return Bun.spawnSync(["bun", "run", setupScript, ...args], {
|
|
26
|
+
cwd,
|
|
27
|
+
env: { ...process.env, XDG_CONFIG_HOME: cwd },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readConfig(filePath: string): { plugin?: unknown } {
|
|
32
|
+
const parsed: unknown = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
33
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
34
|
+
throw new Error("config is not an object");
|
|
35
|
+
}
|
|
36
|
+
return parsed as { plugin?: unknown };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("setup script - config", () => {
|
|
40
|
+
let tempDir: string;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
tempDir = makeTempDir();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(async () => {
|
|
47
|
+
await Bun.$`rm -rf ${tempDir}`;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("creates opencode.json with plugin when no config exists (global)", () => {
|
|
51
|
+
const result = run(tempDir);
|
|
52
|
+
expect(result.exitCode).toBe(0);
|
|
53
|
+
|
|
54
|
+
const configPath = path.join(tempDir, "opencode", "opencode.json");
|
|
55
|
+
expect(existsSync(configPath)).toBe(true);
|
|
56
|
+
|
|
57
|
+
const config = readConfig(configPath);
|
|
58
|
+
expect(config.plugin).toContain("@mechanai/deepreview");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("--local installs into .opencode/ in cwd", () => {
|
|
62
|
+
const result = run(tempDir, ["--local"]);
|
|
63
|
+
expect(result.exitCode).toBe(0);
|
|
64
|
+
|
|
65
|
+
const configPath = path.join(tempDir, "opencode.json");
|
|
66
|
+
expect(existsSync(configPath)).toBe(true);
|
|
67
|
+
|
|
68
|
+
const agentsDir = path.join(tempDir, ".opencode", "agents");
|
|
69
|
+
expect(existsSync(path.join(agentsDir, "deepreview-synthesizer.md"))).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("adds plugin to existing config without duplicating", () => {
|
|
73
|
+
const configPath = path.join(tempDir, "opencode");
|
|
74
|
+
mkdirSync(configPath, { recursive: true });
|
|
75
|
+
writeFileSync(
|
|
76
|
+
path.join(configPath, "opencode.json"),
|
|
77
|
+
JSON.stringify({ plugin: ["other-plugin"] }, null, 2),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
run(tempDir);
|
|
81
|
+
const config = readConfig(path.join(configPath, "opencode.json"));
|
|
82
|
+
expect(config.plugin).toEqual(["other-plugin", "@mechanai/deepreview"]);
|
|
83
|
+
|
|
84
|
+
// Run again — should not duplicate
|
|
85
|
+
run(tempDir);
|
|
86
|
+
const config2 = readConfig(path.join(configPath, "opencode.json"));
|
|
87
|
+
expect(config2.plugin).toEqual(["other-plugin", "@mechanai/deepreview"]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("preserves JSONC comments", () => {
|
|
91
|
+
const configDir = path.join(tempDir, "opencode");
|
|
92
|
+
mkdirSync(configDir, { recursive: true });
|
|
93
|
+
const content = '{\n // My comment\n "provider": {}\n}\n';
|
|
94
|
+
writeFileSync(path.join(configDir, "opencode.jsonc"), content);
|
|
95
|
+
|
|
96
|
+
run(tempDir);
|
|
97
|
+
const result = readFileSync(path.join(configDir, "opencode.jsonc"), "utf-8");
|
|
98
|
+
expect(result).toContain("// My comment");
|
|
99
|
+
expect(result).toContain("@mechanai/deepreview");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("setup script - symlinks", () => {
|
|
104
|
+
let tempDir: string;
|
|
105
|
+
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
tempDir = makeTempDir();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
afterEach(async () => {
|
|
111
|
+
await Bun.$`rm -rf ${tempDir}`;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("creates symlinks for agents and commands (global)", () => {
|
|
115
|
+
const result = run(tempDir);
|
|
116
|
+
expect(result.exitCode).toBe(0);
|
|
117
|
+
|
|
118
|
+
const agentsDir = path.join(tempDir, "opencode", "agents");
|
|
119
|
+
const synthesizer = path.join(agentsDir, "deepreview-synthesizer.md");
|
|
120
|
+
expect(existsSync(synthesizer)).toBe(true);
|
|
121
|
+
expect(lstatSync(synthesizer).isSymbolicLink()).toBe(true);
|
|
122
|
+
|
|
123
|
+
const commandsDir = path.join(tempDir, "opencode", "commands");
|
|
124
|
+
const mainCmd = path.join(commandsDir, "deepreview.md");
|
|
125
|
+
expect(existsSync(mainCmd)).toBe(true);
|
|
126
|
+
expect(lstatSync(mainCmd).isSymbolicLink()).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("removes stale symlinks on upgrade", () => {
|
|
130
|
+
const agentsDir = path.join(tempDir, "opencode", "agents");
|
|
131
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
132
|
+
|
|
133
|
+
// Create a fake stale symlink that contains "deepreview" but doesn't exist in package
|
|
134
|
+
symlinkSync("/nonexistent", path.join(agentsDir, "deepreview-old-agent.md"));
|
|
135
|
+
|
|
136
|
+
run(tempDir);
|
|
137
|
+
|
|
138
|
+
// Stale symlink should be removed
|
|
139
|
+
expect(existsSync(path.join(agentsDir, "deepreview-old-agent.md"))).toBe(false);
|
|
140
|
+
// But real agents should exist
|
|
141
|
+
expect(existsSync(path.join(agentsDir, "deepreview-synthesizer.md"))).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("handles dangling symlinks at dest gracefully", () => {
|
|
145
|
+
const agentsDir = path.join(tempDir, "opencode", "agents");
|
|
146
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
147
|
+
|
|
148
|
+
// Create a dangling symlink where a real agent should go
|
|
149
|
+
symlinkSync("/nonexistent", path.join(agentsDir, "deepreview-synthesizer.md"));
|
|
150
|
+
|
|
151
|
+
const result = run(tempDir);
|
|
152
|
+
expect(result.exitCode).toBe(0);
|
|
153
|
+
|
|
154
|
+
// Should have replaced the dangling symlink
|
|
155
|
+
const dest = path.join(agentsDir, "deepreview-synthesizer.md");
|
|
156
|
+
expect(lstatSync(dest).isSymbolicLink()).toBe(true);
|
|
157
|
+
expect(existsSync(dest)).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
});
|
package/src/setup.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup script for @mechanai/deepreview.
|
|
3
|
+
*
|
|
4
|
+
* Ensures the plugin is registered in opencode.json and symlinks agents/commands
|
|
5
|
+
* into the OpenCode config directory.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* bunx @mechanai/deepreview/setup # Install globally (~/.config/opencode/)
|
|
9
|
+
* bunx @mechanai/deepreview/setup --local # Install into current project (.opencode/)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
existsSync,
|
|
14
|
+
lstatSync,
|
|
15
|
+
mkdirSync,
|
|
16
|
+
readdirSync,
|
|
17
|
+
symlinkSync,
|
|
18
|
+
unlinkSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
writeFileSync,
|
|
21
|
+
} from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import os from "node:os";
|
|
24
|
+
import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser";
|
|
25
|
+
|
|
26
|
+
const PACKAGE_NAME = "@mechanai/deepreview";
|
|
27
|
+
const local = process.argv.includes("--local");
|
|
28
|
+
const cwd = process.cwd();
|
|
29
|
+
|
|
30
|
+
const globalConfigDir = path.join(
|
|
31
|
+
process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"),
|
|
32
|
+
"opencode",
|
|
33
|
+
);
|
|
34
|
+
const targetDir = local ? path.join(cwd, ".opencode") : globalConfigDir;
|
|
35
|
+
|
|
36
|
+
// Resolve the package directory (where this script lives)
|
|
37
|
+
const packageDir = path.resolve(import.meta.dirname, "..");
|
|
38
|
+
const packageOpencode = path.join(packageDir, ".opencode");
|
|
39
|
+
|
|
40
|
+
function ensurePluginInConfig() {
|
|
41
|
+
const configFiles = ["opencode.jsonc", "opencode.json"];
|
|
42
|
+
const searchDir = local ? cwd : globalConfigDir;
|
|
43
|
+
let configPath: string | undefined;
|
|
44
|
+
|
|
45
|
+
for (const file of configFiles) {
|
|
46
|
+
const candidate = path.join(searchDir, file);
|
|
47
|
+
if (existsSync(candidate)) {
|
|
48
|
+
configPath = candidate;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (configPath === undefined) {
|
|
54
|
+
configPath = path.join(searchDir, "opencode.json");
|
|
55
|
+
mkdirSync(searchDir, { recursive: true });
|
|
56
|
+
writeFileSync(configPath, JSON.stringify({ plugin: [PACKAGE_NAME] }, null, 2) + "\n");
|
|
57
|
+
console.log(`Created ${configPath} with plugin entry.`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
62
|
+
|
|
63
|
+
// Check if plugin is already registered
|
|
64
|
+
let config: Record<string, unknown>;
|
|
65
|
+
try {
|
|
66
|
+
const parsed: unknown = parseJsonc(raw);
|
|
67
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
68
|
+
throw new Error("not an object");
|
|
69
|
+
}
|
|
70
|
+
// oxlint-disable-next-line no-unsafe-type-assertion -- Why: validated above with type guards
|
|
71
|
+
config = parsed as Record<string, unknown>;
|
|
72
|
+
} catch {
|
|
73
|
+
console.error(`Could not parse ${configPath}. Add the plugin manually:`);
|
|
74
|
+
console.error(` "plugin": ["${PACKAGE_NAME}"]`);
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const pluginArray = Array.isArray(config.plugin) ? config.plugin : [];
|
|
80
|
+
const plugins = pluginArray.filter((p): p is string => typeof p === "string");
|
|
81
|
+
if (plugins.includes(PACKAGE_NAME)) {
|
|
82
|
+
console.log(`Plugin already registered in ${configPath}.`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Use jsonc-parser to insert into the plugin array without stripping comments
|
|
87
|
+
const formatting = { formattingOptions: { insertSpaces: true, tabSize: 2 } };
|
|
88
|
+
const edits = Array.isArray(config.plugin)
|
|
89
|
+
? modify(raw, ["plugin", pluginArray.length], PACKAGE_NAME, formatting)
|
|
90
|
+
: modify(raw, ["plugin"], [PACKAGE_NAME], formatting);
|
|
91
|
+
|
|
92
|
+
writeFileSync(configPath, applyEdits(raw, edits));
|
|
93
|
+
console.log(`Added "${PACKAGE_NAME}" to plugin array in ${configPath}.`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function symlinkDirectory(kind: "agents" | "commands") {
|
|
97
|
+
const sourceDir = path.join(packageOpencode, kind);
|
|
98
|
+
if (!existsSync(sourceDir)) return;
|
|
99
|
+
|
|
100
|
+
const destDir = path.join(targetDir, kind);
|
|
101
|
+
mkdirSync(destDir, { recursive: true });
|
|
102
|
+
|
|
103
|
+
const sourceFiles = new Set(readdirSync(sourceDir).filter((f) => f.endsWith(".md")));
|
|
104
|
+
let created = 0;
|
|
105
|
+
|
|
106
|
+
// Remove stale deepreview symlinks that no longer exist in the package
|
|
107
|
+
for (const file of readdirSync(destDir)) {
|
|
108
|
+
if (!file.startsWith("deepreview-") && !file.startsWith("_deepreview-")) continue;
|
|
109
|
+
const dest = path.join(destDir, file);
|
|
110
|
+
try {
|
|
111
|
+
if (lstatSync(dest).isSymbolicLink() && !sourceFiles.has(file)) {
|
|
112
|
+
unlinkSync(dest);
|
|
113
|
+
}
|
|
114
|
+
} catch (err: unknown) {
|
|
115
|
+
if (err instanceof Error && !("code" in err && err.code === "ENOENT")) {
|
|
116
|
+
console.warn(`Could not check ${dest}: ${err.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const file of sourceFiles) {
|
|
122
|
+
const dest = path.join(destDir, file);
|
|
123
|
+
const source = path.relative(destDir, path.join(sourceDir, file));
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
// Use lstatSync to detect both regular files and dangling symlinks
|
|
127
|
+
const stat = lstatSync(dest);
|
|
128
|
+
if (!stat.isSymbolicLink()) {
|
|
129
|
+
console.warn(`Skipping ${dest}: not a symlink (would overwrite regular file)`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
unlinkSync(dest);
|
|
133
|
+
} catch (err: unknown) {
|
|
134
|
+
if (err instanceof Error && "code" in err && err.code !== "ENOENT") {
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
symlinkSync(source, dest);
|
|
139
|
+
created++;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const label = local ? `.opencode/${kind}/` : path.join(targetDir, kind) + "/";
|
|
143
|
+
console.log(`Linked ${created} ${kind} into ${label}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Run
|
|
147
|
+
ensurePluginInConfig();
|
|
148
|
+
symlinkDirectory("agents");
|
|
149
|
+
symlinkDirectory("commands");
|
|
150
|
+
const scope = local ? "project" : "global";
|
|
151
|
+
console.log(`Done (${scope}). Run opencode to use /deepreview commands.`);
|