pluidr 0.4.1 → 0.6.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 +294 -73
- package/package.json +7 -2
- package/src/cli/commands/doctor.js +99 -0
- package/src/cli/commands/init.js +14 -7
- package/src/cli/commands/uninstall.js +67 -0
- package/src/cli/commands/update.js +22 -0
- package/src/cli/index.js +31 -1
- package/src/cli/wizard/selectModelTier.js +22 -31
- package/src/core/agentPromptWriter.js +2 -2
- package/src/core/agentPromptWriter.test.js +56 -0
- package/src/core/backup.js +35 -5
- package/src/core/backup.test.js +51 -0
- package/src/core/configBuilder.js +13 -0
- package/src/core/configBuilder.test.js +47 -0
- package/src/core/configWriter.js +7 -4
- package/src/core/configWriter.test.js +26 -0
- package/src/core/identityHeader.test.js +15 -0
- package/src/core/paths.js +4 -15
- package/src/core/paths.test.js +21 -0
- package/src/core/pluginWriter.js +12 -8
- package/src/core/pluginWriter.test.js +41 -0
- package/src/core/squeezeInstaller.js +141 -0
- package/src/core/squeezeInstaller.test.js +77 -0
- package/src/plugins/README.md +29 -15
- package/src/plugins/{parent-session.js → pluidr-flow.js} +1 -6
- package/src/plugins/pluidr-squeeze.js +56 -0
- package/src/templates/agent-prompts/auditor.txt +20 -0
- package/src/templates/agent-prompts/coder.txt +33 -7
- package/src/templates/agent-prompts/{writer.txt → compose-reporter.txt} +7 -7
- package/src/templates/agent-prompts/composer.txt +428 -0
- package/src/templates/agent-prompts/{reporter.txt → debug-reporter.txt} +3 -7
- package/src/templates/agent-prompts/debugger.txt +64 -18
- package/src/templates/agent-prompts/fixer.txt +7 -0
- package/src/templates/agent-prompts/hierarchy.txt +29 -15
- package/src/templates/agent-prompts/patcher.txt +20 -0
- package/src/templates/agent-prompts/plan-checker.txt +5 -5
- package/src/templates/agent-prompts/plan-writer.txt +3 -3
- package/src/templates/agent-prompts/probe-reporter.txt +62 -0
- package/src/templates/agent-prompts/prober.txt +90 -0
- package/src/templates/agent-prompts/researcher.txt +3 -3
- package/src/templates/agent-prompts/reviewer.txt +16 -4
- package/src/templates/agent-prompts/tester.txt +10 -1
- package/src/templates/agent-prompts/tracer.txt +33 -0
- package/src/templates/model-defaults.json +45 -2
- package/src/templates/opencode.config.json +227 -73
- package/src/templates/rtk-checksums.json +7 -0
- package/src/templates/agent-prompts/builder.txt +0 -107
- package/src/templates/agent-prompts/explorer.txt +0 -53
- package/src/templates/agent-prompts/planner.txt +0 -126
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync, copyFileSync, rmSync, readdirSync } from "node:fs"
|
|
2
|
+
import { join, dirname } from "node:path"
|
|
3
|
+
import { getConfigDir, getConfigPath } from "../../core/paths.js"
|
|
4
|
+
|
|
5
|
+
function findLatestBackup() {
|
|
6
|
+
const dir = getConfigDir()
|
|
7
|
+
let backups = []
|
|
8
|
+
try {
|
|
9
|
+
backups = readdirSync(dir)
|
|
10
|
+
.filter((f) => f.startsWith("opencode.jsonc.bak."))
|
|
11
|
+
.sort()
|
|
12
|
+
.reverse()
|
|
13
|
+
} catch {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
return backups.length > 0 ? join(dir, backups[0]) : null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function runUninstall() {
|
|
20
|
+
const configDir = getConfigDir()
|
|
21
|
+
const configPath = getConfigPath()
|
|
22
|
+
const promptsDir = join(configDir, "prompts")
|
|
23
|
+
const pluginsDir = join(configDir, "plugins")
|
|
24
|
+
const binDir = join(configDir, "bin")
|
|
25
|
+
|
|
26
|
+
const summary = { restored: null, removed: [] }
|
|
27
|
+
|
|
28
|
+
// Restore latest backup
|
|
29
|
+
const latestBackup = findLatestBackup()
|
|
30
|
+
if (latestBackup && existsSync(configPath)) {
|
|
31
|
+
copyFileSync(latestBackup, configPath)
|
|
32
|
+
summary.restored = latestBackup
|
|
33
|
+
} else if (latestBackup) {
|
|
34
|
+
// Config doesn't exist but backup does — restore anyway
|
|
35
|
+
copyFileSync(latestBackup, configPath)
|
|
36
|
+
summary.restored = latestBackup
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Remove Pluidr-installed artifacts
|
|
40
|
+
if (existsSync(promptsDir)) {
|
|
41
|
+
rmSync(promptsDir, { recursive: true, force: true })
|
|
42
|
+
summary.removed.push("prompts/")
|
|
43
|
+
}
|
|
44
|
+
if (existsSync(pluginsDir)) {
|
|
45
|
+
rmSync(pluginsDir, { recursive: true, force: true })
|
|
46
|
+
summary.removed.push("plugins/")
|
|
47
|
+
}
|
|
48
|
+
if (existsSync(binDir)) {
|
|
49
|
+
rmSync(binDir, { recursive: true, force: true })
|
|
50
|
+
summary.removed.push("bin/")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Print summary
|
|
54
|
+
console.log("Uninstall complete:")
|
|
55
|
+
if (summary.restored) {
|
|
56
|
+
console.log(` ✓ Restored ${summary.restored}`)
|
|
57
|
+
} else {
|
|
58
|
+
console.log(" - No backup found to restore")
|
|
59
|
+
}
|
|
60
|
+
if (summary.removed.length > 0) {
|
|
61
|
+
console.log(` ✓ Removed: ${summary.removed.join(", ")}`)
|
|
62
|
+
} else {
|
|
63
|
+
console.log(" - No Pluidr artifacts found to remove")
|
|
64
|
+
}
|
|
65
|
+
console.log(" - Kept opencode.jsonc (preserves user customizations)")
|
|
66
|
+
console.log(" - Kept package.json (may be used by other plugins)")
|
|
67
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { confirm, isCancel } from "@clack/prompts"
|
|
3
|
+
import { getConfigPath } from "../../core/paths.js"
|
|
4
|
+
import { runInit } from "./init.js"
|
|
5
|
+
|
|
6
|
+
export async function runUpdate() {
|
|
7
|
+
const configPath = getConfigPath()
|
|
8
|
+
|
|
9
|
+
if (existsSync(configPath)) {
|
|
10
|
+
const answer = await confirm({
|
|
11
|
+
message: "Existing Pluidr config found. Overwrite? (Y/n)",
|
|
12
|
+
initialValue: true,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
if (isCancel(answer) || !answer) {
|
|
16
|
+
console.log("Update cancelled.")
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
await runInit()
|
|
22
|
+
}
|
package/src/cli/index.js
CHANGED
|
@@ -2,13 +2,31 @@ import { readFileSync } from "node:fs"
|
|
|
2
2
|
import { resolve, dirname } from "node:path"
|
|
3
3
|
import { fileURLToPath } from "node:url"
|
|
4
4
|
import { runInit } from "./commands/init.js"
|
|
5
|
+
import { runUninstall } from "./commands/uninstall.js"
|
|
6
|
+
import { runUpdate } from "./commands/update.js"
|
|
7
|
+
import { runDoctor } from "./commands/doctor.js"
|
|
5
8
|
|
|
6
9
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
10
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, "../../package.json"), "utf-8"))
|
|
8
11
|
|
|
12
|
+
const HELP = `Usage: pluidr <command>
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
init Set up OpenCode with Pluidr's 12-agent pipeline
|
|
16
|
+
uninstall Remove Pluidr artifacts and restore previous config
|
|
17
|
+
update Re-run setup (overwrites current config)
|
|
18
|
+
doctor Verify installation health
|
|
19
|
+
--version, -v Print version number
|
|
20
|
+
--help, -h Show this help message`
|
|
21
|
+
|
|
9
22
|
export function run(argv) {
|
|
10
23
|
const cmd = argv[2]
|
|
11
24
|
|
|
25
|
+
if (cmd === "--help" || cmd === "-h") {
|
|
26
|
+
console.log(HELP)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
if (cmd === "--version" || cmd === "-v") {
|
|
13
31
|
console.log(pkg.version)
|
|
14
32
|
return
|
|
@@ -18,6 +36,18 @@ export function run(argv) {
|
|
|
18
36
|
return runInit()
|
|
19
37
|
}
|
|
20
38
|
|
|
21
|
-
|
|
39
|
+
if (cmd === "uninstall") {
|
|
40
|
+
return runUninstall()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (cmd === "update") {
|
|
44
|
+
return runUpdate()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (cmd === "doctor") {
|
|
48
|
+
return runDoctor()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(HELP)
|
|
22
52
|
process.exit(1)
|
|
23
53
|
}
|
|
@@ -7,42 +7,33 @@ const TIER_LABELS = {
|
|
|
7
7
|
|
|
8
8
|
export async function selectModelTier(modelDefaults) {
|
|
9
9
|
const choices = {}
|
|
10
|
-
|
|
11
10
|
for (const [tierKey, tierInfo] of Object.entries(modelDefaults)) {
|
|
12
11
|
const defaultModel = `${tierInfo.provider}/${tierInfo.model}`
|
|
13
12
|
const label = TIER_LABELS[tierKey] ?? tierKey
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
} else {
|
|
38
|
-
selectedModel = answer
|
|
39
|
-
}
|
|
40
|
-
} catch {
|
|
41
|
-
selectedModel = defaultModel
|
|
14
|
+
const recommendedOptions = (tierInfo.recommended || []).map((r) => ({
|
|
15
|
+
value: r.value,
|
|
16
|
+
label: r.label,
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
const options = [
|
|
20
|
+
...recommendedOptions,
|
|
21
|
+
{ value: "__custom__", label: "custom (enter provider/model)" },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const answer = await select({
|
|
25
|
+
message: label,
|
|
26
|
+
options,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
if (isCancel(answer)) {
|
|
30
|
+
choices[tierKey] = defaultModel
|
|
31
|
+
} else if (answer === "__custom__") {
|
|
32
|
+
const customModel = await text({ message: "Enter model:" })
|
|
33
|
+
choices[tierKey] = isCancel(customModel) ? defaultModel : (customModel || defaultModel)
|
|
34
|
+
} else {
|
|
35
|
+
choices[tierKey] = answer
|
|
42
36
|
}
|
|
43
|
-
|
|
44
|
-
choices[tierKey] = selectedModel
|
|
45
37
|
}
|
|
46
|
-
|
|
47
38
|
return choices
|
|
48
39
|
}
|
|
@@ -8,9 +8,9 @@ import IDENTITY_HEADER from "./identityHeader.js"
|
|
|
8
8
|
// copied verbatim — no agent-identity header.
|
|
9
9
|
const REFERENCE_FILES = new Set(["hierarchy"])
|
|
10
10
|
|
|
11
|
-
export function writeAgentPrompts(templatesDir) {
|
|
11
|
+
export function writeAgentPrompts(templatesDir, destDir) {
|
|
12
12
|
const sourceDir = resolve(templatesDir, "agent-prompts")
|
|
13
|
-
|
|
13
|
+
if (!destDir) destDir = getPromptsDir()
|
|
14
14
|
|
|
15
15
|
mkdirSync(destDir, { recursive: true })
|
|
16
16
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, after } from "node:test"
|
|
2
|
+
import assert from "node:assert"
|
|
3
|
+
import { existsSync, mkdtempSync, writeFileSync, readFileSync, rmSync, mkdirSync } from "node:fs"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { tmpdir } from "node:os"
|
|
6
|
+
import { writeAgentPrompts } from "./agentPromptWriter.js"
|
|
7
|
+
|
|
8
|
+
describe("agentPromptWriter", () => {
|
|
9
|
+
const destDir = mkdtempSync(join(tmpdir(), "pluidr-apw-dest-"))
|
|
10
|
+
|
|
11
|
+
after(() => {
|
|
12
|
+
// Clean up any files written during tests
|
|
13
|
+
if (existsSync(destDir)) rmSync(destDir, { recursive: true })
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it("prepends identity header to agent prompts", () => {
|
|
17
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "apw-source-"))
|
|
18
|
+
const agentPromptsDir = join(tmpDir, "agent-prompts")
|
|
19
|
+
mkdirSync(agentPromptsDir, { recursive: true })
|
|
20
|
+
|
|
21
|
+
writeFileSync(join(agentPromptsDir, "coder.txt"), "test instructions", "utf-8")
|
|
22
|
+
writeFileSync(join(agentPromptsDir, "compose-reporter.txt"), "write docs", "utf-8")
|
|
23
|
+
|
|
24
|
+
writeAgentPrompts(tmpDir, destDir)
|
|
25
|
+
|
|
26
|
+
const coderContent = readFileSync(join(destDir, "coder.txt"), "utf-8")
|
|
27
|
+
const composeReporterContent = readFileSync(join(destDir, "compose-reporter.txt"), "utf-8")
|
|
28
|
+
|
|
29
|
+
assert.ok(coderContent.startsWith('You are the "coder" agent.\n\n'),
|
|
30
|
+
"coder should have identity header")
|
|
31
|
+
assert.ok(composeReporterContent.startsWith('You are the "compose-reporter" agent.\n\n'),
|
|
32
|
+
"compose-reporter should have identity header")
|
|
33
|
+
assert.ok(coderContent.includes("test instructions"),
|
|
34
|
+
"original content preserved in coder")
|
|
35
|
+
assert.ok(composeReporterContent.includes("write docs"),
|
|
36
|
+
"original content preserved in compose-reporter")
|
|
37
|
+
|
|
38
|
+
rmSync(tmpDir, { recursive: true })
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("copies hierarchy.txt verbatim without identity header", () => {
|
|
42
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "apw-source2-"))
|
|
43
|
+
const agentPromptsDir = join(tmpDir, "agent-prompts")
|
|
44
|
+
mkdirSync(agentPromptsDir, { recursive: true })
|
|
45
|
+
|
|
46
|
+
writeFileSync(join(agentPromptsDir, "hierarchy.txt"), "global reference rules", "utf-8")
|
|
47
|
+
|
|
48
|
+
writeAgentPrompts(tmpDir, destDir)
|
|
49
|
+
|
|
50
|
+
const hierarchyContent = readFileSync(join(destDir, "hierarchy.txt"), "utf-8")
|
|
51
|
+
assert.strictEqual(hierarchyContent, "global reference rules",
|
|
52
|
+
"hierarchy.txt should be copied verbatim without header")
|
|
53
|
+
|
|
54
|
+
rmSync(tmpDir, { recursive: true })
|
|
55
|
+
})
|
|
56
|
+
})
|
package/src/core/backup.js
CHANGED
|
@@ -1,8 +1,38 @@
|
|
|
1
|
-
import { existsSync, copyFileSync } from "node:fs"
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, copyFileSync, readdirSync, unlinkSync } from "node:fs"
|
|
2
|
+
import { join, dirname } from "node:path"
|
|
3
|
+
import { getConfigPath } from "./paths.js"
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const MAX_BACKUPS = 5
|
|
6
|
+
|
|
7
|
+
function timestamp() {
|
|
8
|
+
// ponytail: simplified formatting using native Date serialization
|
|
9
|
+
const s = new Date().toISOString().replace(/[-:]/g, "")
|
|
10
|
+
return `${s.slice(0, 8)}_${s.slice(9, 15)}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function rotateBackups(configPath) {
|
|
14
|
+
const dir = dirname(configPath)
|
|
15
|
+
const baseName = "opencode.jsonc.bak"
|
|
16
|
+
let backups = []
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
backups = readdirSync(dir)
|
|
20
|
+
.filter((f) => f.startsWith(baseName + "."))
|
|
21
|
+
.map((f) => join(dir, f))
|
|
22
|
+
.sort()
|
|
23
|
+
} catch { /* dir doesn't exist yet */ }
|
|
24
|
+
|
|
25
|
+
while (backups.length >= MAX_BACKUPS) {
|
|
26
|
+
const oldest = backups.shift()
|
|
27
|
+
try { unlinkSync(oldest) } catch {}
|
|
7
28
|
}
|
|
8
29
|
}
|
|
30
|
+
|
|
31
|
+
export function backupExistingConfig(configPath) {
|
|
32
|
+
const resolvedPath = configPath || getConfigPath()
|
|
33
|
+
if (!existsSync(resolvedPath)) return
|
|
34
|
+
|
|
35
|
+
rotateBackups(resolvedPath)
|
|
36
|
+
const backupPath = `${resolvedPath}.bak.${timestamp()}`
|
|
37
|
+
copyFileSync(resolvedPath, backupPath)
|
|
38
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, before, after } from "node:test"
|
|
2
|
+
import assert from "node:assert"
|
|
3
|
+
import { existsSync, writeFileSync, readFileSync, rmSync, readdirSync, mkdirSync, mkdtempSync } from "node:fs"
|
|
4
|
+
import { join, dirname } from "node:path"
|
|
5
|
+
import { tmpdir } from "node:os"
|
|
6
|
+
import { backupExistingConfig } from "./backup.js"
|
|
7
|
+
|
|
8
|
+
describe("backup", () => {
|
|
9
|
+
const tempDir = mkdtempSync(join(tmpdir(), "pluidr-backup-"))
|
|
10
|
+
const configPath = join(tempDir, "opencode.jsonc")
|
|
11
|
+
const TEST_CONTENT = JSON.stringify({ test: true })
|
|
12
|
+
|
|
13
|
+
function findBackups() {
|
|
14
|
+
try {
|
|
15
|
+
return readdirSync(tempDir).filter((f) => f.startsWith("opencode.jsonc.bak."))
|
|
16
|
+
} catch { return [] }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
before(() => {
|
|
20
|
+
mkdirSync(tempDir, { recursive: true })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
after(() => {
|
|
24
|
+
if (existsSync(tempDir)) rmSync(tempDir, { recursive: true })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("skips if no config exists (no throw)", () => {
|
|
28
|
+
// Ensure no config file exists in temp dir
|
|
29
|
+
if (existsSync(configPath)) rmSync(configPath)
|
|
30
|
+
for (const bak of findBackups()) {
|
|
31
|
+
rmSync(join(tempDir, bak))
|
|
32
|
+
}
|
|
33
|
+
backupExistingConfig(configPath)
|
|
34
|
+
assert.ok(true)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("copies existing config to backup with timestamped name", () => {
|
|
38
|
+
// Write test config
|
|
39
|
+
writeFileSync(configPath, TEST_CONTENT, "utf-8")
|
|
40
|
+
|
|
41
|
+
backupExistingConfig(configPath)
|
|
42
|
+
|
|
43
|
+
const backups = findBackups()
|
|
44
|
+
assert.ok(backups.length > 0, "timestamped backup file should exist")
|
|
45
|
+
const newest = backups.sort().reverse()[0]
|
|
46
|
+
const newestPath = join(tempDir, newest)
|
|
47
|
+
assert.ok(newestPath.includes("opencode.jsonc.bak."), "backup should be timestamped")
|
|
48
|
+
const backupContent = JSON.parse(readFileSync(newestPath, "utf-8"))
|
|
49
|
+
assert.deepStrictEqual(backupContent, { test: true })
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -8,6 +8,19 @@ export function buildConfig(tierChoices, templatesDir) {
|
|
|
8
8
|
const config = JSON.parse(readFileSync(configPath, "utf-8"))
|
|
9
9
|
const modelDefaults = JSON.parse(readFileSync(defaultsPath, "utf-8"))
|
|
10
10
|
|
|
11
|
+
// Validate: every agent in model-defaults must have a key in config.agent
|
|
12
|
+
const defaultedAgents = new Set(
|
|
13
|
+
Object.values(modelDefaults).flatMap((tier) => tier.agents),
|
|
14
|
+
)
|
|
15
|
+
const missingAgents = [...defaultedAgents].filter(
|
|
16
|
+
(name) => !(name in config.agent),
|
|
17
|
+
)
|
|
18
|
+
if (missingAgents.length > 0) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`Template validation failed: agent(s) not found in opencode.config.json: ${missingAgents.join(", ")}`,
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
for (const [tierKey, tier] of Object.entries(modelDefaults)) {
|
|
12
25
|
const model = tierChoices[tierKey]
|
|
13
26
|
for (const agentName of tier.agents) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it } from "node:test"
|
|
2
|
+
import assert from "node:assert"
|
|
3
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { tmpdir } from "node:os"
|
|
6
|
+
import { buildConfig } from "./configBuilder.js"
|
|
7
|
+
|
|
8
|
+
describe("configBuilder", () => {
|
|
9
|
+
it("injects models into agent config per tier", () => {
|
|
10
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "configbuilder-test-"))
|
|
11
|
+
|
|
12
|
+
const configJson = {
|
|
13
|
+
agent: {
|
|
14
|
+
composer: {},
|
|
15
|
+
coder: {},
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
const defaultsJson = {
|
|
19
|
+
reasoningHeavy: {
|
|
20
|
+
provider: "test",
|
|
21
|
+
model: "big-model",
|
|
22
|
+
agents: ["composer"],
|
|
23
|
+
},
|
|
24
|
+
fast: {
|
|
25
|
+
provider: "test",
|
|
26
|
+
model: "small-model",
|
|
27
|
+
agents: ["coder"],
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
writeFileSync(join(tmpDir, "opencode.config.json"), JSON.stringify(configJson), "utf-8")
|
|
32
|
+
writeFileSync(join(tmpDir, "model-defaults.json"), JSON.stringify(defaultsJson), "utf-8")
|
|
33
|
+
|
|
34
|
+
const result = buildConfig(
|
|
35
|
+
{
|
|
36
|
+
reasoningHeavy: "test/big-model",
|
|
37
|
+
fast: "test/small-model",
|
|
38
|
+
},
|
|
39
|
+
tmpDir,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
assert.strictEqual(result.agent.composer.model, "test/big-model")
|
|
43
|
+
assert.strictEqual(result.agent.coder.model, "test/small-model")
|
|
44
|
+
|
|
45
|
+
rmSync(tmpDir, { recursive: true })
|
|
46
|
+
})
|
|
47
|
+
})
|
package/src/core/configWriter.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync } from "node:fs"
|
|
2
|
-
import {
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { getConfigDir } from "./paths.js"
|
|
3
4
|
|
|
4
|
-
export function writeConfig(configObject) {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
export function writeConfig(configObject, targetDir) {
|
|
6
|
+
const baseDir = targetDir || getConfigDir()
|
|
7
|
+
const configPath = join(baseDir, "opencode.jsonc")
|
|
8
|
+
mkdirSync(baseDir, { recursive: true })
|
|
9
|
+
writeFileSync(configPath, JSON.stringify(configObject, null, 2), "utf-8")
|
|
7
10
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it, after } from "node:test"
|
|
2
|
+
import assert from "node:assert"
|
|
3
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { tmpdir } from "node:os"
|
|
6
|
+
import { writeConfig } from "./configWriter.js"
|
|
7
|
+
|
|
8
|
+
describe("configWriter", () => {
|
|
9
|
+
const tempDir = mkdtempSync(join(tmpdir(), "pluidr-configwriter-"))
|
|
10
|
+
|
|
11
|
+
after(() => {
|
|
12
|
+
if (existsSync(tempDir)) rmSync(tempDir, { recursive: true })
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("writes config as formatted JSON to the correct path", () => {
|
|
16
|
+
const configObj = { agent: { composer: { model: "test/model" } } }
|
|
17
|
+
const expected = JSON.stringify(configObj, null, 2)
|
|
18
|
+
|
|
19
|
+
writeConfig(configObj, tempDir)
|
|
20
|
+
|
|
21
|
+
const configPath = join(tempDir, "opencode.jsonc")
|
|
22
|
+
assert.ok(existsSync(configPath), "config file should exist")
|
|
23
|
+
const written = readFileSync(configPath, "utf-8")
|
|
24
|
+
assert.strictEqual(written, expected)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, it } from "node:test"
|
|
2
|
+
import assert from "node:assert"
|
|
3
|
+
import IDENTITY_HEADER from "./identityHeader.js"
|
|
4
|
+
|
|
5
|
+
describe("identityHeader", () => {
|
|
6
|
+
it('returns "You are the <name> agent.\\n\\n" format', () => {
|
|
7
|
+
const result = IDENTITY_HEADER("coder")
|
|
8
|
+
assert.strictEqual(result, 'You are the "coder" agent.\n\n')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('handles multi-word agent names', () => {
|
|
12
|
+
const result = IDENTITY_HEADER("plan-writer")
|
|
13
|
+
assert.strictEqual(result, 'You are the "plan-writer" agent.\n\n')
|
|
14
|
+
})
|
|
15
|
+
})
|
package/src/core/paths.js
CHANGED
|
@@ -3,18 +3,7 @@ import { join } from "node:path"
|
|
|
3
3
|
|
|
4
4
|
const BASE = join(homedir(), ".config", "opencode")
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export function getConfigPath() {
|
|
11
|
-
return join(BASE, "opencode.jsonc")
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function getBackupPath() {
|
|
15
|
-
return join(BASE, "opencode.jsonc.bak")
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function getPromptsDir() {
|
|
19
|
-
return join(BASE, "prompts")
|
|
20
|
-
}
|
|
6
|
+
export const getConfigDir = () => BASE
|
|
7
|
+
export const getConfigPath = () => join(BASE, "opencode.jsonc")
|
|
8
|
+
export const getPromptsDir = () => join(BASE, "prompts")
|
|
9
|
+
// ponytail: getBackupPath removed (YAGNI)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, it } from "node:test"
|
|
2
|
+
import assert from "node:assert"
|
|
3
|
+
import { homedir } from "node:os"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { getConfigDir, getConfigPath, getPromptsDir } from "./paths.js"
|
|
6
|
+
|
|
7
|
+
const BASE = join(homedir(), ".config", "opencode")
|
|
8
|
+
|
|
9
|
+
describe("paths", () => {
|
|
10
|
+
it("getConfigDir returns ~/.config/opencode", () => {
|
|
11
|
+
assert.strictEqual(getConfigDir(), BASE)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it("getConfigPath returns opencode.jsonc path", () => {
|
|
15
|
+
assert.strictEqual(getConfigPath(), join(BASE, "opencode.jsonc"))
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("getPromptsDir returns prompts path", () => {
|
|
19
|
+
assert.strictEqual(getPromptsDir(), join(BASE, "prompts"))
|
|
20
|
+
})
|
|
21
|
+
})
|
package/src/core/pluginWriter.js
CHANGED
|
@@ -4,22 +4,26 @@ import { fileURLToPath } from "node:url"
|
|
|
4
4
|
import { getConfigDir } from "./paths.js"
|
|
5
5
|
|
|
6
6
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
|
-
const
|
|
7
|
+
const PLUGINS = ["pluidr-flow.js", "pluidr-squeeze.js"]
|
|
8
8
|
const PACKAGE_JSON = {
|
|
9
|
+
type: "module",
|
|
9
10
|
dependencies: { "@opencode-ai/plugin": "^1.17.9" },
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export function writePluginBundle() {
|
|
13
|
-
const
|
|
13
|
+
export function writePluginBundle(targetDir) {
|
|
14
|
+
const baseDir = targetDir || getConfigDir()
|
|
15
|
+
const pluginsDir = join(baseDir, "plugins")
|
|
14
16
|
mkdirSync(pluginsDir, { recursive: true })
|
|
15
17
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
+
for (const name of PLUGINS) {
|
|
19
|
+
const sourcePath = resolve(__dirname, "../plugins", name)
|
|
20
|
+
copyFileSync(sourcePath, join(pluginsDir, name))
|
|
21
|
+
}
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
export function writePluginPackageJson() {
|
|
21
|
-
const
|
|
22
|
-
|
|
24
|
+
export function writePluginPackageJson(targetDir) {
|
|
25
|
+
const baseDir = targetDir || getConfigDir()
|
|
26
|
+
const packageJsonPath = join(baseDir, "package.json")
|
|
23
27
|
|
|
24
28
|
writeFileSync(packageJsonPath, JSON.stringify(PACKAGE_JSON, null, 2), "utf-8")
|
|
25
29
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, after } from "node:test"
|
|
2
|
+
import assert from "node:assert"
|
|
3
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"
|
|
4
|
+
import { join, resolve, dirname } from "node:path"
|
|
5
|
+
import { tmpdir } from "node:os"
|
|
6
|
+
import { fileURLToPath } from "node:url"
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
|
|
10
|
+
describe("pluginWriter", () => {
|
|
11
|
+
const tempDir = mkdtempSync(join(tmpdir(), "pluidr-pluginwriter-"))
|
|
12
|
+
|
|
13
|
+
after(() => {
|
|
14
|
+
if (existsSync(tempDir)) rmSync(tempDir, { recursive: true })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it("copies plugin files and writes package.json", async () => {
|
|
18
|
+
const { writePluginBundle, writePluginPackageJson } = await import("./pluginWriter.js")
|
|
19
|
+
|
|
20
|
+
// Use temp dir as target — sandboxed, no real ~/.config/opencode writes
|
|
21
|
+
writePluginBundle(tempDir)
|
|
22
|
+
|
|
23
|
+
const pluginsDestDir = join(tempDir, "plugins")
|
|
24
|
+
const pluginFiles = ["pluidr-flow.js", "pluidr-squeeze.js"]
|
|
25
|
+
for (const f of pluginFiles) {
|
|
26
|
+
assert.ok(existsSync(join(pluginsDestDir, f)),
|
|
27
|
+
`plugin ${f} should be copied to plugins dir`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Clean plugins dir to test package.json separately
|
|
31
|
+
rmSync(pluginsDestDir, { recursive: true })
|
|
32
|
+
|
|
33
|
+
writePluginPackageJson(tempDir)
|
|
34
|
+
|
|
35
|
+
const pkgPath = join(tempDir, "package.json")
|
|
36
|
+
assert.ok(existsSync(pkgPath), "package.json should exist in temp dir")
|
|
37
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"))
|
|
38
|
+
assert.strictEqual(pkg.type, "module")
|
|
39
|
+
assert.ok(pkg.dependencies["@opencode-ai/plugin"])
|
|
40
|
+
})
|
|
41
|
+
})
|