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.
- package/LICENSE +20 -20
- package/README.md +408 -364
- package/bin/pluidr.js +3 -3
- package/package.json +1 -1
- package/src/cli/commands/doctor.js +137 -134
- package/src/cli/commands/init.js +134 -47
- package/src/cli/commands/launch.js +101 -50
- package/src/cli/commands/theme.js +67 -0
- package/src/cli/commands/theme.test.js +28 -0
- package/src/cli/commands/uninstall.js +103 -90
- package/src/cli/commands/update.js +9 -9
- package/src/cli/index.js +63 -57
- package/src/cli/wizard/selectModelTier.js +40 -40
- package/src/core/agentPromptWriter.js +45 -32
- package/src/core/agentPromptWriter.test.js +56 -56
- package/src/core/animation.js +83 -0
- package/src/core/animation.test.js +118 -0
- package/src/core/backup.js +46 -46
- package/src/core/commandsWriter.js +26 -0
- package/src/core/commandsWriter.test.js +29 -0
- package/src/core/configBuilder.js +32 -32
- package/src/core/configBuilder.test.js +93 -93
- package/src/core/configWriter.js +10 -10
- package/src/core/configWriter.test.js +1 -1
- package/src/core/identityHeader.js +8 -8
- package/src/core/paths.js +13 -11
- package/src/core/paths.test.js +33 -29
- package/src/core/pluginWriter.js +43 -29
- package/src/core/skillsWriter.js +21 -0
- package/src/core/skillsWriter.test.js +30 -0
- package/src/core/squeezeInstaller.js +158 -158
- package/src/core/squeezeInstaller.test.js +79 -79
- package/src/core/themeWriter.js +18 -4
- package/src/core/themeWriter.test.js +2 -2
- package/src/core/tuiConfigWriter.js +3 -2
- package/src/core/tuiConfigWriter.test.js +4 -4
- package/src/core/version.js +8 -8
- package/src/core/versionCheck.js +44 -44
- package/src/plugins/README.md +57 -68
- package/src/plugins/pluidr-squeeze.js +77 -77
- package/src/templates/agent-prompts/analyze.txt +49 -0
- package/src/templates/agent-prompts/auditor.txt +20 -20
- package/src/templates/agent-prompts/builder.txt +10 -0
- package/src/templates/agent-prompts/coder.txt +87 -87
- package/src/templates/agent-prompts/compose-reporter.txt +55 -55
- package/src/templates/agent-prompts/compose.txt +50 -0
- package/src/templates/agent-prompts/composer.txt +414 -420
- package/src/templates/agent-prompts/debug-reporter.txt +65 -65
- package/src/templates/agent-prompts/debug.txt +49 -0
- package/src/templates/agent-prompts/debugger.txt +149 -144
- package/src/templates/agent-prompts/explorer.txt +10 -0
- package/src/templates/agent-prompts/fixer.txt +66 -66
- package/src/templates/agent-prompts/hierarchy.txt +101 -96
- package/src/templates/agent-prompts/inspector.txt +79 -79
- package/src/templates/agent-prompts/patcher.txt +20 -20
- package/src/templates/agent-prompts/plan-checker.txt +45 -45
- package/src/templates/agent-prompts/plan-writer.txt +57 -57
- package/src/templates/agent-prompts/probe-reporter.txt +62 -62
- package/src/templates/agent-prompts/prober.txt +93 -87
- package/src/templates/agent-prompts/reporter.txt +10 -0
- package/src/templates/agent-prompts/researcher.txt +48 -48
- package/src/templates/agent-prompts/reviewer.txt +57 -57
- package/src/templates/agent-prompts/tester.txt +66 -66
- package/src/templates/agent-prompts/tracer.txt +33 -33
- package/src/templates/agent-prompts/verifier.txt +10 -0
- package/src/templates/commands/squeeze-dashboard.md +5 -0
- package/src/templates/commands/squeeze-health.md +10 -0
- package/src/templates/commands/squeeze-quick.md +10 -0
- package/src/templates/model-defaults.json +59 -73
- package/src/templates/opencode.config.json +243 -572
- package/src/templates/skills/brooks-lint-rca/SKILL.md +48 -0
- package/src/templates/skills/codebase-fact-finding/SKILL.md +39 -0
- package/src/templates/skills/diff-review/SKILL.md +42 -0
- package/src/templates/skills/general-coding/SKILL.md +44 -0
- package/src/templates/skills/minimal-fixing/SKILL.md +25 -0
- package/src/templates/skills/plan-checking/SKILL.md +33 -0
- package/src/templates/skills/ponytail-patching/SKILL.md +20 -0
- package/src/templates/skills/prd-formatting/SKILL.md +45 -0
- package/src/templates/skills/refactoring-patterns/SKILL.md +37 -0
- package/src/templates/skills/security-auditing/SKILL.md +35 -0
- package/src/templates/skills/security-remediation/SKILL.md +37 -0
- package/src/templates/skills/summary-reporting/SKILL.md +83 -0
- package/src/templates/skills/test-assurance/SKILL.md +48 -0
- package/src/templates/skills/test-mocking-strategy/SKILL.md +18 -0
- package/src/templates/skills/ui-design-audit/SKILL.md +23 -0
- package/src/templates/skills/ui-design-system/SKILL.md +37 -0
- package/src/templates/skills/wstg-recon/SKILL.md +33 -0
- package/src/templates/themes/pluidr-colorful.json +241 -0
- package/src/templates/themes/pluidr-dark.json +177 -0
- 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, "
|
|
22
|
-
writeFileSync(join(agentPromptsDir, "
|
|
23
|
-
|
|
24
|
-
writeAgentPrompts(tmpDir, destDir)
|
|
25
|
-
|
|
26
|
-
const coderContent = readFileSync(join(destDir, "
|
|
27
|
-
const composeReporterContent = readFileSync(join(destDir, "
|
|
28
|
-
|
|
29
|
-
assert.ok(coderContent.startsWith('You are the "
|
|
30
|
-
"
|
|
31
|
-
assert.ok(composeReporterContent.startsWith('You are the "
|
|
32
|
-
"
|
|
33
|
-
assert.ok(coderContent.includes("test instructions"),
|
|
34
|
-
"original content preserved in
|
|
35
|
-
assert.ok(composeReporterContent.includes("write docs"),
|
|
36
|
-
"original content preserved in
|
|
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
|
+
})
|
package/src/core/backup.js
CHANGED
|
@@ -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
|
+
}
|