pluidr 0.6.1 → 0.7.1

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.
Files changed (48) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +365 -357
  3. package/bin/pluidr.js +3 -3
  4. package/package.json +45 -45
  5. package/src/cli/commands/doctor.js +101 -99
  6. package/src/cli/commands/init.js +43 -43
  7. package/src/cli/commands/launch.js +46 -0
  8. package/src/cli/commands/uninstall.js +63 -67
  9. package/src/cli/commands/update.js +6 -22
  10. package/src/cli/index.js +57 -53
  11. package/src/cli/wizard/selectModelTier.js +40 -39
  12. package/src/core/agentPromptWriter.js +32 -32
  13. package/src/core/agentPromptWriter.test.js +56 -56
  14. package/src/core/backup.js +40 -38
  15. package/src/core/configBuilder.js +32 -32
  16. package/src/core/configBuilder.test.js +93 -47
  17. package/src/core/configWriter.js +10 -10
  18. package/src/core/identityHeader.js +8 -8
  19. package/src/core/paths.js +9 -9
  20. package/src/core/paths.test.js +21 -21
  21. package/src/core/pluginWriter.js +29 -29
  22. package/src/core/squeezeInstaller.js +161 -161
  23. package/src/core/squeezeInstaller.test.js +75 -75
  24. package/src/core/version.js +8 -0
  25. package/src/core/versionCheck.js +44 -0
  26. package/src/core/versionCheck.test.js +34 -0
  27. package/src/plugins/README.md +68 -68
  28. package/src/plugins/pluidr-squeeze.js +77 -77
  29. package/src/templates/agent-prompts/auditor.txt +20 -20
  30. package/src/templates/agent-prompts/coder.txt +87 -87
  31. package/src/templates/agent-prompts/compose-reporter.txt +55 -55
  32. package/src/templates/agent-prompts/composer.txt +430 -430
  33. package/src/templates/agent-prompts/debug-reporter.txt +65 -65
  34. package/src/templates/agent-prompts/debugger.txt +151 -151
  35. package/src/templates/agent-prompts/fixer.txt +66 -66
  36. package/src/templates/agent-prompts/hierarchy.txt +96 -96
  37. package/src/templates/agent-prompts/inspector.txt +79 -79
  38. package/src/templates/agent-prompts/patcher.txt +20 -20
  39. package/src/templates/agent-prompts/plan-checker.txt +45 -45
  40. package/src/templates/agent-prompts/plan-writer.txt +57 -57
  41. package/src/templates/agent-prompts/probe-reporter.txt +62 -62
  42. package/src/templates/agent-prompts/prober.txt +90 -90
  43. package/src/templates/agent-prompts/researcher.txt +48 -48
  44. package/src/templates/agent-prompts/reviewer.txt +57 -57
  45. package/src/templates/agent-prompts/tester.txt +66 -66
  46. package/src/templates/agent-prompts/tracer.txt +33 -33
  47. package/src/templates/model-defaults.json +73 -55
  48. package/src/templates/opencode.config.json +481 -481
package/src/cli/index.js CHANGED
@@ -1,53 +1,57 @@
1
- import { readFileSync } from "node:fs"
2
- import { resolve, dirname } from "node:path"
3
- import { fileURLToPath } from "node:url"
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"
8
-
9
- const __dirname = dirname(fileURLToPath(import.meta.url))
10
- const pkg = JSON.parse(readFileSync(resolve(__dirname, "../../package.json"), "utf-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
-
22
- export function run(argv) {
23
- const cmd = argv[2]
24
-
25
- if (cmd === "--help" || cmd === "-h") {
26
- console.log(HELP)
27
- return
28
- }
29
-
30
- if (cmd === "--version" || cmd === "-v") {
31
- console.log(pkg.version)
32
- return
33
- }
34
-
35
- if (cmd === "init") {
36
- return runInit()
37
- }
38
-
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)
52
- process.exit(1)
53
- }
1
+ import { readFileSync } from "node:fs"
2
+ import { resolve, dirname } from "node:path"
3
+ import { fileURLToPath } from "node:url"
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"
8
+ import { runLaunch } from "./commands/launch.js"
9
+ import { version } from "../core/version.js"
10
+
11
+ const HELP = `Usage: pluidr [command]
12
+
13
+ pluidr Launch OpenCode powered by Pluidr agents
14
+ pluidr init Install and configure the Pluidr agent pipeline
15
+ pluidr update Check and update the Pluidr system
16
+ pluidr doctor Diagnose the integrity of the current installation
17
+ pluidr uninstall Remove Pluidr artifacts and restore the previous config
18
+ --version, -v Print the current version number
19
+ --help, -h Show this help message`
20
+
21
+ export function run(argv) {
22
+ const cmd = argv[2]
23
+
24
+ if (!cmd) {
25
+ return runLaunch()
26
+ }
27
+
28
+ if (cmd === "--help" || cmd === "-h") {
29
+ console.log(HELP)
30
+ return
31
+ }
32
+
33
+ if (cmd === "--version" || cmd === "-v") {
34
+ console.log(version)
35
+ return
36
+ }
37
+
38
+ if (cmd === "init") {
39
+ return runInit()
40
+ }
41
+
42
+ if (cmd === "uninstall") {
43
+ return runUninstall()
44
+ }
45
+
46
+ if (cmd === "update") {
47
+ return runUpdate()
48
+ }
49
+
50
+ if (cmd === "doctor") {
51
+ return runDoctor()
52
+ }
53
+
54
+ console.error(`Unknown command: ${cmd}\n`)
55
+ console.log(HELP)
56
+ process.exit(1)
57
+ }
@@ -1,39 +1,40 @@
1
- import { select, text, isCancel } from "@clack/prompts"
2
-
3
- const TIER_LABELS = {
4
- reasoningHeavy: "Model for reasoning task",
5
- fast: "Model for fast/cheap task",
6
- }
7
-
8
- export async function selectModelTier(modelDefaults) {
9
- const choices = {}
10
- for (const [tierKey, tierInfo] of Object.entries(modelDefaults)) {
11
- const defaultModel = `${tierInfo.provider}/${tierInfo.model}`
12
- const label = TIER_LABELS[tierKey] ?? tierKey
13
-
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
36
- }
37
- }
38
- return choices
39
- }
1
+ import { select, text, isCancel } from "@clack/prompts"
2
+
3
+ const TIER_LABELS = {
4
+ reasoning: "Model for reasoning task (orchestrators + deep analysis)",
5
+ precision: "Model for precision task (quality gates + verification)",
6
+ fast: "Model for fast/cheap task (builders + formatters)",
7
+ }
8
+
9
+ export async function selectModelTier(modelDefaults) {
10
+ const choices = {}
11
+ for (const [tierKey, tierInfo] of Object.entries(modelDefaults)) {
12
+ const defaultModel = `${tierInfo.provider}/${tierInfo.model}`
13
+ const label = TIER_LABELS[tierKey] ?? tierKey
14
+
15
+ const recommendedOptions = (tierInfo.recommended || []).map((r) => ({
16
+ value: r.value,
17
+ label: r.label,
18
+ }))
19
+
20
+ const options = [
21
+ ...recommendedOptions,
22
+ { value: "__custom__", label: "custom (enter provider/model)" },
23
+ ]
24
+
25
+ const answer = await select({
26
+ message: label,
27
+ options,
28
+ })
29
+
30
+ if (isCancel(answer)) {
31
+ choices[tierKey] = defaultModel
32
+ } else if (answer === "__custom__") {
33
+ const customModel = await text({ message: "Enter model:" })
34
+ choices[tierKey] = isCancel(customModel) ? defaultModel : (customModel || defaultModel)
35
+ } else {
36
+ choices[tierKey] = answer
37
+ }
38
+ }
39
+ return choices
40
+ }
@@ -1,32 +1,32 @@
1
- import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"
2
- import { basename, extname, resolve } from "node:path"
3
- import { getPromptsDir } from "./paths.js"
4
- import IDENTITY_HEADER from "./identityHeader.js"
5
-
6
- // hierarchy.txt is a global reference document (referenced from
7
- // opencode.config.json's `instructions`), not an agent prompt. It must be
8
- // copied verbatim — no agent-identity header.
9
- const REFERENCE_FILES = new Set(["hierarchy"])
10
-
11
- export function writeAgentPrompts(templatesDir, destDir) {
12
- const sourceDir = resolve(templatesDir, "agent-prompts")
13
- if (!destDir) destDir = getPromptsDir()
14
-
15
- mkdirSync(destDir, { recursive: true })
16
-
17
- for (const file of readdirSync(sourceDir)) {
18
- if (extname(file) !== ".txt") continue
19
-
20
- const sourcePath = resolve(sourceDir, file)
21
- const destPath = resolve(destDir, file)
22
- const content = readFileSync(sourcePath, "utf-8")
23
-
24
- const agentName = basename(file, ".txt")
25
- if (REFERENCE_FILES.has(agentName)) {
26
- writeFileSync(destPath, content, "utf-8")
27
- continue
28
- }
29
-
30
- writeFileSync(destPath, IDENTITY_HEADER(agentName) + content, "utf-8")
31
- }
32
- }
1
+ import { mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"
2
+ import { basename, extname, resolve } from "node:path"
3
+ import { getPromptsDir } from "./paths.js"
4
+ import IDENTITY_HEADER from "./identityHeader.js"
5
+
6
+ // hierarchy.txt is a global reference document (referenced from
7
+ // opencode.config.json's `instructions`), not an agent prompt. It must be
8
+ // copied verbatim — no agent-identity header.
9
+ const REFERENCE_FILES = new Set(["hierarchy"])
10
+
11
+ export function writeAgentPrompts(templatesDir, destDir) {
12
+ const sourceDir = resolve(templatesDir, "agent-prompts")
13
+ if (!destDir) destDir = getPromptsDir()
14
+
15
+ mkdirSync(destDir, { recursive: true })
16
+
17
+ for (const file of readdirSync(sourceDir)) {
18
+ if (extname(file) !== ".txt") continue
19
+
20
+ const sourcePath = resolve(sourceDir, file)
21
+ const destPath = resolve(destDir, file)
22
+ const content = readFileSync(sourcePath, "utf-8")
23
+
24
+ const agentName = basename(file, ".txt")
25
+ if (REFERENCE_FILES.has(agentName)) {
26
+ writeFileSync(destPath, content, "utf-8")
27
+ continue
28
+ }
29
+
30
+ writeFileSync(destPath, IDENTITY_HEADER(agentName) + content, "utf-8")
31
+ }
32
+ }
@@ -1,56 +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
- })
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
+ })
@@ -1,38 +1,40 @@
1
- import { existsSync, copyFileSync, readdirSync, unlinkSync } from "node:fs"
2
- import { join, dirname } from "node:path"
3
- import { getConfigPath } from "./paths.js"
4
-
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 {}
28
- }
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
- }
1
+ import { existsSync, copyFileSync, readdirSync, unlinkSync } from "node:fs"
2
+ import { join, dirname } from "node:path"
3
+ import { getConfigPath } from "./paths.js"
4
+
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 (err) {
24
+ if (err.code !== "ENOENT") throw err
25
+ }
26
+
27
+ while (backups.length >= MAX_BACKUPS) {
28
+ const oldest = backups.shift()
29
+ try { unlinkSync(oldest) } catch {}
30
+ }
31
+ }
32
+
33
+ export function backupExistingConfig(configPath) {
34
+ const resolvedPath = configPath || getConfigPath()
35
+ if (!existsSync(resolvedPath)) return
36
+
37
+ rotateBackups(resolvedPath)
38
+ const backupPath = `${resolvedPath}.bak.${timestamp()}`
39
+ copyFileSync(resolvedPath, backupPath)
40
+ }
@@ -1,32 +1,32 @@
1
- import { readFileSync } from "node:fs"
2
- import { resolve } from "node:path"
3
-
4
- export function buildConfig(tierChoices, templatesDir) {
5
- const configPath = resolve(templatesDir, "opencode.config.json")
6
- const defaultsPath = resolve(templatesDir, "model-defaults.json")
7
-
8
- const config = JSON.parse(readFileSync(configPath, "utf-8"))
9
- const modelDefaults = JSON.parse(readFileSync(defaultsPath, "utf-8"))
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
-
24
- for (const [tierKey, tier] of Object.entries(modelDefaults)) {
25
- const model = tierChoices[tierKey]
26
- for (const agentName of tier.agents) {
27
- config.agent[agentName].model = model
28
- }
29
- }
30
-
31
- return config
32
- }
1
+ import { readFileSync } from "node:fs"
2
+ import { resolve } from "node:path"
3
+
4
+ export function buildConfig(tierChoices, templatesDir) {
5
+ const configPath = resolve(templatesDir, "opencode.config.json")
6
+ const defaultsPath = resolve(templatesDir, "model-defaults.json")
7
+
8
+ const config = JSON.parse(readFileSync(configPath, "utf-8"))
9
+ const modelDefaults = JSON.parse(readFileSync(defaultsPath, "utf-8"))
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
+
24
+ for (const [tierKey, tier] of Object.entries(modelDefaults)) {
25
+ const model = tierChoices[tierKey]
26
+ for (const agentName of tier.agents) {
27
+ config.agent[agentName].model = model
28
+ }
29
+ }
30
+
31
+ return config
32
+ }
@@ -1,47 +1,93 @@
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
- })
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
+ reviewer: {},
16
+ coder: {},
17
+ },
18
+ }
19
+ const defaultsJson = {
20
+ reasoning: {
21
+ provider: "test",
22
+ model: "big-model",
23
+ agents: ["composer"],
24
+ },
25
+ precision: {
26
+ provider: "test",
27
+ model: "mid-model",
28
+ agents: ["reviewer"],
29
+ },
30
+ fast: {
31
+ provider: "test",
32
+ model: "small-model",
33
+ agents: ["coder"],
34
+ },
35
+ }
36
+
37
+ writeFileSync(join(tmpDir, "opencode.config.json"), JSON.stringify(configJson), "utf-8")
38
+ writeFileSync(join(tmpDir, "model-defaults.json"), JSON.stringify(defaultsJson), "utf-8")
39
+
40
+ const result = buildConfig(
41
+ {
42
+ reasoning: "test/big-model",
43
+ precision: "test/mid-model",
44
+ fast: "test/small-model",
45
+ },
46
+ tmpDir,
47
+ )
48
+
49
+ assert.strictEqual(result.agent.composer.model, "test/big-model")
50
+ assert.strictEqual(result.agent.reviewer.model, "test/mid-model")
51
+ assert.strictEqual(result.agent.coder.model, "test/small-model")
52
+
53
+ rmSync(tmpDir, { recursive: true })
54
+ })
55
+
56
+ it("throws an error if a defaulted agent is missing from config template", () => {
57
+ const tmpDir = mkdtempSync(join(tmpdir(), "configbuilder-test-err-"))
58
+
59
+ const configJson = {
60
+ agent: {
61
+ // missing 'composer'
62
+ coder: {},
63
+ },
64
+ }
65
+ const defaultsJson = {
66
+ reasoning: {
67
+ provider: "test",
68
+ model: "big-model",
69
+ agents: ["composer"],
70
+ },
71
+ fast: {
72
+ provider: "test",
73
+ model: "small-model",
74
+ agents: ["coder"],
75
+ },
76
+ }
77
+
78
+ writeFileSync(join(tmpDir, "opencode.config.json"), JSON.stringify(configJson), "utf-8")
79
+ writeFileSync(join(tmpDir, "model-defaults.json"), JSON.stringify(defaultsJson), "utf-8")
80
+
81
+ assert.throws(() => {
82
+ buildConfig(
83
+ {
84
+ reasoning: "test/big-model",
85
+ fast: "test/small-model",
86
+ },
87
+ tmpDir,
88
+ )
89
+ }, /Template validation failed: agent\(s\) not found/)
90
+
91
+ rmSync(tmpDir, { recursive: true })
92
+ })
93
+ })