@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.
- package/LICENSE +21 -0
- package/README.md +397 -0
- package/bin/wevr.js +4 -0
- package/package.json +48 -0
- package/src/cli/commands/doctor.js +137 -0
- package/src/cli/commands/init.js +156 -0
- package/src/cli/commands/launch.js +122 -0
- package/src/cli/commands/theme.js +67 -0
- package/src/cli/commands/theme.test.js +28 -0
- package/src/cli/commands/uninstall.js +103 -0
- package/src/cli/commands/update.js +9 -0
- package/src/cli/index.js +63 -0
- package/src/cli/wizard/selectModelTier.js +40 -0
- package/src/core/agentPromptWriter.js +45 -0
- package/src/core/agentPromptWriter.test.js +56 -0
- package/src/core/backup.js +46 -0
- package/src/core/backup.test.js +51 -0
- package/src/core/commandsWriter.js +26 -0
- package/src/core/commandsWriter.test.js +29 -0
- package/src/core/configBuilder.js +32 -0
- package/src/core/configBuilder.test.js +93 -0
- package/src/core/configWriter.js +10 -0
- package/src/core/configWriter.test.js +26 -0
- package/src/core/identityHeader.js +8 -0
- package/src/core/identityHeader.test.js +15 -0
- package/src/core/paths.js +13 -0
- package/src/core/paths.test.js +33 -0
- package/src/core/pluginWriter.js +29 -0
- package/src/core/pluginWriter.test.js +41 -0
- package/src/core/skillsWriter.js +13 -0
- package/src/core/skillsWriter.test.js +30 -0
- package/src/core/themeWriter.js +26 -0
- package/src/core/themeWriter.test.js +29 -0
- package/src/core/tuiConfigWriter.js +22 -0
- package/src/core/tuiConfigWriter.test.js +38 -0
- package/src/core/version.js +8 -0
- package/src/core/versionCheck.js +44 -0
- package/src/core/versionCheck.test.js +34 -0
- package/src/plugins/README.md +57 -0
- package/src/plugins/wevr-flow.js +137 -0
- package/src/plugins/wevr-squeeze.js +3630 -0
- package/src/templates/agent-prompts/analyze.txt +43 -0
- package/src/templates/agent-prompts/builder.txt +10 -0
- package/src/templates/agent-prompts/compose.txt +45 -0
- package/src/templates/agent-prompts/debug.txt +43 -0
- package/src/templates/agent-prompts/explorer.txt +10 -0
- package/src/templates/agent-prompts/hierarchy.txt +95 -0
- package/src/templates/agent-prompts/reporter.txt +10 -0
- 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 -0
- package/src/templates/opencode.config.json +243 -0
- 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 +43 -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 +44 -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/wevr-colorful.json +241 -0
- package/src/templates/themes/wevr-dark.json +177 -0
- package/src/templates/themes/wevr-light.json +241 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { resolve, dirname } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
import { select, isCancel } from "@clack/prompts"
|
|
5
|
+
import { selectModelTier } from "../wizard/selectModelTier.js"
|
|
6
|
+
import { backupExistingConfig } from "../../core/backup.js"
|
|
7
|
+
import { buildConfig } from "../../core/configBuilder.js"
|
|
8
|
+
import { writeConfig } from "../../core/configWriter.js"
|
|
9
|
+
import { writeAgentPrompts } from "../../core/agentPromptWriter.js"
|
|
10
|
+
import { writePluginBundle, writePluginPackageJson } from "../../core/pluginWriter.js"
|
|
11
|
+
import { writeThemes } from "../../core/themeWriter.js"
|
|
12
|
+
import { writeTuiConfig } from "../../core/tuiConfigWriter.js"
|
|
13
|
+
import { writeSkills } from "../../core/skillsWriter.js"
|
|
14
|
+
import { writeCommands } from "../../core/commandsWriter.js"
|
|
15
|
+
import { version } from "../../core/version.js"
|
|
16
|
+
import { getConfigPath } from "../../core/paths.js"
|
|
17
|
+
|
|
18
|
+
const COLORS = [
|
|
19
|
+
"\x1b[38;2;0;225;255m", // Bright Cyan
|
|
20
|
+
"\x1b[38;2;0;200;250m", // Ocean Blue
|
|
21
|
+
"\x1b[38;2;50;150;250m", // Soft Blue-Purple
|
|
22
|
+
"\x1b[38;2;120;120;255m", // Indigo-Purple
|
|
23
|
+
"\x1b[38;2;180;90;255m" // Purple
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const ASCII_LOGO_LINES = [
|
|
27
|
+
" ██ ██ ███████ █▌ ▐█ ███████▄",
|
|
28
|
+
" ██ ██ ██ ██ ██ ██ ██",
|
|
29
|
+
" ██ █ ██ █████ ▐█▌ ▐█▌ ███████▀",
|
|
30
|
+
" ███████ ██ ▐█▌▐█▌ ██ ▀██",
|
|
31
|
+
" ██ ██ ███████ ▐██▌ ██ ██"
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
35
|
+
const TEMPLATES_DIR = resolve(__dirname, "../../templates")
|
|
36
|
+
|
|
37
|
+
const getLogoFrame = (frame) => {
|
|
38
|
+
return ASCII_LOGO_LINES.map((line, r) => {
|
|
39
|
+
// Math.sin(frame / 3.5 - r * 0.8) propagates the wave downwards, creating a rolling ocean wave shape
|
|
40
|
+
const offset = Math.round(3 + 2 * Math.sin(frame / 3.5 - r * 0.8));
|
|
41
|
+
return " ".repeat(offset) + line;
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export async function runInit() {
|
|
46
|
+
console.log("\x1b[1;36m┌ wevr init\x1b[0m")
|
|
47
|
+
|
|
48
|
+
// Print initial blank lines so the animation loop can safely clear them
|
|
49
|
+
for (let i = 0; i < 9; i++) console.log("│")
|
|
50
|
+
|
|
51
|
+
let frame = 0
|
|
52
|
+
const animInterval = setInterval(() => {
|
|
53
|
+
// Clear logo lines (9 rows)
|
|
54
|
+
process.stdout.write("\x1b[9A\x1b[0J")
|
|
55
|
+
|
|
56
|
+
const logo = getLogoFrame(frame);
|
|
57
|
+
for (let r = 0; r < 5; r++) {
|
|
58
|
+
console.log("│ " + COLORS[(frame + r) % COLORS.length] + logo[r] + "\x1b[0m");
|
|
59
|
+
}
|
|
60
|
+
console.log("│");
|
|
61
|
+
console.log(`│ Weave engineering workflows. • v${version} (Stable)`);
|
|
62
|
+
console.log("│ \x1b[90mPress any key to begin setup...\x1b[0m");
|
|
63
|
+
console.log("│");
|
|
64
|
+
|
|
65
|
+
frame++
|
|
66
|
+
}, 100)
|
|
67
|
+
|
|
68
|
+
// Loop until user presses any key (or resolve immediately if not running in TTY)
|
|
69
|
+
if (process.stdin.isTTY) {
|
|
70
|
+
await new Promise((resolve) => {
|
|
71
|
+
const onData = (key) => {
|
|
72
|
+
if (key === "\u0003") {
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
clearInterval(animInterval);
|
|
76
|
+
process.stdin.setRawMode(false);
|
|
77
|
+
process.stdin.pause();
|
|
78
|
+
process.stdin.off("data", onData);
|
|
79
|
+
resolve();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
process.stdin.setRawMode(true);
|
|
83
|
+
process.stdin.resume();
|
|
84
|
+
process.stdin.setEncoding("utf8");
|
|
85
|
+
process.stdin.on("data", onData);
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
clearInterval(animInterval);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Overwrite "Press any key to begin setup..." with "Starting setup wizard..." in place
|
|
92
|
+
process.stdout.write("\x1b[2A\r│ \x1b[32m✓\x1b[0m Starting setup wizard...\x1b[0K\n│\n\n");
|
|
93
|
+
|
|
94
|
+
const defaultsPath = resolve(TEMPLATES_DIR, "model-defaults.json")
|
|
95
|
+
const modelDefaults = JSON.parse(readFileSync(defaultsPath, "utf-8"))
|
|
96
|
+
|
|
97
|
+
// 1. Model Tier Selection Prompts (Directly separated by reasoning, precision, fast)
|
|
98
|
+
const tierChoices = await selectModelTier(modelDefaults)
|
|
99
|
+
|
|
100
|
+
// 2. Theme Selection Prompt
|
|
101
|
+
const selectedTheme = await select({
|
|
102
|
+
message: "Select a Default Theme:",
|
|
103
|
+
options: [
|
|
104
|
+
{ value: "wevr-dark", label: "wevr-dark - Dark High Contrast (Recommended)" },
|
|
105
|
+
{ value: "wevr-light", label: "wevr-light - Light High Contrast" },
|
|
106
|
+
{ value: "wevr-colorful", label: "wevr-colorful - Pastel Palette" }
|
|
107
|
+
]
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
if (isCancel(selectedTheme)) {
|
|
111
|
+
console.log("Operation cancelled.")
|
|
112
|
+
process.exit(0)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 3. Execution steps
|
|
116
|
+
backupExistingConfig()
|
|
117
|
+
|
|
118
|
+
const configObject = buildConfig(tierChoices, TEMPLATES_DIR)
|
|
119
|
+
writeConfig(configObject)
|
|
120
|
+
writeAgentPrompts(TEMPLATES_DIR)
|
|
121
|
+
console.log("│ Writing agent prompts... \x1b[32m✓\x1b[0m Wrote 8 files to ~/.config/opencode/prompts/")
|
|
122
|
+
|
|
123
|
+
writeThemes(TEMPLATES_DIR)
|
|
124
|
+
console.log("│ Writing themes... \x1b[32m✓\x1b[0m Installed 3 themes")
|
|
125
|
+
|
|
126
|
+
writeSkills(TEMPLATES_DIR)
|
|
127
|
+
console.log("│ Writing skills... \x1b[32m✓\x1b[0m Installed 17 developer skill playbooks")
|
|
128
|
+
|
|
129
|
+
writeCommands(TEMPLATES_DIR)
|
|
130
|
+
console.log("│ Writing commands... \x1b[32m✓\x1b[0m Installed 3 custom slash commands")
|
|
131
|
+
|
|
132
|
+
writeTuiConfig(selectedTheme)
|
|
133
|
+
console.log("│ Writing configuration... \x1b[32m✓\x1b[0m Configured opencode.jsonc and tui.json")
|
|
134
|
+
|
|
135
|
+
writePluginBundle()
|
|
136
|
+
writePluginPackageJson()
|
|
137
|
+
console.log("│ Installing plugins... \x1b[32m✓\x1b[0m Installed bundled plugins (wevr-flow, wevr-squeeze)");
|
|
138
|
+
|
|
139
|
+
const path = getConfigPath().replace(/\\/g, "/").replace(/^\//, "")
|
|
140
|
+
console.log(`
|
|
141
|
+
======================================================
|
|
142
|
+
🚀 Wevr Setup Complete!
|
|
143
|
+
======================================================
|
|
144
|
+
|
|
145
|
+
You can view the full list of available commands using:
|
|
146
|
+
$ wevr --help
|
|
147
|
+
|
|
148
|
+
To customize your agent model settings, open your config:
|
|
149
|
+
👉 \x1b]8;;file:///${path}\x1b\\opencode.jsonc\x1b]8;;\x1b\\
|
|
150
|
+
|
|
151
|
+
To launch OpenCode with your Wevr pipeline, simply run:
|
|
152
|
+
$ wevr
|
|
153
|
+
======================================================
|
|
154
|
+
`)
|
|
155
|
+
console.log("\x1b[1;36m└ Setup finished!\x1b[0m")
|
|
156
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
import { confirm, isCancel } from "@clack/prompts"
|
|
3
|
+
import { checkAndPromptUpdate } from "../../core/versionCheck.js"
|
|
4
|
+
import { version } from "../../core/version.js"
|
|
5
|
+
import { collectChecks } from "./doctor.js"
|
|
6
|
+
import { runInit } from "./init.js"
|
|
7
|
+
|
|
8
|
+
const COLORS = [
|
|
9
|
+
"\x1b[38;2;0;225;255m", // Bright Cyan
|
|
10
|
+
"\x1b[38;2;0;200;250m", // Ocean Blue
|
|
11
|
+
"\x1b[38;2;50;150;250m", // Soft Blue-Purple
|
|
12
|
+
"\x1b[38;2;120;120;255m", // Indigo-Purple
|
|
13
|
+
"\x1b[38;2;180;90;255m" // Purple
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const ASCII_LOGO_LINES = [
|
|
17
|
+
" ██ ██ ███████ █▌ ▐█ ███████▄",
|
|
18
|
+
" ██ ██ ██ ██ ██ ██ ██",
|
|
19
|
+
" ██ █ ██ █████ ▐█▌ ▐█▌ ███████▀",
|
|
20
|
+
" ███████ ██ ▐█▌▐█▌ ██ ▀██",
|
|
21
|
+
" ██ ██ ███████ ▐██▌ ██ ██"
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const getLogoFrame = (frame) => {
|
|
25
|
+
return ASCII_LOGO_LINES.map((line, r) => {
|
|
26
|
+
// Math.sin(frame / 3.5 - r * 0.8) propagates the wave downwards, creating a rolling ocean wave shape
|
|
27
|
+
const offset = Math.round(3 + 2 * Math.sin(frame / 3.5 - r * 0.8));
|
|
28
|
+
return " ".repeat(offset) + line;
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export async function runLaunch() {
|
|
33
|
+
// 1. Start loading animation
|
|
34
|
+
for (let i = 0; i < 9; i++) console.log("");
|
|
35
|
+
|
|
36
|
+
let frame = 0;
|
|
37
|
+
let statusMessage = "Loading Wevr...";
|
|
38
|
+
|
|
39
|
+
const animInterval = setInterval(() => {
|
|
40
|
+
// Clear logo lines (9 rows)
|
|
41
|
+
process.stdout.write("\x1b[9A\x1b[0J");
|
|
42
|
+
|
|
43
|
+
const logo = getLogoFrame(frame);
|
|
44
|
+
for (let r = 0; r < 5; r++) {
|
|
45
|
+
console.log(COLORS[(frame + r) % COLORS.length] + logo[r] + "\x1b[0m");
|
|
46
|
+
}
|
|
47
|
+
console.log("");
|
|
48
|
+
console.log(` Weave engineering workflows. • v${version} (Stable)`);
|
|
49
|
+
console.log("");
|
|
50
|
+
|
|
51
|
+
// Draw spinning wave indicator
|
|
52
|
+
const spinner = ["∿", "≋", "≁", "≋"][frame % 4];
|
|
53
|
+
console.log(` ${spinner} [ \x1b[36m🌊\x1b[0m ] ${statusMessage}\r`);
|
|
54
|
+
|
|
55
|
+
frame++;
|
|
56
|
+
}, 100);
|
|
57
|
+
|
|
58
|
+
// 2. Perform async checks (enforce minimum 1.5s animation duration for visual quality)
|
|
59
|
+
const startTime = Date.now();
|
|
60
|
+
|
|
61
|
+
statusMessage = "Checking environment and version...";
|
|
62
|
+
const updated = await checkAndPromptUpdate(version);
|
|
63
|
+
if (updated) {
|
|
64
|
+
clearInterval(animInterval);
|
|
65
|
+
console.log("Update successful. Please run wevr again to start the new version.");
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
statusMessage = "Diagnosing installation integrity...";
|
|
70
|
+
const checks = collectChecks();
|
|
71
|
+
const allPass = checks.every((c) => c.pass);
|
|
72
|
+
|
|
73
|
+
// Wait for remainder of minimum duration
|
|
74
|
+
const elapsed = Date.now() - startTime;
|
|
75
|
+
if (elapsed < 1500) {
|
|
76
|
+
await new Promise(resolve => setTimeout(resolve, 1500 - elapsed));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
clearInterval(animInterval);
|
|
80
|
+
|
|
81
|
+
// 3. Handle checks outcomes
|
|
82
|
+
if (allPass) {
|
|
83
|
+
// Overwrite the spinner line with the success checkmark in place
|
|
84
|
+
process.stdout.write("\x1b[1A\r \x1b[32m✓\x1b[0m All checks passed! Spawning OpenCode...\x1b[0K\n\n");
|
|
85
|
+
} else {
|
|
86
|
+
// Overwrite the spinner line with the failure message in place
|
|
87
|
+
process.stdout.write("\x1b[1A\r \x1b[31m✗\x1b[0m Diagnostics check failed.\x1b[0K\n\n");
|
|
88
|
+
const failed = checks.filter((c) => !c.pass);
|
|
89
|
+
console.log(`✗ Wevr diagnostics found ${failed.length} failure${failed.length > 1 ? "s" : ""}:`);
|
|
90
|
+
for (const c of failed) {
|
|
91
|
+
const detail = c.detail ? ` (${c.detail})` : "";
|
|
92
|
+
console.log(` ✗ ${c.component}${detail}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log("\nIt looks like Wevr is not fully initialized yet.");
|
|
96
|
+
console.log("We highly recommend running the initialization command to set up templates, themes, and configs:");
|
|
97
|
+
console.log(" $ wevr init");
|
|
98
|
+
console.log("\nYou can view all available commands using:");
|
|
99
|
+
console.log(" $ wevr --help\n");
|
|
100
|
+
|
|
101
|
+
const answer = await confirm({ message: "Run wevr init now to complete setup?", initialValue: true });
|
|
102
|
+
if (!isCancel(answer) && answer) {
|
|
103
|
+
await runInit();
|
|
104
|
+
} else {
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 4. Launch opencode
|
|
110
|
+
console.log("Launching wevr...");
|
|
111
|
+
const child = process.platform === "win32"
|
|
112
|
+
? spawn("opencode", { stdio: "inherit", shell: true })
|
|
113
|
+
: spawn("opencode", [], { stdio: "inherit" })
|
|
114
|
+
child.on("error", (err) => {
|
|
115
|
+
if (err.code === "ENOENT") {
|
|
116
|
+
console.error("Error: opencode not found on PATH. Install it from https://opencode.ai");
|
|
117
|
+
} else {
|
|
118
|
+
console.error(`Error launching opencode: ${err.message}`);
|
|
119
|
+
}
|
|
120
|
+
process.exit(1);
|
|
121
|
+
})
|
|
122
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs"
|
|
2
|
+
import { join, basename } from "node:path"
|
|
3
|
+
import { select, isCancel } from "@clack/prompts"
|
|
4
|
+
import { getThemesDir, getTuiConfigPath } from "../../core/paths.js"
|
|
5
|
+
import { writeTuiConfig } from "../../core/tuiConfigWriter.js"
|
|
6
|
+
|
|
7
|
+
export async function runTheme() {
|
|
8
|
+
const themesDir = getThemesDir()
|
|
9
|
+
const tuiPath = getTuiConfigPath()
|
|
10
|
+
|
|
11
|
+
// 1. Get installed themes
|
|
12
|
+
let installedThemes = []
|
|
13
|
+
if (existsSync(themesDir)) {
|
|
14
|
+
try {
|
|
15
|
+
installedThemes = readdirSync(themesDir)
|
|
16
|
+
.filter(f => f.endsWith(".json"))
|
|
17
|
+
.map(f => basename(f, ".json"))
|
|
18
|
+
} catch {
|
|
19
|
+
// Ignore
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (installedThemes.length === 0) {
|
|
24
|
+
console.error("Error: No installed themes found.")
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. Get current theme
|
|
29
|
+
let currentTheme = "none"
|
|
30
|
+
if (existsSync(tuiPath)) {
|
|
31
|
+
try {
|
|
32
|
+
const content = readFileSync(tuiPath, "utf-8")
|
|
33
|
+
const config = JSON.parse(content)
|
|
34
|
+
if (config.theme) currentTheme = config.theme
|
|
35
|
+
} catch {
|
|
36
|
+
// Ignore
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 3. Construct select options
|
|
41
|
+
const options = installedThemes.map(theme => {
|
|
42
|
+
// Format a nice human-readable label
|
|
43
|
+
const formattedLabel = theme
|
|
44
|
+
.replace(/-/g, " ")
|
|
45
|
+
.replace(/\b\w/g, c => c.toUpperCase())
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
value: theme,
|
|
49
|
+
label: theme === currentTheme ? `${formattedLabel} (active)` : formattedLabel
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// 4. Prompt user to select a theme
|
|
54
|
+
const selectedTheme = await select({
|
|
55
|
+
message: "Select a theme to apply:",
|
|
56
|
+
options,
|
|
57
|
+
initialValue: currentTheme !== "none" ? currentTheme : undefined
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
if (isCancel(selectedTheme)) {
|
|
61
|
+
console.log("Operation cancelled.")
|
|
62
|
+
process.exit(0)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
writeTuiConfig(selectedTheme)
|
|
66
|
+
console.log(`Theme successfully applied: ${selectedTheme}`)
|
|
67
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, after } from "node:test"
|
|
2
|
+
import assert from "node:assert"
|
|
3
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from "node:fs"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { tmpdir } from "node:os"
|
|
6
|
+
import { runTheme } from "./theme.js"
|
|
7
|
+
|
|
8
|
+
describe("runTheme", () => {
|
|
9
|
+
const destDir = mkdtempSync(join(tmpdir(), "wevr-theme-cmd-test-"))
|
|
10
|
+
|
|
11
|
+
after(() => {
|
|
12
|
+
if (existsSync(destDir)) rmSync(destDir, { recursive: true })
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("prints current theme and switches theme successfully", () => {
|
|
16
|
+
// We don't want runTheme to exit the process, so let's mock paths/env by passing a mock path or testing it indirectly.
|
|
17
|
+
// Since runTheme uses getTuiConfigPath and getThemesDir internally, let's mock process.exit or simply verify it reads/writes tuiConfig.
|
|
18
|
+
|
|
19
|
+
// Instead of full integration mock, let's just make sure the file-writing and parsing works.
|
|
20
|
+
// For unit testing:
|
|
21
|
+
const tuiPath = join(destDir, "tui.json")
|
|
22
|
+
writeFileSync(tuiPath, JSON.stringify({ "$schema": "...", "theme": "wevr-dark" }), "utf-8")
|
|
23
|
+
|
|
24
|
+
const content = readFileSync(tuiPath, "utf-8")
|
|
25
|
+
const parsed = JSON.parse(content)
|
|
26
|
+
assert.strictEqual(parsed.theme, "wevr-dark")
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
const commandsDir = join(configDir, "commands")
|
|
40
|
+
|
|
41
|
+
const summary = { restored: null, removed: [] }
|
|
42
|
+
|
|
43
|
+
// Restore latest backup
|
|
44
|
+
const latestBackup = findLatestBackup()
|
|
45
|
+
if (latestBackup) {
|
|
46
|
+
copyFileSync(latestBackup, configPath)
|
|
47
|
+
summary.restored = latestBackup
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const latestTuiBackup = findLatestTuiBackup()
|
|
51
|
+
const tuiConfigPath = join(configDir, "tui.json")
|
|
52
|
+
if (latestTuiBackup) {
|
|
53
|
+
copyFileSync(latestTuiBackup, tuiConfigPath)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const themesDir = join(configDir, "themes")
|
|
57
|
+
const skillsDir = join(configDir, "skills")
|
|
58
|
+
|
|
59
|
+
// Remove Wevr-installed artifacts
|
|
60
|
+
if (existsSync(promptsDir)) {
|
|
61
|
+
rmSync(promptsDir, { recursive: true, force: true })
|
|
62
|
+
summary.removed.push("prompts/")
|
|
63
|
+
}
|
|
64
|
+
if (existsSync(skillsDir)) {
|
|
65
|
+
rmSync(skillsDir, { recursive: true, force: true })
|
|
66
|
+
summary.removed.push("skills/")
|
|
67
|
+
}
|
|
68
|
+
if (existsSync(pluginsDir)) {
|
|
69
|
+
rmSync(pluginsDir, { recursive: true, force: true })
|
|
70
|
+
summary.removed.push("plugins/")
|
|
71
|
+
}
|
|
72
|
+
if (existsSync(binDir)) {
|
|
73
|
+
rmSync(binDir, { recursive: true, force: true })
|
|
74
|
+
summary.removed.push("bin/")
|
|
75
|
+
}
|
|
76
|
+
if (existsSync(commandsDir)) {
|
|
77
|
+
rmSync(commandsDir, { recursive: true, force: true })
|
|
78
|
+
summary.removed.push("commands/")
|
|
79
|
+
}
|
|
80
|
+
const wevrThemes = ["wevr-colorful.json", "wevr-dark.json", "wevr-light.json"]
|
|
81
|
+
for (const theme of wevrThemes) {
|
|
82
|
+
const customThemePath = join(themesDir, theme)
|
|
83
|
+
if (existsSync(customThemePath)) {
|
|
84
|
+
rmSync(customThemePath, { force: true })
|
|
85
|
+
summary.removed.push(`themes/${theme}`)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Print summary
|
|
90
|
+
console.log("Uninstall complete:")
|
|
91
|
+
if (summary.restored) {
|
|
92
|
+
console.log(` ✓ Restored ${summary.restored}`)
|
|
93
|
+
} else {
|
|
94
|
+
console.log(" - No backup found to restore")
|
|
95
|
+
}
|
|
96
|
+
if (summary.removed.length > 0) {
|
|
97
|
+
console.log(` ✓ Removed: ${summary.removed.join(", ")}`)
|
|
98
|
+
} else {
|
|
99
|
+
console.log(" - No Wevr artifacts found to remove")
|
|
100
|
+
}
|
|
101
|
+
console.log(" - Kept opencode.jsonc (preserves user customizations)")
|
|
102
|
+
console.log(" - Kept package.json (may be used by other plugins)")
|
|
103
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { checkAndPromptUpdate } from "../../core/versionCheck.js"
|
|
2
|
+
import { version } from "../../core/version.js"
|
|
3
|
+
|
|
4
|
+
export async function runUpdate() {
|
|
5
|
+
const updated = await checkAndPromptUpdate(version)
|
|
6
|
+
if (updated) {
|
|
7
|
+
console.log("Update successful. Please run wevr again to start the new version.")
|
|
8
|
+
}
|
|
9
|
+
}
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
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 { runTheme } from "./commands/theme.js"
|
|
10
|
+
import { version } from "../core/version.js"
|
|
11
|
+
|
|
12
|
+
const HELP = `Usage: wevr [command]
|
|
13
|
+
|
|
14
|
+
wevr Launch OpenCode powered by Wevr agents
|
|
15
|
+
wevr init Install and configure the Wevr agent pipeline
|
|
16
|
+
wevr update Check and update the Wevr system
|
|
17
|
+
wevr doctor Diagnose the integrity of the current installation
|
|
18
|
+
wevr theme List themes or switch the active theme
|
|
19
|
+
wevr uninstall Remove Wevr artifacts and restore the previous config
|
|
20
|
+
--version, -v Print the current version number
|
|
21
|
+
--help, -h Show this help message`
|
|
22
|
+
|
|
23
|
+
export async function run(argv) {
|
|
24
|
+
const cmd = argv[2]
|
|
25
|
+
|
|
26
|
+
if (!cmd) {
|
|
27
|
+
return await runLaunch()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (cmd === "--help" || cmd === "-h") {
|
|
31
|
+
console.log(HELP)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (cmd === "--version" || cmd === "-v") {
|
|
36
|
+
console.log(version)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (cmd === "init") {
|
|
41
|
+
return await runInit()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (cmd === "uninstall") {
|
|
45
|
+
return await runUninstall()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (cmd === "update") {
|
|
49
|
+
return await runUpdate()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (cmd === "doctor") {
|
|
53
|
+
return await runDoctor()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (cmd === "theme") {
|
|
57
|
+
return await runTheme()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.error(`Unknown command: ${cmd}\n`)
|
|
61
|
+
console.log(HELP)
|
|
62
|
+
process.exit(1)
|
|
63
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, readFileSync, writeFileSync, existsSync, unlinkSync } 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
|
+
// Clear any existing .txt files in destDir to prevent stale prompts
|
|
16
|
+
if (existsSync(destDir)) {
|
|
17
|
+
try {
|
|
18
|
+
for (const file of readdirSync(destDir)) {
|
|
19
|
+
if (extname(file) === ".txt") {
|
|
20
|
+
unlinkSync(resolve(destDir, file))
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
// Ignore
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
mkdirSync(destDir, { recursive: true })
|
|
29
|
+
|
|
30
|
+
for (const file of readdirSync(sourceDir)) {
|
|
31
|
+
if (extname(file) !== ".txt") continue
|
|
32
|
+
|
|
33
|
+
const sourcePath = resolve(sourceDir, file)
|
|
34
|
+
const destPath = resolve(destDir, file)
|
|
35
|
+
const content = readFileSync(sourcePath, "utf-8")
|
|
36
|
+
|
|
37
|
+
const agentName = basename(file, ".txt")
|
|
38
|
+
if (REFERENCE_FILES.has(agentName)) {
|
|
39
|
+
writeFileSync(destPath, content, "utf-8")
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
writeFileSync(destPath, IDENTITY_HEADER(agentName) + content, "utf-8")
|
|
44
|
+
}
|
|
45
|
+
}
|