use std::path::{Path, PathBuf};
use crate::error::Result;
pub fn generate_opencode_plugin(sqz_path: &str) -> String {
let sqz_path = crate::tool_hooks::json_escape_string_value(sqz_path);
format!(
r#"/**
* sqz — OpenCode plugin for transparent context compression.
*
* Intercepts shell commands and pipes output through sqz for token savings.
* Install: copy to ~/.config/opencode/plugins/sqz.ts
* Discovery is automatic — no opencode.json entry needed (and in fact
* including one causes the plugin to load twice, per issue #10).
*/
const SqzPluginFactory = async (ctx: any) => {{
const SQZ_PATH = "{sqz_path}";
// Commands that should not be intercepted.
const INTERACTIVE = new Set([
"vim", "vi", "nano", "emacs", "less", "more", "top", "htop",
"ssh", "python", "python3", "node", "irb", "ghci",
"psql", "mysql", "sqlite3", "mongo", "redis-cli",
]);
function isInteractive(cmd: string): boolean {{
const base = cmd.split(/\s+/)[0]?.split("/").pop() ?? "";
if (INTERACTIVE.has(base)) return true;
if (cmd.includes("--watch") || cmd.includes("run dev") ||
cmd.includes("run start") || cmd.includes("run serve")) return true;
return false;
}}
function shouldIntercept(tool: string): boolean {{
return ["bash", "shell", "terminal", "run_shell_command"].includes(tool.toLowerCase());
}}
// Detect that a command has already been wrapped by sqz. Before this
// guard was in place OpenCode could call the hook twice on the same
// command (for retried tool calls, or when a previous rewrite was
// echoed back to the agent and the agent re-submitted it) and each
// pass would prepend another `SQZ_CMD=$base` prefix, producing monsters
// like `SQZ_CMD=SQZ_CMD=ddev SQZ_CMD=ddev ddev exec ...` (reported as
// a follow-up to issue #5). We skip if any of these markers appear:
// * the case-insensitive substring "sqz_cmd=" or "sqz compress"
// (covers the tail of prior wraps regardless of case; SQZ_CMD= is
// legacy pre-issue-#10 but still valid in POSIX shell hooks)
// * a leading `VAR=` assignment that starts with SQZ_
// (defensive catch-all for exotic wrap variants)
// * the base command itself is sqz or sqz-mcp (running sqz directly
// — compressing sqz's own output is pointless and causes loops)
function isAlreadyWrapped(cmd: string): boolean {{
const lowered = cmd.toLowerCase();
if (lowered.includes("sqz_cmd=")) return true;
if (lowered.includes("sqz compress")) return true;
if (lowered.includes("| sqz ") || lowered.includes("| sqz\t")) return true;
if (/^\s*SQZ_[A-Z0-9_]+=/.test(cmd)) return true;
const base = extractBaseCmd(cmd);
if (base === "sqz" || base === "sqz-mcp" || base === "sqz.exe") return true;
return false;
}}
// Extract the base command name defensively. If the command has
// leading env-var assignments (VAR=val VAR2=val2 actual_cmd arg1),
// skip past them so the base is `actual_cmd` — not `VAR=val`.
function extractBaseCmd(cmd: string): string {{
const tokens = cmd.split(/\s+/).filter(t => t.length > 0);
for (const tok of tokens) {{
// A token is an env assignment if it matches NAME=VALUE where NAME
// is a valid env var identifier. Skip it and keep looking.
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(tok)) continue;
return tok.split("/").pop() ?? "unknown";
}}
return "unknown";
}}
// Shell-escape a command-name label so it's safe to inline into the
// rewritten shell command. Agents occasionally invoke commands via
// paths with spaces (`"/my tools/foo" --arg`) and in the LLM
// roundtrip that can survive to `extractBaseCmd`'s output. Quote the
// label unless it's pure ASCII alphanumeric.
function shellEscapeLabel(s: string): string {{
if (/^[A-Za-z0-9_.-]+$/.test(s)) return s;
return "'" + s.replace(/'/g, "'\\''") + "'";
}}
return {{
"tool.execute.before": async (input: any, output: any) => {{
const tool = input.tool ?? "";
if (!shouldIntercept(tool)) return;
const cmd = output.args?.command ?? "";
if (!cmd || isAlreadyWrapped(cmd) || isInteractive(cmd)) return;
// Rewrite: pipe through `sqz compress --cmd <base>`.
//
// Issue #10: the previous form was `SQZ_CMD=<base> <cmd> 2>&1 |
// <sqz> compress`, which uses sh-specific inline env-var syntax.
// On Windows, OpenCode Desktop routes bash-tool commands through
// PowerShell (or cmd.exe when $SHELL is unset), and both parse
// `SQZ_CMD=cmd` as a command name — raising CommandNotFoundException
// and producing zero compression. `--cmd NAME` is a normal CLI
// argument, shell-neutral, works in POSIX sh, zsh, fish, PowerShell,
// and cmd.exe.
const base = extractBaseCmd(cmd);
const label = shellEscapeLabel(base);
output.args.command = `${{cmd}} 2>&1 | ${{SQZ_PATH}} compress --cmd ${{label}}`;
}},
}};
}};
// V1 default export — modern OpenCode (post-V1 loader) reads `id` here
// and displays "sqz" in the plugin list. Without this, OpenCode falls
// back to the raw `file:///...` spec as the plugin name (@itguy327 on
// issue #10). `readV1Plugin` in OpenCode's plugin/shared.ts requires
// file-source plugins to declare an id — otherwise `resolvePluginId`
// throws.
export default {{
id: "sqz",
server: SqzPluginFactory,
}};
// Legacy named export — pre-V1 OpenCode versions walk Object.values(mod)
// looking for factory functions. Assigning the same reference as the
// default export's `.server` means the legacy `seen` Set dedups via
// identity, so the factory fires exactly once either way. Kept for
// backward compatibility with OpenCode versions that predate the V1
// loader (roughly anything before mid-2025).
export const SqzPlugin = SqzPluginFactory;
"#
)
}
pub fn opencode_plugin_path() -> PathBuf {
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("."));
home.join(".config")
.join("opencode")
.join("plugins")
.join("sqz.ts")
}
pub fn install_opencode_plugin(sqz_path: &str) -> Result<bool> {
let plugin_path = opencode_plugin_path();
if plugin_path.exists() {
return Ok(false);
}
if let Some(parent) = plugin_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
crate::error::SqzError::Other(format!(
"failed to create OpenCode plugins dir {}: {e}",
parent.display()
))
})?;
}
let content = generate_opencode_plugin(sqz_path);
std::fs::write(&plugin_path, &content).map_err(|e| {
crate::error::SqzError::Other(format!(
"failed to write OpenCode plugin to {}: {e}",
plugin_path.display()
))
})?;
Ok(true)
}
pub fn find_opencode_config(project_dir: &Path) -> Option<PathBuf> {
let jsonc = project_dir.join("opencode.jsonc");
if jsonc.exists() {
return Some(jsonc);
}
let json = project_dir.join("opencode.json");
if json.exists() {
return Some(json);
}
None
}
pub fn opencode_config_has_comments(project_dir: &Path) -> bool {
let path = match find_opencode_config(project_dir) {
Some(p) => p,
None => return false,
};
if path.extension().map(|e| e != "jsonc").unwrap_or(true) {
return false;
}
let content = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(_) => return false,
};
strip_jsonc_comments(&content) != content
}
pub fn strip_jsonc_comments(src: &str) -> String {
let mut out = String::with_capacity(src.len());
let bytes = src.as_bytes();
let mut i = 0;
let len = bytes.len();
while i < len {
let b = bytes[i];
if b == b'"' {
out.push('"');
i += 1;
while i < len {
let c = bytes[i];
out.push(c as char);
if c == b'\\' && i + 1 < len {
out.push(bytes[i + 1] as char);
i += 2;
continue;
}
i += 1;
if c == b'"' {
break;
}
}
continue;
}
if b == b'/' && i + 1 < len && bytes[i + 1] == b'/' {
i += 2;
while i < len && bytes[i] != b'\n' {
i += 1;
}
continue;
}
if b == b'/' && i + 1 < len && bytes[i + 1] == b'*' {
i += 2;
while i + 1 < len && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
if bytes[i] == b'\n' {
out.push('\n');
}
i += 1;
}
if i + 1 < len {
i += 2;
}
continue;
}
out.push(b as char);
i += 1;
}
out
}
pub fn update_opencode_config(project_dir: &Path) -> Result<bool> {
let (updated, _) = update_opencode_config_detailed(project_dir)?;
Ok(updated)
}
pub fn update_opencode_config_detailed(project_dir: &Path) -> Result<(bool, bool)> {
fn sqz_mcp_value() -> serde_json::Value {
serde_json::json!({
"type": "local",
"command": ["sqz-mcp", "--transport", "stdio"]
})
}
if let Some(existing_path) = find_opencode_config(project_dir) {
let is_jsonc = existing_path
.extension()
.map(|e| e == "jsonc")
.unwrap_or(false);
let content = std::fs::read_to_string(&existing_path).map_err(|e| {
crate::error::SqzError::Other(format!(
"failed to read {}: {e}",
existing_path.display()
))
})?;
let parseable = if is_jsonc {
strip_jsonc_comments(&content)
} else {
content.clone()
};
let had_comments = is_jsonc && parseable != content;
let mut config: serde_json::Value = serde_json::from_str(&parseable).map_err(|e| {
crate::error::SqzError::Other(format!(
"failed to parse {}: {e}",
existing_path.display()
))
})?;
let obj = config.as_object_mut().ok_or_else(|| {
crate::error::SqzError::Other(format!(
"{} root is not a JSON object",
existing_path.display()
))
})?;
let mut changed = false;
if let Some(arr) = obj.get_mut("plugin").and_then(|v| v.as_array_mut()) {
let before = arr.len();
arr.retain(|v| v.as_str() != Some("sqz"));
if arr.len() != before {
changed = true;
}
if arr.is_empty() {
obj.remove("plugin");
changed = true;
}
}
let mcp_entry = obj.entry("mcp").or_insert_with(|| serde_json::json!({}));
if let Some(mcp_obj) = mcp_entry.as_object_mut() {
if !mcp_obj.contains_key("sqz") {
mcp_obj.insert("sqz".to_string(), sqz_mcp_value());
changed = true;
}
} else {
return Err(crate::error::SqzError::Other(format!(
"{} has an `mcp` field that is not an object; \
refusing to modify it automatically",
existing_path.display()
)));
}
if !changed {
return Ok((false, false));
}
let updated = serde_json::to_string_pretty(&config).map_err(|e| {
crate::error::SqzError::Other(format!("failed to serialize config: {e}"))
})?;
std::fs::write(&existing_path, format!("{updated}\n")).map_err(|e| {
crate::error::SqzError::Other(format!(
"failed to write {}: {e}",
existing_path.display()
))
})?;
Ok((true, had_comments))
} else {
let config = serde_json::json!({
"$schema": "https://opencode.ai/config.json",
"mcp": {
"sqz": sqz_mcp_value()
}
});
let content = serde_json::to_string_pretty(&config).map_err(|e| {
crate::error::SqzError::Other(format!("failed to serialize opencode.json: {e}"))
})?;
let path = project_dir.join("opencode.json");
std::fs::write(&path, format!("{content}\n")).map_err(|e| {
crate::error::SqzError::Other(format!("failed to write opencode.json: {e}"))
})?;
Ok((true, false))
}
}
pub fn remove_sqz_from_opencode_config(project_dir: &Path) -> Result<Option<(PathBuf, bool)>> {
let path = match find_opencode_config(project_dir) {
Some(p) => p,
None => return Ok(None),
};
let is_jsonc = path.extension().map(|e| e == "jsonc").unwrap_or(false);
let raw = std::fs::read_to_string(&path).map_err(|e| {
crate::error::SqzError::Other(format!("failed to read {}: {e}", path.display()))
})?;
let parseable = if is_jsonc {
strip_jsonc_comments(&raw)
} else {
raw.clone()
};
let mut config: serde_json::Value = match serde_json::from_str(&parseable) {
Ok(v) => v,
Err(_) => {
return Ok(Some((path, false)));
}
};
let mut changed = false;
if let Some(obj) = config.as_object_mut() {
if let Some(plugin) = obj.get_mut("plugin").and_then(|v| v.as_array_mut()) {
let before = plugin.len();
plugin.retain(|v| v.as_str() != Some("sqz"));
if plugin.len() != before {
changed = true;
}
if plugin.is_empty() {
obj.remove("plugin");
}
}
if let Some(mcp) = obj.get_mut("mcp").and_then(|v| v.as_object_mut()) {
if mcp.remove("sqz").is_some() {
changed = true;
}
if mcp.is_empty() {
obj.remove("mcp");
}
}
}
if !changed {
return Ok(Some((path, false)));
}
let essentially_empty = match config.as_object() {
Some(obj) => {
obj.is_empty()
|| (obj.len() == 1
&& obj.get("$schema").and_then(|v| v.as_str())
== Some("https://opencode.ai/config.json"))
}
None => false,
};
if essentially_empty {
std::fs::remove_file(&path).map_err(|e| {
crate::error::SqzError::Other(format!(
"failed to remove {}: {e}",
path.display()
))
})?;
return Ok(Some((path, true)));
}
let updated = serde_json::to_string_pretty(&config).map_err(|e| {
crate::error::SqzError::Other(format!("failed to serialize config: {e}"))
})?;
std::fs::write(&path, format!("{updated}\n")).map_err(|e| {
crate::error::SqzError::Other(format!(
"failed to write {}: {e}",
path.display()
))
})?;
Ok(Some((path, true)))
}
fn is_already_wrapped(command: &str) -> bool {
let lowered = command.to_ascii_lowercase();
if lowered.contains("sqz_cmd=") {
return true;
}
if lowered.contains("sqz compress") {
return true;
}
if lowered.contains("| sqz ") || lowered.contains("| sqz\t") {
return true;
}
let trimmed = command.trim_start();
if let Some(eq_idx) = trimmed.find('=') {
let name = &trimmed[..eq_idx];
if name.starts_with("SQZ_")
&& !name.is_empty()
&& name
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
{
return true;
}
}
let base = extract_base_cmd(command);
if base == "sqz" || base == "sqz-mcp" || base == "sqz.exe" {
return true;
}
false
}
fn extract_base_cmd(command: &str) -> &str {
for tok in command.split_whitespace() {
if is_env_assignment(tok) {
continue;
}
return tok.rsplit('/').next().unwrap_or("unknown");
}
"unknown"
}
fn is_env_assignment(token: &str) -> bool {
let eq = match token.find('=') {
Some(i) => i,
None => return false,
};
if eq == 0 {
return false;
}
let name = &token[..eq];
let mut chars = name.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
pub fn process_opencode_hook(input: &str) -> Result<String> {
let parsed: serde_json::Value = serde_json::from_str(input)
.map_err(|e| crate::error::SqzError::Other(format!("opencode hook: invalid JSON: {e}")))?;
let tool = parsed
.get("tool")
.or_else(|| parsed.get("toolName"))
.or_else(|| parsed.get("tool_name"))
.and_then(|v| v.as_str())
.unwrap_or("");
if !matches!(
tool.to_lowercase().as_str(),
"bash" | "shell" | "terminal" | "run_shell_command"
) {
return Ok(input.to_string());
}
let command = parsed
.get("args")
.or_else(|| parsed.get("toolCall"))
.or_else(|| parsed.get("tool_input"))
.and_then(|v| v.get("command"))
.and_then(|v| v.as_str())
.unwrap_or("");
if command.is_empty() || is_already_wrapped(command) {
return Ok(input.to_string());
}
let base = extract_base_cmd(command);
if matches!(
base,
"vim" | "vi" | "nano" | "emacs" | "less" | "more" | "top" | "htop"
| "ssh" | "python" | "python3" | "node" | "irb" | "ghci"
| "psql" | "mysql" | "sqlite3" | "mongo" | "redis-cli"
) || command.contains("--watch")
|| command.contains("run dev")
|| command.contains("run start")
|| command.contains("run serve")
{
return Ok(input.to_string());
}
let base_cmd = base;
let escaped_base = if base_cmd
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
{
base_cmd.to_string()
} else {
format!("'{}'", base_cmd.replace('\'', "'\\''"))
};
let rewritten = format!(
"{} 2>&1 | sqz compress --cmd {}",
command, escaped_base,
);
let output = serde_json::json!({
"decision": "approve",
"reason": "sqz: command output will be compressed for token savings",
"updatedInput": {
"command": rewritten
},
"args": {
"command": rewritten
}
});
serde_json::to_string(&output)
.map_err(|e| crate::error::SqzError::Other(format!("opencode hook: serialize error: {e}")))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_opencode_plugin_contains_sqz_path() {
let content = generate_opencode_plugin("/usr/local/bin/sqz");
assert!(content.contains("/usr/local/bin/sqz"));
assert!(content.contains("SqzPlugin"));
assert!(content.contains("tool.execute.before"));
}
#[test]
fn test_generate_opencode_plugin_windows_path_escaped() {
let windows_path = r"C:\Users\SqzUser\.cargo\bin\sqz.exe";
let content = generate_opencode_plugin(windows_path);
assert!(
content.contains(r#"const SQZ_PATH = "C:\\Users\\SqzUser\\.cargo\\bin\\sqz.exe""#),
"expected JS-escaped path in plugin — got:\n{content}"
);
assert!(
!content.contains(r#"const SQZ_PATH = "C:\U"#),
"plugin must not contain unescaped backslashes in the string literal"
);
}
#[test]
fn test_generate_opencode_plugin_has_interactive_check() {
let content = generate_opencode_plugin("sqz");
assert!(content.contains("isInteractive"));
assert!(content.contains("vim"));
assert!(content.contains("--watch"));
}
#[test]
fn test_generate_opencode_plugin_declares_v1_id() {
let content = generate_opencode_plugin("sqz");
assert!(
content.contains("id: \"sqz\""),
"plugin must default-export `id: \"sqz\"` so OpenCode's \
V1 loader (shared.ts readV1Plugin/resolvePluginId) \
displays \"sqz\" in the UI instead of the file path; \
got:\n{content}"
);
assert!(
content.contains("server: SqzPluginFactory"),
"plugin must default-export `server: <factory>` for V1 \
loader compliance; got:\n{content}"
);
assert!(
content.contains("export default {"),
"plugin must have a default export per OpenCode V1 shape; \
got:\n{content}"
);
}
#[test]
fn test_generate_opencode_plugin_legacy_named_export_preserved() {
let content = generate_opencode_plugin("sqz");
assert!(
content.contains("export const SqzPlugin = SqzPluginFactory"),
"legacy named export must alias the same factory reference \
as the V1 default export — otherwise old OpenCode versions \
would see two distinct factories in `Object.values(mod)` \
and fire the hook twice; got:\n{content}"
);
}
#[test]
fn test_process_opencode_hook_rewrites_bash() {
let input = r#"{"tool":"bash","args":{"command":"git status"}}"#;
let result = process_opencode_hook(input).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["decision"].as_str().unwrap(), "approve");
let cmd = parsed["args"]["command"].as_str().unwrap();
assert!(cmd.contains("sqz compress"), "should pipe through sqz: {cmd}");
assert!(cmd.contains("git status"), "should preserve original: {cmd}");
assert!(cmd.contains("--cmd git"), "should pass base command via --cmd: {cmd}");
assert!(
!cmd.contains("SQZ_CMD="),
"must not emit legacy sh-style env prefix: {cmd}"
);
}
#[test]
fn test_process_opencode_hook_passes_non_shell() {
let input = r#"{"tool":"read_file","args":{"path":"file.txt"}}"#;
let result = process_opencode_hook(input).unwrap();
assert_eq!(result, input, "non-shell tools should pass through");
}
#[test]
fn test_process_opencode_hook_skips_sqz_commands() {
let input = r#"{"tool":"bash","args":{"command":"sqz stats"}}"#;
let result = process_opencode_hook(input).unwrap();
assert_eq!(result, input, "sqz commands should not be double-wrapped");
}
#[test]
fn test_process_opencode_hook_skips_interactive() {
let input = r#"{"tool":"bash","args":{"command":"vim file.txt"}}"#;
let result = process_opencode_hook(input).unwrap();
assert_eq!(result, input, "interactive commands should pass through");
}
#[test]
fn test_process_opencode_hook_skips_watch() {
let input = r#"{"tool":"bash","args":{"command":"npm run dev --watch"}}"#;
let result = process_opencode_hook(input).unwrap();
assert_eq!(result, input, "watch mode should pass through");
}
#[test]
fn test_process_opencode_hook_invalid_json() {
let result = process_opencode_hook("not json");
assert!(result.is_err());
}
#[test]
fn test_process_opencode_hook_empty_command() {
let input = r#"{"tool":"bash","args":{"command":""}}"#;
let result = process_opencode_hook(input).unwrap();
assert_eq!(result, input);
}
#[test]
fn test_process_opencode_hook_run_shell_command() {
let input = r#"{"tool":"run_shell_command","args":{"command":"ls -la"}}"#;
let result = process_opencode_hook(input).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
let cmd = parsed["args"]["command"].as_str().unwrap();
assert!(cmd.contains("sqz compress"));
}
#[test]
fn test_install_opencode_plugin_creates_file() {
let dir = tempfile::tempdir().unwrap();
std::env::set_var("HOME", dir.path());
let result = install_opencode_plugin("sqz");
assert!(result.is_ok());
let plugin_path = dir
.path()
.join(".config/opencode/plugins/sqz.ts");
assert!(plugin_path.exists(), "plugin file should exist");
let content = std::fs::read_to_string(&plugin_path).unwrap();
assert!(content.contains("SqzPlugin"));
}
#[test]
fn test_update_opencode_config_creates_new() {
let dir = tempfile::tempdir().unwrap();
let result = update_opencode_config(dir.path()).unwrap();
assert!(result, "should create new config");
let config_path = dir.path().join("opencode.json");
assert!(config_path.exists());
let content = std::fs::read_to_string(&config_path).unwrap();
assert!(content.contains("\"sqz\""));
assert!(content.contains("sqz-mcp"));
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert!(
parsed.get("plugin").is_none(),
"fresh-install opencode.json must not include `plugin`; got: {content}"
);
assert_eq!(
parsed["mcp"]["sqz"]["type"].as_str(),
Some("local"),
"mcp.sqz must be present"
);
}
#[test]
fn test_update_opencode_config_adds_to_existing() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("opencode.json");
std::fs::write(
&config_path,
r#"{"$schema":"https://opencode.ai/config.json","plugin":["other"]}"#,
)
.unwrap();
let result = update_opencode_config(dir.path()).unwrap();
assert!(result, "should update existing config");
let content = std::fs::read_to_string(&config_path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
let plugins = parsed["plugin"].as_array().unwrap();
assert!(
!plugins.iter().any(|v| v.as_str() == Some("sqz")),
"issue #10: sqz must NOT be registered as a config-level plugin \
(the local plugin file at ~/.config/opencode/plugins/sqz.ts \
already loads it; double-registering causes double hook firing)"
);
assert!(
plugins.iter().any(|v| v.as_str() == Some("other")),
"pre-existing plugin entries from OTHER plugins must be preserved"
);
assert_eq!(
parsed["mcp"]["sqz"]["type"].as_str(),
Some("local"),
"mcp.sqz must be added"
);
}
#[test]
fn test_update_opencode_config_removes_legacy_sqz_plugin_entry() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("opencode.json");
std::fs::write(
&config_path,
r#"{"plugin":["other","sqz"]}"#,
)
.unwrap();
let changed = update_opencode_config(dir.path()).unwrap();
assert!(changed, "must report that the legacy plugin entry was stripped");
let after = std::fs::read_to_string(&config_path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&after).unwrap();
let plugins = parsed["plugin"].as_array().unwrap();
assert!(
!plugins.iter().any(|v| v.as_str() == Some("sqz")),
"legacy sqz plugin entry must be stripped on re-init"
);
assert!(
plugins.iter().any(|v| v.as_str() == Some("other")),
"other plugin entries must survive the cleanup"
);
}
#[test]
fn test_update_opencode_config_drops_empty_plugin_array_after_cleanup() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("opencode.json");
std::fs::write(&config_path, r#"{"plugin":["sqz"]}"#).unwrap();
update_opencode_config(dir.path()).unwrap();
let after = std::fs::read_to_string(&config_path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&after).unwrap();
assert!(
parsed.get("plugin").is_none(),
"empty plugin array should be dropped entirely, got: {after}"
);
}
#[test]
fn test_update_opencode_config_skips_if_present() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("opencode.json");
std::fs::write(
&config_path,
r#"{
"mcp": {
"sqz": {
"type": "local",
"command": ["sqz-mcp", "--transport", "stdio"]
}
}
}"#,
)
.unwrap();
let result = update_opencode_config(dir.path()).unwrap();
assert!(
!result,
"a config that already has just the mcp.sqz entry (no plugin[]) \
must be idempotent — nothing more to do"
);
}
#[test]
fn test_update_opencode_config_adds_missing_mcp_entry() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("opencode.json");
std::fs::write(&config_path, r#"{"plugin":["sqz"]}"#).unwrap();
let changed = update_opencode_config(dir.path()).unwrap();
assert!(changed, "must report that mcp.sqz was added");
let after = std::fs::read_to_string(&config_path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&after).unwrap();
assert_eq!(
parsed["mcp"]["sqz"]["type"].as_str(),
Some("local"),
"mcp.sqz must be populated with the default server entry"
);
}
#[test]
fn test_process_opencode_hook_skips_already_wrapped_sqz_cmd_prefix() {
let input = r#"{"tool":"bash","args":{"command":"SQZ_CMD=ddev ddev exec --dir=/var/www/html php -v 2>&1 | /home/user/.cargo/bin/sqz compress"}}"#;
let result = process_opencode_hook(input).unwrap();
assert_eq!(
result, input,
"already-wrapped command must pass through unchanged; \
otherwise each pass accumulates another SQZ_CMD= prefix"
);
}
#[test]
fn test_process_opencode_hook_guard_is_case_insensitive() {
let input = r#"{"tool":"bash","args":{"command":"SQZ_CMD=git git status"}}"#;
let result = process_opencode_hook(input).unwrap();
assert_eq!(
result, input,
"uppercase SQZ_CMD= prefix must short-circuit the wrap"
);
}
#[test]
fn test_process_opencode_hook_skips_leading_env_assignments_for_base() {
let input = r#"{"tool":"bash","args":{"command":"FOO=bar BAZ=qux make test"}}"#;
let result = process_opencode_hook(input).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
let cmd = parsed["args"]["command"].as_str().unwrap();
assert!(
cmd.contains("--cmd make"),
"base command must be `make`, not `FOO=bar`; got: {cmd}"
);
assert!(
cmd.contains("FOO=bar BAZ=qux make test"),
"original command must be preserved: {cmd}"
);
}
#[test]
fn test_process_opencode_hook_skips_bare_sqz_invocation() {
for cmd in ["sqz stats", "sqz gain", "/usr/local/bin/sqz compress"] {
let input = format!(
r#"{{"tool":"bash","args":{{"command":"{cmd}"}}}}"#
);
let result = process_opencode_hook(&input).unwrap();
assert_eq!(
result, input,
"sqz-invoking command `{cmd}` must not be rewrapped"
);
}
}
#[test]
fn test_generate_opencode_plugin_has_double_wrap_guard() {
let content = generate_opencode_plugin("sqz");
assert!(
content.contains("function isAlreadyWrapped(cmd: string): boolean"),
"generated plugin must define isAlreadyWrapped helper"
);
assert!(
content.contains(r#"lowered.includes("sqz_cmd=")"#),
"plugin must check for the SQZ_CMD= prior-wrap prefix"
);
assert!(
content.contains(r#"lowered.includes("sqz compress")"#),
"plugin must check for the `sqz compress` prior-wrap tail"
);
assert!(
content.contains("isAlreadyWrapped(cmd)"),
"plugin hook body must call isAlreadyWrapped on the command"
);
assert!(
content.contains("function extractBaseCmd(cmd: string): string"),
"plugin must define extractBaseCmd that skips env assignments"
);
assert!(
content.contains("extractBaseCmd(cmd)"),
"plugin hook body must use extractBaseCmd, not raw split"
);
}
#[test]
fn test_is_already_wrapped_detects_all_marker_shapes() {
assert!(is_already_wrapped("SQZ_CMD=git git status"));
assert!(is_already_wrapped("sqz_cmd=git git status"));
assert!(is_already_wrapped("git status | sqz compress"));
assert!(is_already_wrapped("git status 2>&1 | /path/sqz compress"));
assert!(is_already_wrapped("ls -la | sqz compress-stream"));
assert!(is_already_wrapped("sqz stats"));
assert!(is_already_wrapped("/usr/local/bin/sqz gain"));
assert!(is_already_wrapped("SQZ_FOO=bar cmd"));
assert!(!is_already_wrapped("git status"));
assert!(!is_already_wrapped("grep sqz logfile.txt"));
assert!(!is_already_wrapped("cargo test --package my-sqz-crate"));
}
#[test]
fn test_extract_base_cmd_skips_env_assignments() {
assert_eq!(extract_base_cmd("make test"), "make");
assert_eq!(extract_base_cmd("FOO=bar make test"), "make");
assert_eq!(extract_base_cmd("FOO=bar BAZ=qux make test"), "make");
assert_eq!(extract_base_cmd("/usr/bin/git status"), "git");
assert_eq!(extract_base_cmd(""), "unknown");
assert_eq!(extract_base_cmd("FOO=bar"), "unknown");
}
#[test]
fn test_is_env_assignment() {
assert!(is_env_assignment("FOO=bar"));
assert!(is_env_assignment("FOO="));
assert!(is_env_assignment("_underscore=1"));
assert!(is_env_assignment("MixedCase_1=x"));
assert!(!is_env_assignment("=bar"));
assert!(!is_env_assignment("FOO"));
assert!(!is_env_assignment("--flag=value"));
assert!(!is_env_assignment("123=value"));
assert!(!is_env_assignment("FOO BAR=baz"));
}
#[test]
fn test_update_merges_into_existing_jsonc() {
let dir = tempfile::tempdir().unwrap();
let jsonc = dir.path().join("opencode.jsonc");
std::fs::write(
&jsonc,
r#"{
// user's own config with a comment
"$schema": "https://opencode.ai/config.json",
"model": "anthropic/claude-sonnet-4-5",
/* another comment */
"plugin": ["other-plugin"]
}
"#,
)
.unwrap();
let changed = update_opencode_config(dir.path()).unwrap();
assert!(changed, "must merge sqz entries into the existing .jsonc");
assert!(jsonc.exists(), "original .jsonc must still exist");
assert!(
!dir.path().join("opencode.json").exists(),
"must not create a parallel opencode.json alongside .jsonc \
(that's the issue #6 bug)"
);
let after = std::fs::read_to_string(&jsonc).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&after).unwrap();
let plugins = parsed["plugin"].as_array().unwrap();
assert!(
!plugins.iter().any(|v| v.as_str() == Some("sqz")),
"issue #10: sqz must NOT be added to plugin[]"
);
assert!(
plugins.iter().any(|v| v.as_str() == Some("other-plugin")),
"pre-existing plugin entries must be preserved"
);
assert_eq!(
parsed["model"].as_str(),
Some("anthropic/claude-sonnet-4-5"),
"unrelated user keys must survive the merge"
);
assert_eq!(
parsed["mcp"]["sqz"]["type"].as_str(),
Some("local"),
"mcp.sqz must be registered"
);
}
#[test]
fn test_update_opencode_config_detailed_reports_comments_lost() {
let dir = tempfile::tempdir().unwrap();
let jsonc = dir.path().join("opencode.jsonc");
std::fs::write(
&jsonc,
r#"{
// comment to be dropped
"plugin": ["other"]
}
"#,
)
.unwrap();
let (changed, comments_lost) =
update_opencode_config_detailed(dir.path()).unwrap();
assert!(changed);
assert!(
comments_lost,
"merger must report that comments were dropped from .jsonc"
);
}
#[test]
fn test_update_creates_plain_json_when_nothing_exists() {
let dir = tempfile::tempdir().unwrap();
update_opencode_config(dir.path()).unwrap();
assert!(dir.path().join("opencode.json").exists());
assert!(!dir.path().join("opencode.jsonc").exists());
}
#[test]
fn test_find_opencode_config_prefers_jsonc() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("opencode.json"), "{}").unwrap();
std::fs::write(dir.path().join("opencode.jsonc"), "{}").unwrap();
let found = find_opencode_config(dir.path()).unwrap();
assert_eq!(
found.file_name().unwrap(),
"opencode.jsonc",
"must prefer the .jsonc variant when both exist — the user \
is maintaining .jsonc for its comment support"
);
}
#[test]
fn test_find_opencode_config_returns_none_when_missing() {
let dir = tempfile::tempdir().unwrap();
assert!(find_opencode_config(dir.path()).is_none());
}
#[test]
fn test_opencode_config_has_comments_detects_jsonc_comments() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("opencode.jsonc"),
"// a line comment\n{\"plugin\":[]}\n",
)
.unwrap();
assert!(opencode_config_has_comments(dir.path()));
}
#[test]
fn test_opencode_config_has_comments_ignores_plain_json() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("opencode.json"),
r#"{"url":"http://example.com"}"#,
)
.unwrap();
assert!(!opencode_config_has_comments(dir.path()));
}
#[test]
fn test_strip_jsonc_comments_removes_line_comments() {
let src = "{\n // leading comment\n \"a\": 1 // trailing\n}";
let stripped = strip_jsonc_comments(src);
assert!(!stripped.contains("leading comment"));
assert!(!stripped.contains("trailing"));
let parsed: serde_json::Value = serde_json::from_str(&stripped).unwrap();
assert_eq!(parsed["a"], 1);
}
#[test]
fn test_strip_jsonc_comments_removes_block_comments() {
let src = "{\n /* block\n comment */\n \"a\": 1\n}";
let stripped = strip_jsonc_comments(src);
assert!(!stripped.contains("block"));
let parsed: serde_json::Value = serde_json::from_str(&stripped).unwrap();
assert_eq!(parsed["a"], 1);
}
#[test]
fn test_strip_jsonc_comments_preserves_strings() {
let src = r#"{"url": "http://example.com", "re": "/* not a comment */"}"#;
let stripped = strip_jsonc_comments(src);
let parsed: serde_json::Value = serde_json::from_str(&stripped).unwrap();
assert_eq!(parsed["url"], "http://example.com");
assert_eq!(parsed["re"], "/* not a comment */");
}
#[test]
fn test_strip_jsonc_comments_preserves_escaped_quote_in_string() {
let src = r#"{"s": "a\"//b"}"#;
let stripped = strip_jsonc_comments(src);
let parsed: serde_json::Value = serde_json::from_str(&stripped).unwrap();
assert_eq!(parsed["s"], r#"a"//b"#);
}
#[test]
fn test_strip_jsonc_comments_tolerates_unterminated_block() {
let src = "{\"a\":1 /* never ends";
let _ = strip_jsonc_comments(src); }
#[test]
fn test_remove_sqz_preserves_other_user_config() {
let dir = tempfile::tempdir().unwrap();
let config = dir.path().join("opencode.json");
std::fs::write(
&config,
r#"{
"$schema": "https://opencode.ai/config.json",
"model": "anthropic/claude-sonnet-4-5",
"plugin": ["other-plugin", "sqz"],
"mcp": {
"sqz": { "type": "local", "command": ["sqz-mcp"] },
"jira": { "type": "remote", "url": "https://jira.example.com/mcp" }
}
}
"#,
)
.unwrap();
let (path, changed) =
remove_sqz_from_opencode_config(dir.path()).unwrap().unwrap();
assert_eq!(path, config);
assert!(changed, "must report that sqz entries were removed");
assert!(
config.exists(),
"file must NOT be deleted — only sqz's entries removed"
);
let after = std::fs::read_to_string(&config).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&after).unwrap();
let plugins = parsed["plugin"].as_array().unwrap();
assert!(!plugins.iter().any(|v| v.as_str() == Some("sqz")));
assert!(plugins.iter().any(|v| v.as_str() == Some("other-plugin")));
let mcp = parsed["mcp"].as_object().unwrap();
assert!(!mcp.contains_key("sqz"), "mcp.sqz must be gone");
assert!(mcp.contains_key("jira"), "mcp.jira must survive");
assert_eq!(
parsed["model"].as_str(),
Some("anthropic/claude-sonnet-4-5"),
"unrelated keys must survive"
);
}
#[test]
fn test_remove_sqz_deletes_file_when_nothing_else_remains() {
let dir = tempfile::tempdir().unwrap();
let config = dir.path().join("opencode.json");
std::fs::write(
&config,
r#"{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"sqz": { "type": "local", "command": ["sqz-mcp", "--transport", "stdio"] }
},
"plugin": ["sqz"]
}
"#,
)
.unwrap();
let (_, changed) =
remove_sqz_from_opencode_config(dir.path()).unwrap().unwrap();
assert!(changed);
assert!(
!config.exists(),
"file with only $schema + sqz entries must be removed"
);
}
#[test]
fn test_remove_sqz_returns_none_when_config_missing() {
let dir = tempfile::tempdir().unwrap();
let result = remove_sqz_from_opencode_config(dir.path()).unwrap();
assert!(result.is_none());
}
#[test]
fn test_remove_sqz_from_jsonc_drops_comments() {
let dir = tempfile::tempdir().unwrap();
let jsonc = dir.path().join("opencode.jsonc");
std::fs::write(
&jsonc,
r#"{
// user's comment
"model": "x",
"plugin": ["sqz", "other"]
}
"#,
)
.unwrap();
let (path, changed) =
remove_sqz_from_opencode_config(dir.path()).unwrap().unwrap();
assert_eq!(path, jsonc);
assert!(changed);
assert!(path.exists(), "jsonc file kept because `model` and `other` remain");
let after = std::fs::read_to_string(&jsonc).unwrap();
assert!(
!after.contains("// user's comment"),
"comments are dropped by the serde_json round-trip; \
documented in update_opencode_config_detailed"
);
let parsed: serde_json::Value = serde_json::from_str(&after).unwrap();
let plugins = parsed["plugin"].as_array().unwrap();
assert_eq!(plugins.len(), 1);
assert_eq!(plugins[0], "other");
}
#[test]
fn issue_10_opencode_rewrite_works_in_powershell_syntax() {
let input = r#"{"tool":"bash","args":{"command":"dotnet build NewNeonCheckers3.sln"}}"#;
let result = process_opencode_hook(input).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
let cmd = parsed["args"]["command"].as_str().unwrap();
assert!(
!cmd.contains("SQZ_CMD="),
"issue #10: rewrite must not emit `SQZ_CMD=` (breaks on \
PowerShell/cmd.exe); got: {cmd}"
);
assert!(
cmd.contains("--cmd dotnet"),
"rewrite must pass label via --cmd; got: {cmd}"
);
let first_token = cmd.split_whitespace().next().unwrap_or("");
assert_eq!(
first_token, "dotnet",
"first token of the rewritten command must be the user's \
command itself, not an env-var assignment; got: {cmd}"
);
}
#[test]
fn issue_10_ts_plugin_emits_cmd_flag_not_env_prefix() {
let content = generate_opencode_plugin("sqz");
assert!(
content.contains("compress --cmd"),
"TS plugin must build rewrite with `compress --cmd ${{base}}`"
);
assert!(
!content.contains("SQZ_CMD=${base}"),
"TS plugin must not emit the legacy `SQZ_CMD=${{base}}` prefix"
);
}
#[test]
fn issue_10_fresh_opencode_config_has_no_plugin_entry() {
let dir = tempfile::tempdir().unwrap();
update_opencode_config(dir.path()).unwrap();
let content = std::fs::read_to_string(dir.path().join("opencode.json")).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert!(
parsed.get("plugin").is_none(),
"issue #10: fresh opencode.json must not include `plugin` key; got: {content}"
);
assert_eq!(
parsed["mcp"]["sqz"]["type"].as_str(),
Some("local"),
"mcp.sqz is the one sqz-authored entry that belongs in \
opencode.json; must still be registered"
);
}
#[test]
fn issue_10_reinit_strips_legacy_plugin_entry() {
let dir = tempfile::tempdir().unwrap();
let config = dir.path().join("opencode.json");
std::fs::write(
&config,
r#"{"$schema":"https://opencode.ai/config.json","mcp":{"sqz":{"type":"local","command":["sqz-mcp","--transport","stdio"]}},"plugin":["sqz"]}"#,
)
.unwrap();
let changed = update_opencode_config(dir.path()).unwrap();
assert!(changed, "re-init must report a change (the legacy entry was stripped)");
let after = std::fs::read_to_string(&config).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&after).unwrap();
assert!(
parsed.get("plugin").is_none(),
"legacy `plugin: [\"sqz\"]` must be stripped on re-init; got: {after}"
);
assert_eq!(
parsed["mcp"]["sqz"]["type"].as_str(),
Some("local"),
"mcp.sqz must survive cleanup of the plugin entry"
);
}
}