pluidr 0.7.5 → 0.8.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.
Files changed (90) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +408 -364
  3. package/bin/pluidr.js +3 -3
  4. package/package.json +1 -1
  5. package/src/cli/commands/doctor.js +137 -134
  6. package/src/cli/commands/init.js +134 -47
  7. package/src/cli/commands/launch.js +101 -50
  8. package/src/cli/commands/theme.js +67 -0
  9. package/src/cli/commands/theme.test.js +28 -0
  10. package/src/cli/commands/uninstall.js +103 -90
  11. package/src/cli/commands/update.js +9 -9
  12. package/src/cli/index.js +63 -57
  13. package/src/cli/wizard/selectModelTier.js +40 -40
  14. package/src/core/agentPromptWriter.js +45 -32
  15. package/src/core/agentPromptWriter.test.js +56 -56
  16. package/src/core/animation.js +83 -0
  17. package/src/core/animation.test.js +118 -0
  18. package/src/core/backup.js +46 -46
  19. package/src/core/commandsWriter.js +26 -0
  20. package/src/core/commandsWriter.test.js +29 -0
  21. package/src/core/configBuilder.js +32 -32
  22. package/src/core/configBuilder.test.js +93 -93
  23. package/src/core/configWriter.js +10 -10
  24. package/src/core/configWriter.test.js +1 -1
  25. package/src/core/identityHeader.js +8 -8
  26. package/src/core/paths.js +13 -11
  27. package/src/core/paths.test.js +33 -29
  28. package/src/core/pluginWriter.js +43 -29
  29. package/src/core/skillsWriter.js +21 -0
  30. package/src/core/skillsWriter.test.js +30 -0
  31. package/src/core/squeezeInstaller.js +158 -158
  32. package/src/core/squeezeInstaller.test.js +79 -79
  33. package/src/core/themeWriter.js +18 -4
  34. package/src/core/themeWriter.test.js +2 -2
  35. package/src/core/tuiConfigWriter.js +3 -2
  36. package/src/core/tuiConfigWriter.test.js +4 -4
  37. package/src/core/version.js +8 -8
  38. package/src/core/versionCheck.js +44 -44
  39. package/src/plugins/README.md +57 -68
  40. package/src/plugins/pluidr-squeeze.js +77 -77
  41. package/src/templates/agent-prompts/analyze.txt +49 -0
  42. package/src/templates/agent-prompts/auditor.txt +20 -20
  43. package/src/templates/agent-prompts/builder.txt +10 -0
  44. package/src/templates/agent-prompts/coder.txt +87 -87
  45. package/src/templates/agent-prompts/compose-reporter.txt +55 -55
  46. package/src/templates/agent-prompts/compose.txt +50 -0
  47. package/src/templates/agent-prompts/composer.txt +414 -420
  48. package/src/templates/agent-prompts/debug-reporter.txt +65 -65
  49. package/src/templates/agent-prompts/debug.txt +49 -0
  50. package/src/templates/agent-prompts/debugger.txt +149 -144
  51. package/src/templates/agent-prompts/explorer.txt +10 -0
  52. package/src/templates/agent-prompts/fixer.txt +66 -66
  53. package/src/templates/agent-prompts/hierarchy.txt +101 -96
  54. package/src/templates/agent-prompts/inspector.txt +79 -79
  55. package/src/templates/agent-prompts/patcher.txt +20 -20
  56. package/src/templates/agent-prompts/plan-checker.txt +45 -45
  57. package/src/templates/agent-prompts/plan-writer.txt +57 -57
  58. package/src/templates/agent-prompts/probe-reporter.txt +62 -62
  59. package/src/templates/agent-prompts/prober.txt +93 -87
  60. package/src/templates/agent-prompts/reporter.txt +10 -0
  61. package/src/templates/agent-prompts/researcher.txt +48 -48
  62. package/src/templates/agent-prompts/reviewer.txt +57 -57
  63. package/src/templates/agent-prompts/tester.txt +66 -66
  64. package/src/templates/agent-prompts/tracer.txt +33 -33
  65. package/src/templates/agent-prompts/verifier.txt +10 -0
  66. package/src/templates/commands/squeeze-dashboard.md +5 -0
  67. package/src/templates/commands/squeeze-health.md +10 -0
  68. package/src/templates/commands/squeeze-quick.md +10 -0
  69. package/src/templates/model-defaults.json +59 -73
  70. package/src/templates/opencode.config.json +243 -572
  71. package/src/templates/skills/brooks-lint-rca/SKILL.md +48 -0
  72. package/src/templates/skills/codebase-fact-finding/SKILL.md +39 -0
  73. package/src/templates/skills/diff-review/SKILL.md +42 -0
  74. package/src/templates/skills/general-coding/SKILL.md +44 -0
  75. package/src/templates/skills/minimal-fixing/SKILL.md +25 -0
  76. package/src/templates/skills/plan-checking/SKILL.md +33 -0
  77. package/src/templates/skills/ponytail-patching/SKILL.md +20 -0
  78. package/src/templates/skills/prd-formatting/SKILL.md +45 -0
  79. package/src/templates/skills/refactoring-patterns/SKILL.md +37 -0
  80. package/src/templates/skills/security-auditing/SKILL.md +35 -0
  81. package/src/templates/skills/security-remediation/SKILL.md +37 -0
  82. package/src/templates/skills/summary-reporting/SKILL.md +83 -0
  83. package/src/templates/skills/test-assurance/SKILL.md +48 -0
  84. package/src/templates/skills/test-mocking-strategy/SKILL.md +18 -0
  85. package/src/templates/skills/ui-design-audit/SKILL.md +23 -0
  86. package/src/templates/skills/ui-design-system/SKILL.md +37 -0
  87. package/src/templates/skills/wstg-recon/SKILL.md +33 -0
  88. package/src/templates/themes/pluidr-colorful.json +241 -0
  89. package/src/templates/themes/pluidr-dark.json +177 -0
  90. package/src/templates/themes/pluidr-light.json +241 -0
@@ -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, "builder.txt"), "test instructions", "utf-8")
22
+ writeFileSync(join(agentPromptsDir, "reporter.txt"), "write docs", "utf-8")
23
+
24
+ writeAgentPrompts(tmpDir, destDir)
25
+
26
+ const coderContent = readFileSync(join(destDir, "builder.txt"), "utf-8")
27
+ const composeReporterContent = readFileSync(join(destDir, "reporter.txt"), "utf-8")
28
+
29
+ assert.ok(coderContent.startsWith('You are the "builder" agent.\n\n'),
30
+ "builder should have identity header")
31
+ assert.ok(composeReporterContent.startsWith('You are the "reporter" agent.\n\n'),
32
+ "reporter should have identity header")
33
+ assert.ok(coderContent.includes("test instructions"),
34
+ "original content preserved in builder")
35
+ assert.ok(composeReporterContent.includes("write docs"),
36
+ "original content preserved in 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
+ })
@@ -0,0 +1,83 @@
1
+ import fs from "node:fs"
2
+ import { getTuiConfigPath } from "./paths.js"
3
+
4
+ export const COLORS = [
5
+ "\x1b[38;2;0;225;255m", // Bright Cyan
6
+ "\x1b[38;2;0;200;250m", // Ocean Blue
7
+ "\x1b[38;2;50;150;250m", // Soft Blue-Purple
8
+ "\x1b[38;2;120;120;255m", // Indigo-Purple
9
+ "\x1b[38;2;180;90;255m" // Purple
10
+ ]
11
+
12
+ export const ASCII_LOGO_LINES = [
13
+ " ██████ ██ ██ ██ ██ ███████ ███████ ",
14
+ " ██ ██ ██ ██ ██ ██ ██ ██ ██ ██",
15
+ " ██████ ██ ██ ██ ██ ██ ██ ███████ ",
16
+ " ██ ██ ██ ██ ██ ██ ██ ██ ██ ",
17
+ " ██ ███████ █████ ██ ███████ ██ ██"
18
+ ]
19
+
20
+ export function getAnimationType() {
21
+ // 1. Check command line args
22
+ const argv = process.argv
23
+ const animIndex = argv.indexOf("--animation")
24
+ if (animIndex !== -1 && animIndex + 1 < argv.length) {
25
+ const val = argv[animIndex + 1].toLowerCase()
26
+ if (["float", "wave", "pulse", "none"].includes(val)) {
27
+ return val
28
+ }
29
+ }
30
+
31
+ // 2. Check env variable
32
+ if (process.env.PLUIDR_ANIMATION) {
33
+ const val = process.env.PLUIDR_ANIMATION.toLowerCase()
34
+ if (["float", "wave", "pulse", "none"].includes(val)) {
35
+ return val
36
+ }
37
+ }
38
+
39
+ // 3. Check tui.json
40
+ const path = getTuiConfigPath()
41
+ if (fs.existsSync(path)) {
42
+ try {
43
+ const config = JSON.parse(fs.readFileSync(path, "utf-8"))
44
+ if (config.animation) {
45
+ const val = config.animation.toLowerCase()
46
+ if (["float", "wave", "pulse", "none"].includes(val)) {
47
+ return val
48
+ }
49
+ }
50
+ } catch {
51
+ // Ignore
52
+ }
53
+ }
54
+
55
+ return "wave" // default to wave
56
+ }
57
+
58
+ export function getLogoLine(line, r, frame, type) {
59
+ let offset = 3
60
+ let colorIndex = (frame + r) % COLORS.length
61
+
62
+ switch (type) {
63
+ case "wave":
64
+ offset = Math.round(3 + 2 * Math.sin(frame / 3.5 - r * 0.8))
65
+ break
66
+ case "float":
67
+ offset = Math.round(3 + 2 * Math.sin(frame / 3.5))
68
+ break
69
+ case "pulse":
70
+ offset = 3
71
+ colorIndex = frame % COLORS.length
72
+ break
73
+ case "none":
74
+ offset = 3
75
+ colorIndex = r % COLORS.length
76
+ break
77
+ }
78
+
79
+ return {
80
+ text: " ".repeat(offset) + line,
81
+ color: COLORS[colorIndex]
82
+ }
83
+ }
@@ -0,0 +1,118 @@
1
+ import { describe, it, before, after, beforeEach, afterEach, mock } from "node:test"
2
+ import assert from "node:assert"
3
+ import fs from "node:fs"
4
+ import { getAnimationType, getLogoLine, COLORS, ASCII_LOGO_LINES } from "./animation.js"
5
+
6
+ describe("animation utility", () => {
7
+ let originalArgv
8
+ let originalEnv
9
+
10
+ before(() => {
11
+ originalArgv = [...process.argv]
12
+ originalEnv = { ...process.env }
13
+ })
14
+
15
+ after(() => {
16
+ process.argv = originalArgv
17
+ process.env = originalEnv
18
+ })
19
+
20
+ beforeEach(() => {
21
+ // Clear animation env and argv for clean start
22
+ delete process.env.PLUIDR_ANIMATION
23
+ process.argv = [originalArgv[0], originalArgv[1]]
24
+ })
25
+
26
+ afterEach(() => {
27
+ mock.reset()
28
+ })
29
+
30
+ describe("getAnimationType", () => {
31
+ it("should return wave as the default when no configs are present", () => {
32
+ // Mock existsSync to return false for tui.json
33
+ mock.method(fs, "existsSync", () => false)
34
+
35
+ const type = getAnimationType()
36
+ assert.strictEqual(type, "wave")
37
+ })
38
+
39
+ it("should prioritize CLI argument over others", () => {
40
+ mock.method(fs, "existsSync", () => true)
41
+ mock.method(fs, "readFileSync", () => JSON.stringify({ animation: "float" }))
42
+
43
+ process.env.PLUIDR_ANIMATION = "none"
44
+ process.argv.push("--animation", "pulse")
45
+
46
+ const type = getAnimationType()
47
+ assert.strictEqual(type, "pulse")
48
+ })
49
+
50
+ it("should prioritize env variable over tui.json", () => {
51
+ mock.method(fs, "existsSync", () => true)
52
+ mock.method(fs, "readFileSync", () => JSON.stringify({ animation: "float" }))
53
+
54
+ process.env.PLUIDR_ANIMATION = "none"
55
+
56
+ const type = getAnimationType()
57
+ assert.strictEqual(type, "none")
58
+ })
59
+
60
+ it("should fallback to default wave if config has invalid animation type", () => {
61
+ mock.method(fs, "existsSync", () => true)
62
+ mock.method(fs, "readFileSync", () => JSON.stringify({ animation: "invalid_type_abc" }))
63
+
64
+ const type = getAnimationType()
65
+ assert.strictEqual(type, "wave")
66
+ })
67
+
68
+ it("should read animation type from tui.json if no CLI/env configs are set", () => {
69
+ mock.method(fs, "existsSync", () => true)
70
+ mock.method(fs, "readFileSync", () => JSON.stringify({ animation: "float" }))
71
+
72
+ const type = getAnimationType()
73
+ assert.strictEqual(type, "float")
74
+ })
75
+ })
76
+
77
+ describe("getLogoLine", () => {
78
+ const sampleLine = "TEST_LINE"
79
+
80
+ it("renders float animation with horizontal synchronization", () => {
81
+ const frame = 0
82
+ const r = 0
83
+ const res = getLogoLine(sampleLine, r, frame, "float")
84
+
85
+ assert.ok(res.text.includes(sampleLine))
86
+ assert.strictEqual(res.color, COLORS[0])
87
+ })
88
+
89
+ it("renders wave animation with row-skewed offsets", () => {
90
+ const frame = 5
91
+ const r = 2
92
+ const res = getLogoLine(sampleLine, r, frame, "wave")
93
+
94
+ assert.ok(res.text.includes(sampleLine))
95
+ assert.strictEqual(res.color, COLORS[(frame + r) % COLORS.length])
96
+ })
97
+
98
+ it("renders pulse animation with static offset but pulsing color", () => {
99
+ const frame = 3
100
+ const r = 1
101
+ const res = getLogoLine(sampleLine, r, frame, "pulse")
102
+
103
+ // Pulse maintains a static offset of 3 spaces
104
+ assert.strictEqual(res.text, " " + sampleLine)
105
+ assert.strictEqual(res.color, COLORS[frame % COLORS.length])
106
+ })
107
+
108
+ it("renders none animation with static offset and static gradient colors", () => {
109
+ const frame = 10
110
+ const r = 3
111
+ const res = getLogoLine(sampleLine, r, frame, "none")
112
+
113
+ // None maintains static offset of 3 spaces
114
+ assert.strictEqual(res.text, " " + sampleLine)
115
+ assert.strictEqual(res.color, COLORS[r % COLORS.length])
116
+ })
117
+ })
118
+ })
@@ -1,46 +1,46 @@
1
- import { existsSync, copyFileSync, readdirSync, unlinkSync } from "node:fs"
2
- import { join, dirname } from "node:path"
3
- import { getConfigPath, getTuiConfigPath } 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)) {
36
- rotateBackups(resolvedPath)
37
- const backupPath = `${resolvedPath}.bak.${timestamp()}`
38
- copyFileSync(resolvedPath, backupPath)
39
- }
40
-
41
- const tuiPath = getTuiConfigPath()
42
- if (existsSync(tuiPath)) {
43
- const backupTuiPath = `${tuiPath}.bak.${timestamp()}`
44
- copyFileSync(tuiPath, backupTuiPath)
45
- }
46
- }
1
+ import { existsSync, copyFileSync, readdirSync, unlinkSync } from "node:fs"
2
+ import { join, dirname } from "node:path"
3
+ import { getConfigPath, getTuiConfigPath } 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)) {
36
+ rotateBackups(resolvedPath)
37
+ const backupPath = `${resolvedPath}.bak.${timestamp()}`
38
+ copyFileSync(resolvedPath, backupPath)
39
+ }
40
+
41
+ const tuiPath = getTuiConfigPath()
42
+ if (existsSync(tuiPath)) {
43
+ const backupTuiPath = `${tuiPath}.bak.${timestamp()}`
44
+ copyFileSync(tuiPath, backupTuiPath)
45
+ }
46
+ }
@@ -0,0 +1,26 @@
1
+ import { mkdirSync, cpSync, existsSync, readdirSync, unlinkSync } from "node:fs"
2
+ import { resolve } from "node:path"
3
+ import { getCommandsDir } from "./paths.js"
4
+
5
+ export function writeCommands(templatesDir, destDir) {
6
+ const sourcePath = resolve(templatesDir, "commands")
7
+ const targetDir = destDir || getCommandsDir()
8
+
9
+ // Clear any existing .md files in targetDir to prevent stale commands
10
+ if (existsSync(targetDir)) {
11
+ try {
12
+ for (const file of readdirSync(targetDir)) {
13
+ if (file.endsWith(".md")) {
14
+ unlinkSync(resolve(targetDir, file))
15
+ }
16
+ }
17
+ } catch {
18
+ // Ignore
19
+ }
20
+ }
21
+
22
+ mkdirSync(targetDir, { recursive: true })
23
+ if (existsSync(sourcePath)) {
24
+ cpSync(sourcePath, targetDir, { recursive: true })
25
+ }
26
+ }
@@ -0,0 +1,29 @@
1
+ import { describe, it, after } from "node:test"
2
+ import assert from "node:assert"
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync, mkdirSync, writeFileSync } from "node:fs"
4
+ import { join } from "node:path"
5
+ import { tmpdir } from "node:os"
6
+ import { writeCommands } from "./commandsWriter.js"
7
+
8
+ describe("commandsWriter", () => {
9
+ const destDir = mkdtempSync(join(tmpdir(), "pluidr-command-dest-"))
10
+
11
+ after(() => {
12
+ if (existsSync(destDir)) rmSync(destDir, { recursive: true })
13
+ })
14
+
15
+ it("copies command files to destination directory", () => {
16
+ const tmpSrcDir = mkdtempSync(join(tmpdir(), "pluidr-command-src-"))
17
+ const srcCommandsDir = join(tmpSrcDir, "commands")
18
+ mkdirSync(srcCommandsDir, { recursive: true })
19
+ writeFileSync(join(srcCommandsDir, "squeeze-quick.md"), "mock-command-content", "utf-8")
20
+
21
+ writeCommands(tmpSrcDir, destDir)
22
+
23
+ const destPath = join(destDir, "squeeze-quick.md")
24
+ assert.ok(existsSync(destPath), "command file should be copied")
25
+ assert.strictEqual(readFileSync(destPath, "utf-8"), "mock-command-content")
26
+
27
+ rmSync(tmpSrcDir, { recursive: true })
28
+ })
29
+ })
@@ -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
+ }