Skip to content

Commit 2198734

Browse files
committed
feat: add --dry-run mode to install/uninstall scripts
Signed-off-by: leocavalcante <leo@cavalcante.dev>
1 parent 7b619b5 commit 2198734

File tree

7 files changed

+159
-74
lines changed

7 files changed

+159
-74
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
],
3939
"author": "OpenCode Community",
4040
"license": "MIT",
41+
"dependencies": {
42+
"@opencode-ai/plugin": "^0.1.0"
43+
},
4144
"devDependencies": {
4245
"@biomejs/biome": "^2.3.11",
4346
"@types/bun": "latest"

postinstall.mjs

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import {
2020
const packageRoot = getPackageRoot(import.meta.url)
2121
const AGENTS_SOURCE_DIR = getAgentsSourceDir(packageRoot)
2222

23+
/** Check for --dry-run flag in command line arguments */
24+
const DRY_RUN = process.argv.includes("--dry-run")
25+
2326
/** Minimum character count for valid agent files */
2427
const MIN_CONTENT_LENGTH = 100
2528

@@ -89,25 +92,30 @@ function validateAgentContent(filePath) {
8992
* or all file copies failed
9093
*/
9194
function main() {
92-
console.log("opencode-plugin-opencoder: Installing agents...")
95+
const prefix = DRY_RUN ? "[DRY-RUN] " : ""
96+
console.log(`${prefix}opencode-plugin-opencoder: Installing agents...`)
9397

9498
// Create target directory if it doesn't exist
9599
if (!existsSync(AGENTS_TARGET_DIR)) {
96-
mkdirSync(AGENTS_TARGET_DIR, { recursive: true })
97-
console.log(` Created ${AGENTS_TARGET_DIR}`)
100+
if (DRY_RUN) {
101+
console.log(`${prefix}Would create ${AGENTS_TARGET_DIR}`)
102+
} else {
103+
mkdirSync(AGENTS_TARGET_DIR, { recursive: true })
104+
console.log(` Created ${AGENTS_TARGET_DIR}`)
105+
}
98106
}
99107

100108
// Check if source directory exists
101109
if (!existsSync(AGENTS_SOURCE_DIR)) {
102-
console.error(` Error: Source agents directory not found at ${AGENTS_SOURCE_DIR}`)
110+
console.error(`${prefix} Error: Source agents directory not found at ${AGENTS_SOURCE_DIR}`)
103111
process.exit(1)
104112
}
105113

106114
// Copy all .md files from agents/ to target
107115
const files = readdirSync(AGENTS_SOURCE_DIR).filter((f) => f.endsWith(".md"))
108116

109117
if (files.length === 0) {
110-
console.error(" Error: No agent files found in agents/ directory")
118+
console.error(`${prefix} Error: No agent files found in agents/ directory`)
111119
process.exit(1)
112120
}
113121

@@ -119,54 +127,68 @@ function main() {
119127
const targetPath = join(AGENTS_TARGET_DIR, file)
120128

121129
try {
122-
copyFileSync(sourcePath, targetPath)
123-
124-
// Verify the copy succeeded by comparing file sizes
125-
const sourceSize = statSync(sourcePath).size
126-
const targetSize = statSync(targetPath).size
127-
128-
if (sourceSize !== targetSize) {
129-
throw new Error(
130-
`File size mismatch: source=${sourceSize} bytes, target=${targetSize} bytes`,
131-
)
132-
}
133-
134-
// Validate content structure
135-
const validation = validateAgentContent(targetPath)
136-
if (!validation.valid) {
137-
throw new Error(`Invalid agent file content: ${validation.error}`)
130+
if (DRY_RUN) {
131+
// In dry-run mode, validate source file but don't copy
132+
const validation = validateAgentContent(sourcePath)
133+
if (!validation.valid) {
134+
throw new Error(`Invalid agent file content: ${validation.error}`)
135+
}
136+
successes.push(file)
137+
console.log(`${prefix}Would install: ${file} -> ${targetPath}`)
138+
} else {
139+
copyFileSync(sourcePath, targetPath)
140+
141+
// Verify the copy succeeded by comparing file sizes
142+
const sourceSize = statSync(sourcePath).size
143+
const targetSize = statSync(targetPath).size
144+
145+
if (sourceSize !== targetSize) {
146+
throw new Error(
147+
`File size mismatch: source=${sourceSize} bytes, target=${targetSize} bytes`,
148+
)
149+
}
150+
151+
// Validate content structure
152+
const validation = validateAgentContent(targetPath)
153+
if (!validation.valid) {
154+
throw new Error(`Invalid agent file content: ${validation.error}`)
155+
}
156+
157+
successes.push(file)
158+
console.log(` Installed: ${file}`)
138159
}
139-
140-
successes.push(file)
141-
console.log(` Installed: ${file}`)
142160
} catch (err) {
143161
const error = err instanceof Error ? err : new Error(String(err))
144162
const message = getErrorMessage(error, file, targetPath)
145163
failures.push({ file, message })
146-
console.error(` Failed: ${file} - ${message}`)
164+
console.error(`${prefix} Failed: ${file} - ${message}`)
147165
}
148166
}
149167

150168
// Print summary
151169
console.log("")
152170
if (successes.length > 0 && failures.length === 0) {
153-
console.log(`opencode-plugin-opencoder: Successfully installed ${successes.length} agent(s)`)
154-
console.log(` Location: ${AGENTS_TARGET_DIR}`)
155-
console.log("\nTo use the autonomous development loop, run:")
156-
console.log(" opencode @opencoder")
171+
console.log(
172+
`${prefix}opencode-plugin-opencoder: Successfully installed ${successes.length} agent(s)`,
173+
)
174+
console.log(`${prefix} Location: ${AGENTS_TARGET_DIR}`)
175+
if (!DRY_RUN) {
176+
console.log("\nTo use the autonomous development loop, run:")
177+
console.log(" opencode @opencoder")
178+
}
157179
} else if (successes.length > 0 && failures.length > 0) {
158180
console.log(
159-
`opencode-plugin-opencoder: Installed ${successes.length} of ${files.length} agent(s)`,
181+
`${prefix}opencode-plugin-opencoder: Installed ${successes.length} of ${files.length} agent(s)`,
160182
)
161-
console.log(` Location: ${AGENTS_TARGET_DIR}`)
162-
console.error(`\n ${failures.length} file(s) failed to install:`)
183+
console.log(`${prefix} Location: ${AGENTS_TARGET_DIR}`)
184+
console.error(`\n${prefix} ${failures.length} file(s) failed to install:`)
163185
for (const { file, message } of failures) {
164-
console.error(` - ${file}: ${message}`)
186+
console.error(`${prefix} - ${file}: ${message}`)
165187
}
166188
} else {
167-
console.error("opencode-plugin-opencoder: Failed to install any agents")
189+
console.error(`${prefix}opencode-plugin-opencoder: Failed to install any agents`)
168190
for (const { file, message } of failures) {
169-
console.error(` - ${file}: ${message}`)
191+
console.error(`${prefix} - ${file}: ${message}`)
170192
}
171193
process.exit(1)
172194
}

preuninstall.mjs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import {
2020
const packageRoot = getPackageRoot(import.meta.url)
2121
const AGENTS_SOURCE_DIR = getAgentsSourceDir(packageRoot)
2222

23+
/** Check for --dry-run flag in command line arguments */
24+
const DRY_RUN = process.argv.includes("--dry-run")
25+
2326
/**
2427
* Main entry point for the preuninstall script.
2528
*
@@ -39,24 +42,25 @@ const AGENTS_SOURCE_DIR = getAgentsSourceDir(packageRoot)
3942
* some removals failed. This ensures npm uninstall completes.
4043
*/
4144
function main() {
42-
console.log("opencode-plugin-opencoder: Removing agents...")
45+
const prefix = DRY_RUN ? "[DRY-RUN] " : ""
46+
console.log(`${prefix}opencode-plugin-opencoder: Removing agents...`)
4347

4448
// Check if target directory exists
4549
if (!existsSync(AGENTS_TARGET_DIR)) {
46-
console.log(" No agents directory found, nothing to remove")
50+
console.log(`${prefix} No agents directory found, nothing to remove`)
4751
return
4852
}
4953

5054
// Get list of agents we installed (from source directory)
5155
if (!existsSync(AGENTS_SOURCE_DIR)) {
52-
console.log(" Source agents directory not found, skipping cleanup")
56+
console.log(`${prefix} Source agents directory not found, skipping cleanup`)
5357
return
5458
}
5559

5660
const agentFiles = readdirSync(AGENTS_SOURCE_DIR).filter((f) => f.endsWith(".md"))
5761

5862
if (agentFiles.length === 0) {
59-
console.log(" No agent files to remove")
63+
console.log(`${prefix} No agent files to remove`)
6064
return
6165
}
6266

@@ -67,21 +71,26 @@ function main() {
6771

6872
if (existsSync(targetPath)) {
6973
try {
70-
unlinkSync(targetPath)
71-
console.log(` Removed: ${file}`)
72-
removedCount++
74+
if (DRY_RUN) {
75+
console.log(`${prefix}Would remove: ${targetPath}`)
76+
removedCount++
77+
} else {
78+
unlinkSync(targetPath)
79+
console.log(` Removed: ${file}`)
80+
removedCount++
81+
}
7382
} catch (err) {
7483
const error = err instanceof Error ? err : new Error(String(err))
7584
const message = getErrorMessage(error, file, targetPath)
76-
console.error(` Warning: Could not remove ${file}: ${message}`)
85+
console.error(`${prefix} Warning: Could not remove ${file}: ${message}`)
7786
}
7887
}
7988
}
8089

8190
if (removedCount > 0) {
82-
console.log(`\nopencode-plugin-opencoder: Removed ${removedCount} agent(s)`)
91+
console.log(`\n${prefix}opencode-plugin-opencoder: Removed ${removedCount} agent(s)`)
8392
} else {
84-
console.log("\nopencode-plugin-opencoder: No agents were installed, nothing removed")
93+
console.log(`\n${prefix}opencode-plugin-opencoder: No agents were installed, nothing removed`)
8594
}
8695
}
8796

src/index.ts

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,7 @@
1414
* The agents are installed to ~/.config/opencode/agents/ via the postinstall script.
1515
*/
1616

17-
import pkg from "../package.json"
18-
19-
/**
20-
* The plugin package name.
21-
* @example "opencode-plugin-opencoder"
22-
*/
23-
export const name = pkg.name
24-
25-
/**
26-
* The current plugin version following semver.
27-
* @example "0.1.0"
28-
*/
29-
export const version = pkg.version
30-
31-
/**
32-
* A brief description of the plugin's purpose.
33-
*/
34-
export const description = pkg.description
35-
36-
/**
37-
* List of agent identifiers installed by this plugin.
38-
* These agents are copied to ~/.config/opencode/agents/ on install.
39-
*
40-
* - `opencoder`: Main orchestrator running the Plan-Build-Commit loop
41-
* - `opencoder-planner`: Analyzes codebases and creates development plans
42-
* - `opencoder-builder`: Executes individual tasks with precision
43-
*/
44-
export const agents = ["opencoder", "opencoder-planner", "opencoder-builder"]
17+
// Metadata exports (backwards compatibility)
18+
export { agents, description, name, version } from "./metadata"
19+
// Main plugin export (OpenCode plugin API)
20+
export { OpenCoderPlugin, OpenCoderPlugin as default } from "./plugin"

src/metadata.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Plugin metadata exports for opencode-plugin-opencoder
3+
*
4+
* These exports provide information about the plugin for introspection
5+
* and backwards compatibility with code that imports metadata directly.
6+
*/
7+
8+
import pkg from "../package.json"
9+
10+
/**
11+
* The plugin package name.
12+
* @example "opencode-plugin-opencoder"
13+
*/
14+
export const name = pkg.name
15+
16+
/**
17+
* The current plugin version following semver.
18+
* @example "0.1.0"
19+
*/
20+
export const version = pkg.version
21+
22+
/**
23+
* A brief description of the plugin's purpose.
24+
*/
25+
export const description = pkg.description
26+
27+
/**
28+
* List of agent identifiers installed by this plugin.
29+
* These agents are copied to ~/.config/opencode/agents/ on install.
30+
*
31+
* - `opencoder`: Main orchestrator running the Plan-Build-Commit loop
32+
* - `opencoder-planner`: Analyzes codebases and creates development plans
33+
* - `opencoder-builder`: Executes individual tasks with precision
34+
*/
35+
export const agents = ["opencoder", "opencoder-planner", "opencoder-builder"]

src/plugin.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* OpenCoder Plugin - Main plugin function
3+
*
4+
* This module exports the plugin function that follows the OpenCode plugin API.
5+
* The plugin provides autonomous development agents for continuous codebase improvement.
6+
*
7+
* Note: Agents are installed via postinstall script since the plugin API
8+
* does not currently support dynamic agent registration.
9+
*/
10+
11+
import type { Plugin } from "@opencode-ai/plugin"
12+
13+
/**
14+
* The OpenCoder plugin function.
15+
*
16+
* This plugin provides autonomous development agents:
17+
* - opencoder: Main orchestrator that runs the continuous Plan-Build-Commit loop
18+
* - opencoder-planner: Subagent that analyzes codebases and creates development plans
19+
* - opencoder-builder: Subagent that executes tasks with precision
20+
*
21+
* Usage:
22+
* opencode @opencoder
23+
*
24+
* @param ctx - Plugin context provided by OpenCode
25+
* @returns Hooks object for event subscriptions (minimal for now)
26+
*/
27+
export const OpenCoderPlugin: Plugin = async (_ctx) => {
28+
// Return minimal hooks object
29+
// Can be extended with event handlers in the future
30+
return {}
31+
}

tests/index.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,18 @@ import { describe, expect, it } from "bun:test"
22
import { readdirSync } from "node:fs"
33
import { join } from "node:path"
44
import pkg from "../package.json"
5-
import { agents, description, name, version } from "../src/index.ts"
5+
import DefaultExport, { agents, description, name, OpenCoderPlugin, version } from "../src/index.ts"
66

77
describe("index.ts exports", () => {
8+
it("should export the plugin function as named export", () => {
9+
expect(OpenCoderPlugin).toBeInstanceOf(Function)
10+
})
11+
12+
it("should export the plugin function as default export", () => {
13+
expect(DefaultExport).toBeInstanceOf(Function)
14+
expect(DefaultExport).toBe(OpenCoderPlugin)
15+
})
16+
817
it("should export the plugin name from package.json", () => {
918
expect(name).toBe("opencode-plugin-opencoder")
1019
expect(name).toBe(pkg.name)

0 commit comments

Comments
 (0)