pluidr 0.7.0 → 0.7.2

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 (53) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +365 -365
  3. package/bin/pluidr.js +3 -3
  4. package/package.json +45 -45
  5. package/src/cli/commands/doctor.js +113 -101
  6. package/src/cli/commands/init.js +47 -43
  7. package/src/cli/commands/launch.js +46 -44
  8. package/src/cli/commands/uninstall.js +90 -63
  9. package/src/cli/commands/update.js +6 -6
  10. package/src/cli/index.js +57 -57
  11. package/src/cli/wizard/selectModelTier.js +40 -40
  12. package/src/core/agentPromptWriter.js +32 -32
  13. package/src/core/agentPromptWriter.test.js +56 -56
  14. package/src/core/backup.js +46 -40
  15. package/src/core/configBuilder.js +32 -32
  16. package/src/core/configBuilder.test.js +93 -93
  17. package/src/core/configWriter.js +10 -10
  18. package/src/core/identityHeader.js +8 -8
  19. package/src/core/paths.js +11 -9
  20. package/src/core/paths.test.js +29 -21
  21. package/src/core/pluginWriter.js +29 -29
  22. package/src/core/squeezeInstaller.js +158 -161
  23. package/src/core/squeezeInstaller.test.js +79 -75
  24. package/src/core/themeWriter.js +12 -0
  25. package/src/core/themeWriter.test.js +29 -0
  26. package/src/core/tuiConfigWriter.js +22 -0
  27. package/src/core/tuiConfigWriter.test.js +38 -0
  28. package/src/core/version.js +8 -8
  29. package/src/core/versionCheck.js +44 -35
  30. package/src/core/versionCheck.test.js +12 -2
  31. package/src/plugins/README.md +68 -68
  32. package/src/plugins/pluidr-squeeze.js +77 -77
  33. package/src/templates/agent-prompts/auditor.txt +20 -20
  34. package/src/templates/agent-prompts/coder.txt +87 -87
  35. package/src/templates/agent-prompts/compose-reporter.txt +55 -55
  36. package/src/templates/agent-prompts/composer.txt +429 -430
  37. package/src/templates/agent-prompts/debug-reporter.txt +65 -65
  38. package/src/templates/agent-prompts/debugger.txt +152 -151
  39. package/src/templates/agent-prompts/fixer.txt +66 -66
  40. package/src/templates/agent-prompts/hierarchy.txt +96 -96
  41. package/src/templates/agent-prompts/inspector.txt +79 -79
  42. package/src/templates/agent-prompts/patcher.txt +20 -20
  43. package/src/templates/agent-prompts/plan-checker.txt +45 -45
  44. package/src/templates/agent-prompts/plan-writer.txt +57 -57
  45. package/src/templates/agent-prompts/probe-reporter.txt +62 -62
  46. package/src/templates/agent-prompts/prober.txt +94 -90
  47. package/src/templates/agent-prompts/researcher.txt +48 -48
  48. package/src/templates/agent-prompts/reviewer.txt +57 -57
  49. package/src/templates/agent-prompts/tester.txt +66 -66
  50. package/src/templates/agent-prompts/tracer.txt +33 -33
  51. package/src/templates/model-defaults.json +73 -73
  52. package/src/templates/opencode.config.json +482 -481
  53. package/src/templates/themes/pluidr-contrast.json +177 -0
@@ -1,63 +1,90 @@
1
- import { existsSync, copyFileSync, rmSync, readdirSync } from "node:fs"
2
- import { join } 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) {
31
- copyFileSync(latestBackup, configPath)
32
- summary.restored = latestBackup
33
- }
34
-
35
- // Remove Pluidr-installed artifacts
36
- if (existsSync(promptsDir)) {
37
- rmSync(promptsDir, { recursive: true, force: true })
38
- summary.removed.push("prompts/")
39
- }
40
- if (existsSync(pluginsDir)) {
41
- rmSync(pluginsDir, { recursive: true, force: true })
42
- summary.removed.push("plugins/")
43
- }
44
- if (existsSync(binDir)) {
45
- rmSync(binDir, { recursive: true, force: true })
46
- summary.removed.push("bin/")
47
- }
48
-
49
- // Print summary
50
- console.log("Uninstall complete:")
51
- if (summary.restored) {
52
- console.log(` ✓ Restored ${summary.restored}`)
53
- } else {
54
- console.log(" - No backup found to restore")
55
- }
56
- if (summary.removed.length > 0) {
57
- console.log(` Removed: ${summary.removed.join(", ")}`)
58
- } else {
59
- console.log(" - No Pluidr artifacts found to remove")
60
- }
61
- console.log(" - Kept opencode.jsonc (preserves user customizations)")
62
- console.log(" - Kept package.json (may be used by other plugins)")
63
- }
1
+ import { existsSync, copyFileSync, rmSync, readdirSync } from "node:fs"
2
+ import { join } 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
+ function findLatestTuiBackup() {
20
+ const dir = getConfigDir()
21
+ let backups = []
22
+ try {
23
+ backups = readdirSync(dir)
24
+ .filter((f) => f.startsWith("tui.json.bak."))
25
+ .sort()
26
+ .reverse()
27
+ } catch {
28
+ return null
29
+ }
30
+ return backups.length > 0 ? join(dir, backups[0]) : null
31
+ }
32
+
33
+ export async function runUninstall() {
34
+ const configDir = getConfigDir()
35
+ const configPath = getConfigPath()
36
+ const promptsDir = join(configDir, "prompts")
37
+ const pluginsDir = join(configDir, "plugins")
38
+ const binDir = join(configDir, "bin")
39
+
40
+ const summary = { restored: null, removed: [] }
41
+
42
+ // Restore latest backup
43
+ const latestBackup = findLatestBackup()
44
+ if (latestBackup) {
45
+ copyFileSync(latestBackup, configPath)
46
+ summary.restored = latestBackup
47
+ }
48
+
49
+ const latestTuiBackup = findLatestTuiBackup()
50
+ const tuiConfigPath = join(configDir, "tui.json")
51
+ if (latestTuiBackup) {
52
+ copyFileSync(latestTuiBackup, tuiConfigPath)
53
+ }
54
+
55
+ const themesDir = join(configDir, "themes")
56
+
57
+ // Remove Pluidr-installed artifacts
58
+ if (existsSync(promptsDir)) {
59
+ rmSync(promptsDir, { recursive: true, force: true })
60
+ summary.removed.push("prompts/")
61
+ }
62
+ if (existsSync(pluginsDir)) {
63
+ rmSync(pluginsDir, { recursive: true, force: true })
64
+ summary.removed.push("plugins/")
65
+ }
66
+ if (existsSync(binDir)) {
67
+ rmSync(binDir, { recursive: true, force: true })
68
+ summary.removed.push("bin/")
69
+ }
70
+ const customThemePath = join(themesDir, "pluidr-contrast.json")
71
+ if (existsSync(customThemePath)) {
72
+ rmSync(customThemePath, { force: true })
73
+ summary.removed.push("themes/pluidr-contrast.json")
74
+ }
75
+
76
+ // Print summary
77
+ console.log("Uninstall complete:")
78
+ if (summary.restored) {
79
+ console.log(` ✓ Restored ${summary.restored}`)
80
+ } else {
81
+ console.log(" - No backup found to restore")
82
+ }
83
+ if (summary.removed.length > 0) {
84
+ console.log(` ✓ Removed: ${summary.removed.join(", ")}`)
85
+ } else {
86
+ console.log(" - No Pluidr artifacts found to remove")
87
+ }
88
+ console.log(" - Kept opencode.jsonc (preserves user customizations)")
89
+ console.log(" - Kept package.json (may be used by other plugins)")
90
+ }
@@ -1,6 +1,6 @@
1
- import { checkAndPromptUpdate } from "../../core/versionCheck.js"
2
- import { version } from "../../core/version.js"
3
-
4
- export async function runUpdate() {
5
- await checkAndPromptUpdate(version)
6
- }
1
+ import { checkAndPromptUpdate } from "../../core/versionCheck.js"
2
+ import { version } from "../../core/version.js"
3
+
4
+ export async function runUpdate() {
5
+ await checkAndPromptUpdate(version)
6
+ }
package/src/cli/index.js CHANGED
@@ -1,57 +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
- import { runLaunch } from "./commands/launch.js"
9
- import { version } from "../core/version.js"
10
-
11
- const HELP = `Usage: pluidr [command]
12
-
13
- pluidr Check for updates, run doctor, launch opencode
14
- pluidr init Set up OpenCode with Pluidr's 17-agent pipeline
15
- pluidr update Check for pluidr updates
16
- pluidr doctor Verify installation health
17
- pluidr uninstall Remove Pluidr artifacts and restore previous config
18
- --version, -v Print 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
+ 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,40 +1,40 @@
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
+ 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,40 +1,46 @@
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
+ 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
+ }