@funara/wevr 0.1.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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +397 -0
  3. package/bin/wevr.js +4 -0
  4. package/package.json +48 -0
  5. package/src/cli/commands/doctor.js +137 -0
  6. package/src/cli/commands/init.js +156 -0
  7. package/src/cli/commands/launch.js +122 -0
  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 -0
  11. package/src/cli/commands/update.js +9 -0
  12. package/src/cli/index.js +63 -0
  13. package/src/cli/wizard/selectModelTier.js +40 -0
  14. package/src/core/agentPromptWriter.js +45 -0
  15. package/src/core/agentPromptWriter.test.js +56 -0
  16. package/src/core/backup.js +46 -0
  17. package/src/core/backup.test.js +51 -0
  18. package/src/core/commandsWriter.js +26 -0
  19. package/src/core/commandsWriter.test.js +29 -0
  20. package/src/core/configBuilder.js +32 -0
  21. package/src/core/configBuilder.test.js +93 -0
  22. package/src/core/configWriter.js +10 -0
  23. package/src/core/configWriter.test.js +26 -0
  24. package/src/core/identityHeader.js +8 -0
  25. package/src/core/identityHeader.test.js +15 -0
  26. package/src/core/paths.js +13 -0
  27. package/src/core/paths.test.js +33 -0
  28. package/src/core/pluginWriter.js +29 -0
  29. package/src/core/pluginWriter.test.js +41 -0
  30. package/src/core/skillsWriter.js +13 -0
  31. package/src/core/skillsWriter.test.js +30 -0
  32. package/src/core/themeWriter.js +26 -0
  33. package/src/core/themeWriter.test.js +29 -0
  34. package/src/core/tuiConfigWriter.js +22 -0
  35. package/src/core/tuiConfigWriter.test.js +38 -0
  36. package/src/core/version.js +8 -0
  37. package/src/core/versionCheck.js +44 -0
  38. package/src/core/versionCheck.test.js +34 -0
  39. package/src/plugins/README.md +57 -0
  40. package/src/plugins/wevr-flow.js +137 -0
  41. package/src/plugins/wevr-squeeze.js +3630 -0
  42. package/src/templates/agent-prompts/analyze.txt +43 -0
  43. package/src/templates/agent-prompts/builder.txt +10 -0
  44. package/src/templates/agent-prompts/compose.txt +45 -0
  45. package/src/templates/agent-prompts/debug.txt +43 -0
  46. package/src/templates/agent-prompts/explorer.txt +10 -0
  47. package/src/templates/agent-prompts/hierarchy.txt +95 -0
  48. package/src/templates/agent-prompts/reporter.txt +10 -0
  49. package/src/templates/agent-prompts/verifier.txt +10 -0
  50. package/src/templates/commands/squeeze-dashboard.md +5 -0
  51. package/src/templates/commands/squeeze-health.md +10 -0
  52. package/src/templates/commands/squeeze-quick.md +10 -0
  53. package/src/templates/model-defaults.json +59 -0
  54. package/src/templates/opencode.config.json +243 -0
  55. package/src/templates/skills/brooks-lint-rca/SKILL.md +48 -0
  56. package/src/templates/skills/codebase-fact-finding/SKILL.md +39 -0
  57. package/src/templates/skills/diff-review/SKILL.md +42 -0
  58. package/src/templates/skills/general-coding/SKILL.md +43 -0
  59. package/src/templates/skills/minimal-fixing/SKILL.md +25 -0
  60. package/src/templates/skills/plan-checking/SKILL.md +33 -0
  61. package/src/templates/skills/ponytail-patching/SKILL.md +20 -0
  62. package/src/templates/skills/prd-formatting/SKILL.md +45 -0
  63. package/src/templates/skills/refactoring-patterns/SKILL.md +37 -0
  64. package/src/templates/skills/security-auditing/SKILL.md +35 -0
  65. package/src/templates/skills/security-remediation/SKILL.md +37 -0
  66. package/src/templates/skills/summary-reporting/SKILL.md +83 -0
  67. package/src/templates/skills/test-assurance/SKILL.md +44 -0
  68. package/src/templates/skills/test-mocking-strategy/SKILL.md +18 -0
  69. package/src/templates/skills/ui-design-audit/SKILL.md +23 -0
  70. package/src/templates/skills/ui-design-system/SKILL.md +37 -0
  71. package/src/templates/skills/wstg-recon/SKILL.md +33 -0
  72. package/src/templates/themes/wevr-colorful.json +241 -0
  73. package/src/templates/themes/wevr-dark.json +177 -0
  74. package/src/templates/themes/wevr-light.json +241 -0
@@ -0,0 +1,56 @@
1
+ import { describe, it, after } from "node:test"
2
+ import assert from "node:assert"
3
+ import { existsSync, mkdtempSync, writeFileSync, readFileSync, rmSync, mkdirSync } from "node:fs"
4
+ import { join } from "node:path"
5
+ import { tmpdir } from "node:os"
6
+ import { writeAgentPrompts } from "./agentPromptWriter.js"
7
+
8
+ describe("agentPromptWriter", () => {
9
+ const destDir = mkdtempSync(join(tmpdir(), "wevr-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,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,51 @@
1
+ import { describe, it, before, after } from "node:test"
2
+ import assert from "node:assert"
3
+ import { existsSync, writeFileSync, readFileSync, rmSync, readdirSync, mkdirSync, mkdtempSync } from "node:fs"
4
+ import { join, dirname } from "node:path"
5
+ import { tmpdir } from "node:os"
6
+ import { backupExistingConfig } from "./backup.js"
7
+
8
+ describe("backup", () => {
9
+ const tempDir = mkdtempSync(join(tmpdir(), "wevr-backup-"))
10
+ const configPath = join(tempDir, "opencode.jsonc")
11
+ const TEST_CONTENT = JSON.stringify({ test: true })
12
+
13
+ function findBackups() {
14
+ try {
15
+ return readdirSync(tempDir).filter((f) => f.startsWith("opencode.jsonc.bak."))
16
+ } catch { return [] }
17
+ }
18
+
19
+ before(() => {
20
+ mkdirSync(tempDir, { recursive: true })
21
+ })
22
+
23
+ after(() => {
24
+ if (existsSync(tempDir)) rmSync(tempDir, { recursive: true })
25
+ })
26
+
27
+ it("skips if no config exists (no throw)", () => {
28
+ // Ensure no config file exists in temp dir
29
+ if (existsSync(configPath)) rmSync(configPath)
30
+ for (const bak of findBackups()) {
31
+ rmSync(join(tempDir, bak))
32
+ }
33
+ backupExistingConfig(configPath)
34
+ assert.ok(true)
35
+ })
36
+
37
+ it("copies existing config to backup with timestamped name", () => {
38
+ // Write test config
39
+ writeFileSync(configPath, TEST_CONTENT, "utf-8")
40
+
41
+ backupExistingConfig(configPath)
42
+
43
+ const backups = findBackups()
44
+ assert.ok(backups.length > 0, "timestamped backup file should exist")
45
+ const newest = backups.sort().reverse()[0]
46
+ const newestPath = join(tempDir, newest)
47
+ assert.ok(newestPath.includes("opencode.jsonc.bak."), "backup should be timestamped")
48
+ const backupContent = JSON.parse(readFileSync(newestPath, "utf-8"))
49
+ assert.deepStrictEqual(backupContent, { test: true })
50
+ })
51
+ })
@@ -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(), "wevr-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(), "wevr-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
+ })
@@ -0,0 +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
+ }
@@ -0,0 +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
+ compose: {},
15
+ verifier: {},
16
+ builder: {},
17
+ },
18
+ }
19
+ const defaultsJson = {
20
+ reasoning: {
21
+ provider: "test",
22
+ model: "big-model",
23
+ agents: ["compose"],
24
+ },
25
+ precision: {
26
+ provider: "test",
27
+ model: "mid-model",
28
+ agents: ["verifier"],
29
+ },
30
+ fast: {
31
+ provider: "test",
32
+ model: "small-model",
33
+ agents: ["builder"],
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.compose.model, "test/big-model")
50
+ assert.strictEqual(result.agent.verifier.model, "test/mid-model")
51
+ assert.strictEqual(result.agent.builder.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 'compose'
62
+ builder: {},
63
+ },
64
+ }
65
+ const defaultsJson = {
66
+ reasoning: {
67
+ provider: "test",
68
+ model: "big-model",
69
+ agents: ["compose"],
70
+ },
71
+ fast: {
72
+ provider: "test",
73
+ model: "small-model",
74
+ agents: ["builder"],
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
+ })
@@ -0,0 +1,10 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { getConfigDir } from "./paths.js"
4
+
5
+ export function writeConfig(configObject, targetDir) {
6
+ const baseDir = targetDir || getConfigDir()
7
+ const configPath = join(baseDir, "opencode.jsonc")
8
+ mkdirSync(baseDir, { recursive: true })
9
+ writeFileSync(configPath, JSON.stringify(configObject, null, 2), "utf-8")
10
+ }
@@ -0,0 +1,26 @@
1
+ import { describe, it, after } from "node:test"
2
+ import assert from "node:assert"
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"
4
+ import { join } from "node:path"
5
+ import { tmpdir } from "node:os"
6
+ import { writeConfig } from "./configWriter.js"
7
+
8
+ describe("configWriter", () => {
9
+ const tempDir = mkdtempSync(join(tmpdir(), "wevr-configwriter-"))
10
+
11
+ after(() => {
12
+ if (existsSync(tempDir)) rmSync(tempDir, { recursive: true })
13
+ })
14
+
15
+ it("writes config as formatted JSON to the correct path", () => {
16
+ const configObj = { agent: { compose: { model: "test/model" } } }
17
+ const expected = JSON.stringify(configObj, null, 2)
18
+
19
+ writeConfig(configObj, tempDir)
20
+
21
+ const configPath = join(tempDir, "opencode.jsonc")
22
+ assert.ok(existsSync(configPath), "config file should exist")
23
+ const written = readFileSync(configPath, "utf-8")
24
+ assert.strictEqual(written, expected)
25
+ })
26
+ })
@@ -0,0 +1,8 @@
1
+ // Single source of truth for the agent identity prefix prepended to every
2
+ // generated prompt file. Mirrors the system identity concept from the
3
+ // dropped hook-based AgentSelfIdentityPlugin, but as a static prepend
4
+ // (KISS: agent names are known at install time).
5
+
6
+ export default function IDENTITY_HEADER(agentName) {
7
+ return `You are the "${agentName}" agent.\n\n`
8
+ }
@@ -0,0 +1,15 @@
1
+ import { describe, it } from "node:test"
2
+ import assert from "node:assert"
3
+ import IDENTITY_HEADER from "./identityHeader.js"
4
+
5
+ describe("identityHeader", () => {
6
+ it('returns "You are the <name> agent.\\n\\n" format', () => {
7
+ const result = IDENTITY_HEADER("coder")
8
+ assert.strictEqual(result, 'You are the "coder" agent.\n\n')
9
+ })
10
+
11
+ it('handles multi-word agent names', () => {
12
+ const result = IDENTITY_HEADER("plan-writer")
13
+ assert.strictEqual(result, 'You are the "plan-writer" agent.\n\n')
14
+ })
15
+ })
@@ -0,0 +1,13 @@
1
+ import { homedir } from "node:os"
2
+ import { join } from "node:path"
3
+
4
+ const BASE = join(homedir(), ".config", "opencode")
5
+
6
+ export const getConfigDir = () => BASE
7
+ export const getConfigPath = () => join(BASE, "opencode.jsonc")
8
+ export const getTuiConfigPath = () => join(BASE, "tui.json")
9
+ export const getPromptsDir = () => join(BASE, "prompts")
10
+ export const getThemesDir = () => join(BASE, "themes")
11
+ export const getSkillsDir = () => join(BASE, "skills")
12
+ export const getCommandsDir = () => join(BASE, "commands")
13
+ // ponytail: getBackupPath removed (YAGNI)
@@ -0,0 +1,33 @@
1
+ import { describe, it } from "node:test"
2
+ import assert from "node:assert"
3
+ import { homedir } from "node:os"
4
+ import { join } from "node:path"
5
+ import { getConfigDir, getConfigPath, getTuiConfigPath, getPromptsDir, getThemesDir, getSkillsDir } from "./paths.js"
6
+
7
+ const BASE = join(homedir(), ".config", "opencode")
8
+
9
+ describe("paths", () => {
10
+ it("getConfigDir returns ~/.config/opencode", () => {
11
+ assert.strictEqual(getConfigDir(), BASE)
12
+ })
13
+
14
+ it("getConfigPath returns opencode.jsonc path", () => {
15
+ assert.strictEqual(getConfigPath(), join(BASE, "opencode.jsonc"))
16
+ })
17
+
18
+ it("getTuiConfigPath returns tui.json path", () => {
19
+ assert.strictEqual(getTuiConfigPath(), join(BASE, "tui.json"))
20
+ })
21
+
22
+ it("getPromptsDir returns prompts path", () => {
23
+ assert.strictEqual(getPromptsDir(), join(BASE, "prompts"))
24
+ })
25
+
26
+ it("getThemesDir returns themes path", () => {
27
+ assert.strictEqual(getThemesDir(), join(BASE, "themes"))
28
+ })
29
+
30
+ it("getSkillsDir returns skills path", () => {
31
+ assert.strictEqual(getSkillsDir(), join(BASE, "skills"))
32
+ })
33
+ })
@@ -0,0 +1,29 @@
1
+ import { copyFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs"
2
+ import { dirname, join, resolve } from "node:path"
3
+ import { fileURLToPath } from "node:url"
4
+ import { getConfigDir } from "./paths.js"
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url))
7
+ const PLUGINS = ["wevr-flow.js", "wevr-squeeze.js"]
8
+ const PACKAGE_JSON = {
9
+ type: "module",
10
+ dependencies: { "@opencode-ai/plugin": "^1.17.9" },
11
+ }
12
+
13
+ export function writePluginBundle(targetDir) {
14
+ const baseDir = targetDir || getConfigDir()
15
+ const pluginsDir = join(baseDir, "plugins")
16
+ mkdirSync(pluginsDir, { recursive: true })
17
+
18
+ for (const name of PLUGINS) {
19
+ const sourcePath = resolve(__dirname, "../plugins", name)
20
+ copyFileSync(sourcePath, join(pluginsDir, name))
21
+ }
22
+ }
23
+
24
+ export function writePluginPackageJson(targetDir) {
25
+ const baseDir = targetDir || getConfigDir()
26
+ const packageJsonPath = join(baseDir, "package.json")
27
+
28
+ writeFileSync(packageJsonPath, JSON.stringify(PACKAGE_JSON, null, 2), "utf-8")
29
+ }
@@ -0,0 +1,41 @@
1
+ import { describe, it, after } from "node:test"
2
+ import assert from "node:assert"
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"
4
+ import { join, resolve, dirname } from "node:path"
5
+ import { tmpdir } from "node:os"
6
+ import { fileURLToPath } from "node:url"
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url))
9
+
10
+ describe("pluginWriter", () => {
11
+ const tempDir = mkdtempSync(join(tmpdir(), "wevr-pluginwriter-"))
12
+
13
+ after(() => {
14
+ if (existsSync(tempDir)) rmSync(tempDir, { recursive: true })
15
+ })
16
+
17
+ it("copies plugin files and writes package.json", async () => {
18
+ const { writePluginBundle, writePluginPackageJson } = await import("./pluginWriter.js")
19
+
20
+ // Use temp dir as target — sandboxed, no real ~/.config/opencode writes
21
+ writePluginBundle(tempDir)
22
+
23
+ const pluginsDestDir = join(tempDir, "plugins")
24
+ const pluginFiles = ["wevr-flow.js", "wevr-squeeze.js"]
25
+ for (const f of pluginFiles) {
26
+ assert.ok(existsSync(join(pluginsDestDir, f)),
27
+ `plugin ${f} should be copied to plugins dir`)
28
+ }
29
+
30
+ // Clean plugins dir to test package.json separately
31
+ rmSync(pluginsDestDir, { recursive: true })
32
+
33
+ writePluginPackageJson(tempDir)
34
+
35
+ const pkgPath = join(tempDir, "package.json")
36
+ assert.ok(existsSync(pkgPath), "package.json should exist in temp dir")
37
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"))
38
+ assert.strictEqual(pkg.type, "module")
39
+ assert.ok(pkg.dependencies["@opencode-ai/plugin"])
40
+ })
41
+ })
@@ -0,0 +1,13 @@
1
+ import { mkdirSync, cpSync, existsSync } from "node:fs"
2
+ import { resolve } from "node:path"
3
+ import { getSkillsDir } from "./paths.js"
4
+
5
+ export function writeSkills(templatesDir, destDir) {
6
+ const sourcePath = resolve(templatesDir, "skills")
7
+ const targetDir = destDir || getSkillsDir()
8
+
9
+ mkdirSync(targetDir, { recursive: true })
10
+ if (existsSync(sourcePath)) {
11
+ cpSync(sourcePath, targetDir, { recursive: true })
12
+ }
13
+ }
@@ -0,0 +1,30 @@
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 { writeSkills } from "./skillsWriter.js"
7
+
8
+ describe("skillsWriter", () => {
9
+ const destDir = mkdtempSync(join(tmpdir(), "wevr-skills-dest-"))
10
+
11
+ after(() => {
12
+ if (existsSync(destDir)) rmSync(destDir, { recursive: true })
13
+ })
14
+
15
+ it("copies skills directories recursively to destination directory", () => {
16
+ const tmpSrcDir = mkdtempSync(join(tmpdir(), "wevr-skills-src-"))
17
+ const srcSkillsDir = join(tmpSrcDir, "skills")
18
+ const testSkillDir = join(srcSkillsDir, "test-skill")
19
+ mkdirSync(testSkillDir, { recursive: true })
20
+ writeFileSync(join(testSkillDir, "SKILL.md"), "mock-skill-content", "utf-8")
21
+
22
+ writeSkills(tmpSrcDir, destDir)
23
+
24
+ const destPath = join(destDir, "test-skill", "SKILL.md")
25
+ assert.ok(existsSync(destPath), "SKILL.md should be copied recursively")
26
+ assert.strictEqual(readFileSync(destPath, "utf-8"), "mock-skill-content")
27
+
28
+ rmSync(tmpSrcDir, { recursive: true })
29
+ })
30
+ })
@@ -0,0 +1,26 @@
1
+ import { mkdirSync, cpSync, existsSync, readdirSync, unlinkSync } from "node:fs"
2
+ import { resolve } from "node:path"
3
+ import { getThemesDir } from "./paths.js"
4
+
5
+ export function writeThemes(templatesDir, destDir) {
6
+ const sourcePath = resolve(templatesDir, "themes")
7
+ const targetDir = destDir || getThemesDir()
8
+
9
+ // Clear any existing .json files in targetDir to prevent stale themes
10
+ if (existsSync(targetDir)) {
11
+ try {
12
+ for (const file of readdirSync(targetDir)) {
13
+ if (file.endsWith(".json")) {
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 { writeThemes } from "./themeWriter.js"
7
+
8
+ describe("themeWriter", () => {
9
+ const destDir = mkdtempSync(join(tmpdir(), "wevr-theme-dest-"))
10
+
11
+ after(() => {
12
+ if (existsSync(destDir)) rmSync(destDir, { recursive: true })
13
+ })
14
+
15
+ it("copies theme file to destination directory", () => {
16
+ const tmpSrcDir = mkdtempSync(join(tmpdir(), "wevr-theme-src-"))
17
+ const srcThemesDir = join(tmpSrcDir, "themes")
18
+ mkdirSync(srcThemesDir, { recursive: true })
19
+ writeFileSync(join(srcThemesDir, "wevr-dark.json"), "mock-theme-content", "utf-8")
20
+
21
+ writeThemes(tmpSrcDir, destDir)
22
+
23
+ const destPath = join(destDir, "wevr-dark.json")
24
+ assert.ok(existsSync(destPath), "theme file should be copied")
25
+ assert.strictEqual(readFileSync(destPath, "utf-8"), "mock-theme-content")
26
+
27
+ rmSync(tmpSrcDir, { recursive: true })
28
+ })
29
+ })
@@ -0,0 +1,22 @@
1
+ import { readFileSync, writeFileSync, existsSync } from "node:fs"
2
+ import { getTuiConfigPath } from "./paths.js"
3
+
4
+ export function writeTuiConfig(themeName, customPath) {
5
+ const path = customPath || getTuiConfigPath()
6
+ let config = {
7
+ "$schema": "https://opencode.ai/tui.json"
8
+ }
9
+
10
+ if (existsSync(path)) {
11
+ try {
12
+ const content = readFileSync(path, "utf-8")
13
+ config = JSON.parse(content)
14
+ } catch {
15
+ // If parsing fails, fall back to default structure
16
+ }
17
+ }
18
+
19
+ config.theme = themeName
20
+
21
+ writeFileSync(path, JSON.stringify(config, null, 2), "utf-8")
22
+ }