feat(KIN-010): implement rebuild-frontend post-pipeline hook
- scripts/rebuild-frontend.sh: builds Vue 3 frontend and restarts uvicorn API - cli/main.py: hook group with add/list/remove/logs/setup commands; `hook setup` idempotently registers rebuild-frontend for a project - agents/runner.py: call run_hooks(event="pipeline_completed") after successful pipeline; wrap in try/except so hook errors never block results - tests: 3 tests for hook_setup CLI + 3 tests for pipeline→hooks integration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6705b302f7
commit
01b269e2b8
6 changed files with 355 additions and 2 deletions
130
cli/main.py
130
cli/main.py
|
|
@ -15,6 +15,7 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|||
|
||||
from core.db import init_db
|
||||
from core import models
|
||||
from core import hooks as hooks_module
|
||||
from agents.bootstrap import (
|
||||
detect_tech_stack, detect_modules, extract_decisions_from_claude_md,
|
||||
find_vault_root, scan_obsidian, format_preview, save_to_db,
|
||||
|
|
@ -720,6 +721,135 @@ def bootstrap(ctx, path, project_id, name, vault_path, yes):
|
|||
f"{dec_count} decisions, {task_count} tasks.")
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# hook
|
||||
# ===========================================================================
|
||||
|
||||
@cli.group()
|
||||
def hook():
|
||||
"""Manage post-pipeline hooks."""
|
||||
|
||||
|
||||
@hook.command("add")
|
||||
@click.option("--project", "project_id", required=True, help="Project ID")
|
||||
@click.option("--name", required=True, help="Hook name")
|
||||
@click.option("--event", required=True, help="Event: pipeline_completed, step_completed")
|
||||
@click.option("--command", required=True, help="Shell command to run")
|
||||
@click.option("--module-path", default=None, help="Trigger only when module path matches (fnmatch)")
|
||||
@click.option("--working-dir", default=None, help="Working directory for the command")
|
||||
@click.pass_context
|
||||
def hook_add(ctx, project_id, name, event, command, module_path, working_dir):
|
||||
"""Add a post-pipeline hook to a project."""
|
||||
conn = ctx.obj["conn"]
|
||||
p = models.get_project(conn, project_id)
|
||||
if not p:
|
||||
click.echo(f"Project '{project_id}' not found.", err=True)
|
||||
raise SystemExit(1)
|
||||
h = hooks_module.create_hook(
|
||||
conn, project_id, name, event, command,
|
||||
trigger_module_path=module_path,
|
||||
working_dir=working_dir,
|
||||
)
|
||||
click.echo(f"Created hook: #{h['id']} {h['name']} [{h['event']}] → {h['command']}")
|
||||
|
||||
|
||||
@hook.command("list")
|
||||
@click.option("--project", "project_id", required=True, help="Project ID")
|
||||
@click.pass_context
|
||||
def hook_list(ctx, project_id):
|
||||
"""List hooks for a project."""
|
||||
conn = ctx.obj["conn"]
|
||||
hs = hooks_module.get_hooks(conn, project_id, enabled_only=False)
|
||||
if not hs:
|
||||
click.echo("No hooks found.")
|
||||
return
|
||||
rows = [
|
||||
[str(h["id"]), h["name"], h["event"],
|
||||
h["command"][:40], h.get("trigger_module_path") or "-",
|
||||
"yes" if h["enabled"] else "no"]
|
||||
for h in hs
|
||||
]
|
||||
click.echo(_table(["ID", "Name", "Event", "Command", "Module", "Enabled"], rows))
|
||||
|
||||
|
||||
@hook.command("remove")
|
||||
@click.argument("hook_id", type=int)
|
||||
@click.pass_context
|
||||
def hook_remove(ctx, hook_id):
|
||||
"""Remove a hook by ID."""
|
||||
conn = ctx.obj["conn"]
|
||||
row = conn.execute("SELECT * FROM hooks WHERE id = ?", (hook_id,)).fetchone()
|
||||
if not row:
|
||||
click.echo(f"Hook #{hook_id} not found.", err=True)
|
||||
raise SystemExit(1)
|
||||
hooks_module.delete_hook(conn, hook_id)
|
||||
click.echo(f"Removed hook #{hook_id}.")
|
||||
|
||||
|
||||
@hook.command("logs")
|
||||
@click.option("--project", "project_id", required=True, help="Project ID")
|
||||
@click.option("--limit", default=20, help="Number of log entries (default: 20)")
|
||||
@click.pass_context
|
||||
def hook_logs(ctx, project_id, limit):
|
||||
"""Show recent hook execution logs for a project."""
|
||||
conn = ctx.obj["conn"]
|
||||
logs = hooks_module.get_hook_logs(conn, project_id=project_id, limit=limit)
|
||||
if not logs:
|
||||
click.echo("No hook logs found.")
|
||||
return
|
||||
rows = [
|
||||
[str(l["hook_id"]), l.get("task_id") or "-",
|
||||
"ok" if l["success"] else "fail",
|
||||
str(l["exit_code"]),
|
||||
f"{l['duration_seconds']:.1f}s",
|
||||
l["created_at"][:19]]
|
||||
for l in logs
|
||||
]
|
||||
click.echo(_table(["Hook", "Task", "Result", "Exit", "Duration", "Time"], rows))
|
||||
|
||||
|
||||
@hook.command("setup")
|
||||
@click.option("--project", "project_id", required=True, help="Project ID")
|
||||
@click.option("--scripts-dir", default=None,
|
||||
help="Directory with hook scripts (default: <kin_root>/scripts)")
|
||||
@click.pass_context
|
||||
def hook_setup(ctx, project_id, scripts_dir):
|
||||
"""Register standard hooks for a project.
|
||||
|
||||
Currently registers: rebuild-frontend (fires on web/frontend/* changes).
|
||||
Idempotent — skips hooks that already exist.
|
||||
"""
|
||||
conn = ctx.obj["conn"]
|
||||
p = models.get_project(conn, project_id)
|
||||
if not p:
|
||||
click.echo(f"Project '{project_id}' not found.", err=True)
|
||||
raise SystemExit(1)
|
||||
|
||||
if scripts_dir is None:
|
||||
scripts_dir = str(Path(__file__).parent.parent / "scripts")
|
||||
|
||||
existing_names = {h["name"] for h in hooks_module.get_hooks(conn, project_id, enabled_only=False)}
|
||||
created = []
|
||||
|
||||
if "rebuild-frontend" not in existing_names:
|
||||
rebuild_cmd = str(Path(scripts_dir) / "rebuild-frontend.sh")
|
||||
hooks_module.create_hook(
|
||||
conn, project_id,
|
||||
name="rebuild-frontend",
|
||||
event="pipeline_completed",
|
||||
command=rebuild_cmd,
|
||||
trigger_module_path="web/frontend/*",
|
||||
working_dir=p.get("path"),
|
||||
timeout_seconds=300,
|
||||
)
|
||||
created.append("rebuild-frontend")
|
||||
else:
|
||||
click.echo("Hook 'rebuild-frontend' already exists, skipping.")
|
||||
|
||||
if created:
|
||||
click.echo(f"Registered hooks: {', '.join(created)}")
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Entry point
|
||||
# ===========================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue