165 lines
5.1 KiB
Python
165 lines
5.1 KiB
Python
"""Augment Code CLI native user-hook integration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
import subprocess # nosec B404
|
|
|
|
from .augment_contract import (
|
|
AGENT_KEY,
|
|
AugmentHooks,
|
|
build_handler,
|
|
remove_managed_hooks,
|
|
render_augment_document,
|
|
runtime_contracts,
|
|
)
|
|
from .augment_installation import (
|
|
AugmentRuntimePaths,
|
|
commit_augment_installation,
|
|
commit_augment_uninstall,
|
|
preflight_augment_installation,
|
|
prepare_augment_installation,
|
|
prepare_augment_uninstall,
|
|
)
|
|
from .augment_io import augment_runtime_files_exist, read_augment_document
|
|
from .base import AgentPlugin, InstallConfig, PluginStatus
|
|
|
|
|
|
_REGEX_VALIDATION_TIMEOUT_SECONDS = 10
|
|
_REGEX_VALIDATOR = """import fs from "node:fs";
|
|
const pattern = fs.readFileSync(0, "utf8");
|
|
try {
|
|
new RegExp(pattern);
|
|
} catch (error) {
|
|
process.stderr.write(error instanceof Error ? error.message : String(error));
|
|
process.exit(1);
|
|
}
|
|
"""
|
|
|
|
|
|
def _resolve_node_runtime() -> str:
|
|
node_path = shutil.which("node")
|
|
if node_path is None:
|
|
raise FileNotFoundError(
|
|
"Node.js runtime is required to validate Auggie matcher expressions"
|
|
)
|
|
return node_path
|
|
|
|
|
|
def _validate_matcher(node_path: str, label: str, matcher: str) -> None:
|
|
try:
|
|
result = subprocess.run( # nosec B603
|
|
[
|
|
node_path,
|
|
"--input-type=module",
|
|
"--eval",
|
|
_REGEX_VALIDATOR,
|
|
],
|
|
input=matcher,
|
|
text=True,
|
|
capture_output=True,
|
|
check=False,
|
|
timeout=_REGEX_VALIDATION_TIMEOUT_SECONDS,
|
|
)
|
|
except subprocess.TimeoutExpired as error:
|
|
raise TimeoutError(
|
|
f"{label} matcher validation timed out after "
|
|
f"{_REGEX_VALIDATION_TIMEOUT_SECONDS} seconds"
|
|
) from error
|
|
except OSError as error:
|
|
raise OSError(f"Run Node.js matcher validator: {error}") from error
|
|
if result.returncode == 0:
|
|
return
|
|
message = (
|
|
result.stderr.strip()
|
|
or result.stdout.strip()
|
|
or f"Node.js exited with code {result.returncode}"
|
|
)
|
|
raise ValueError(
|
|
f"{label} matcher must be a valid JavaScript regular expression: {message}"
|
|
)
|
|
|
|
|
|
def _validate_matchers(hooks: AugmentHooks) -> None:
|
|
matchers = [
|
|
(event, group_index, group["matcher"])
|
|
for event in sorted(hooks)
|
|
for group_index, group in enumerate(hooks[event])
|
|
if "matcher" in group
|
|
]
|
|
if not matchers:
|
|
return
|
|
node_path = _resolve_node_runtime()
|
|
for event, group_index, matcher in matchers:
|
|
label = f"Auggie settings group hooks.{event}[{group_index}]"
|
|
_validate_matcher(node_path, label, matcher)
|
|
|
|
|
|
def _installed_hooks(hooks: AugmentHooks, paths: AugmentRuntimePaths) -> AugmentHooks:
|
|
cleaned, _ = remove_managed_hooks(hooks)
|
|
return {
|
|
**cleaned,
|
|
"PreToolUse": [
|
|
*cleaned.get("PreToolUse", []),
|
|
{
|
|
"matcher": ".*",
|
|
"hooks": [build_handler(paths.guard_wrapper_path)],
|
|
},
|
|
],
|
|
"PostToolUse": [
|
|
*cleaned.get("PostToolUse", []),
|
|
{
|
|
"matcher": ".*",
|
|
"hooks": [build_handler(paths.audit_wrapper_path)],
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
class AugmentPlugin(AgentPlugin):
|
|
"""Install Elydora into Auggie's global user settings."""
|
|
|
|
manages_guard_runtime = True
|
|
|
|
def preflight_install(self, config: InstallConfig) -> None:
|
|
document = read_augment_document()
|
|
_validate_matchers(document.hooks)
|
|
preflight_augment_installation(config, document)
|
|
|
|
def install(self, config: InstallConfig) -> None:
|
|
document = read_augment_document()
|
|
_validate_matchers(document.hooks)
|
|
paths = preflight_augment_installation(config, document)
|
|
rendered = render_augment_document(
|
|
document, _installed_hooks(document.hooks, paths)
|
|
)
|
|
changes = prepare_augment_installation(config, paths, rendered)
|
|
commit_augment_installation(changes)
|
|
print("Auggie: user-level PreToolUse and PostToolUse hooks installed.")
|
|
|
|
def uninstall(self, agent_id: str = "") -> None:
|
|
document = read_augment_document()
|
|
if not document.exists:
|
|
return
|
|
hooks, changed = remove_managed_hooks(document.hooks, agent_id)
|
|
if not changed:
|
|
return
|
|
rendered = render_augment_document(document, hooks)
|
|
commit_augment_uninstall(prepare_augment_uninstall(rendered))
|
|
|
|
def status(self) -> PluginStatus:
|
|
document = read_augment_document()
|
|
contracts = runtime_contracts(document.hooks)
|
|
configured = bool(contracts)
|
|
installed = configured and augment_runtime_files_exist(contracts)
|
|
details = (
|
|
f"Config: {document.config_path}"
|
|
if installed
|
|
else f"Configured at {document.config_path}; managed contract incomplete"
|
|
)
|
|
return PluginStatus(
|
|
installed=installed,
|
|
agent=AGENT_KEY,
|
|
details=details if configured else "Not installed",
|
|
)
|