-
Notifications
You must be signed in to change notification settings - Fork 60
feat: surface installed hook actions during apm install #409
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -46,7 +46,7 @@ | |||||
| import re | ||||||
| import shutil | ||||||
| from pathlib import Path | ||||||
| from typing import List, Dict, Tuple, Optional | ||||||
| from typing import Any, List, Dict, Tuple, Optional | ||||||
| from dataclasses import dataclass, field | ||||||
|
|
||||||
| from apm_cli.integration.base_integrator import BaseIntegrator | ||||||
|
|
@@ -58,6 +58,7 @@ class HookIntegrationResult: | |||||
| hooks_integrated: int | ||||||
| scripts_copied: int | ||||||
| target_paths: List[Path] = field(default_factory=list) | ||||||
| display_payloads: List[Dict[str, Any]] = field(default_factory=list) | ||||||
|
|
||||||
|
|
||||||
| class HookIntegrator(BaseIntegrator): | ||||||
|
|
@@ -70,6 +71,75 @@ class HookIntegrator(BaseIntegrator): | |||||
| - Cursor: Merged into .cursor/hooks.json hooks key + .cursor/hooks/<pkg>/ | ||||||
| """ | ||||||
|
|
||||||
| @staticmethod | ||||||
| def _iter_hook_entries(payload: Dict) -> List[Tuple[str, Dict]]: | ||||||
| """Flatten hook payloads into ``(event_name, entry_dict)`` pairs.""" | ||||||
| entries: List[Tuple[str, Dict]] = [] | ||||||
| hooks = payload.get("hooks", {}) | ||||||
| if not isinstance(hooks, dict): | ||||||
| return entries | ||||||
|
|
||||||
| for event_name, matchers in hooks.items(): | ||||||
| if not isinstance(matchers, list): | ||||||
| continue | ||||||
| for matcher in matchers: | ||||||
| if not isinstance(matcher, dict): | ||||||
| continue | ||||||
|
|
||||||
| for key in ("command", "bash", "powershell"): | ||||||
| value = matcher.get(key) | ||||||
| if isinstance(value, str): | ||||||
| entries.append((event_name, {key: value})) | ||||||
|
|
||||||
| nested_hooks = matcher.get("hooks", []) | ||||||
| if not isinstance(nested_hooks, list): | ||||||
| continue | ||||||
| for hook in nested_hooks: | ||||||
| if not isinstance(hook, dict): | ||||||
| continue | ||||||
| for key in ("command", "bash", "powershell"): | ||||||
| value = hook.get(key) | ||||||
| if isinstance(value, str): | ||||||
| entries.append((event_name, {key: value})) | ||||||
| return entries | ||||||
|
|
||||||
| @staticmethod | ||||||
| def _summarize_command(entry: Dict) -> str: | ||||||
| """Return a human-readable summary for a single hook command entry.""" | ||||||
| command = "" | ||||||
| for key in ("command", "bash", "powershell"): | ||||||
| value = entry.get(key) | ||||||
| if isinstance(value, str) and value.strip(): | ||||||
| command = value.strip() | ||||||
| break | ||||||
|
|
||||||
| if not command: | ||||||
| return "runs hook command" | ||||||
|
|
||||||
| for token in command.split(): | ||||||
| cleaned = token.strip("\"'") | ||||||
| if "/" in cleaned or cleaned.startswith("."): | ||||||
| return f"runs {cleaned}" | ||||||
|
|
||||||
| return f"runs {command}" | ||||||
|
|
||||||
| def _build_display_payload(self, target_label: str, output_path: str, source_hook_file: Path, rewritten: Dict) -> Dict[str, Any]: | ||||||
| """Build CLI display metadata for an integrated hook file.""" | ||||||
| actions = [] | ||||||
| for event_name, entry in self._iter_hook_entries(rewritten): | ||||||
| actions.append({ | ||||||
| "event": event_name, | ||||||
| "summary": self._summarize_command(entry), | ||||||
| }) | ||||||
|
|
||||||
| return { | ||||||
| "target_label": target_label, | ||||||
| "output_path": output_path, | ||||||
| "source_hook_file": source_hook_file.name, | ||||||
| "actions": actions, | ||||||
| "rendered_json": json.dumps(rewritten, indent=2, sort_keys=True), | ||||||
|
||||||
| "rendered_json": json.dumps(rewritten, indent=2, sort_keys=True), | |
| "rendered_json": json.dumps(rewritten, indent=2), |
Copilot
AI
Mar 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rendered_json is precomputed for every integrated hook via json.dumps(...), even when apm install is not in --verbose mode. This adds avoidable work/memory on the hot install path. Consider deferring JSON rendering until verbose logging is actually requested (e.g., store the rewritten dict, or store a callable/None and render in the logger code only when needed).
Copilot
AI
Mar 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For VSCode/GitHub hooks, the display payload sets output_path to target_filename only. This makes the verbose line render as hooks.json -> <filename> without the .github/hooks/ prefix, unlike the Claude/Cursor cases that include the full target path. Consider storing the full relative destination path (e.g. .github/hooks/<filename>) or using target_label when formatting the destination so the output is unambiguous.
| target_filename, | |
| rel_path, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new verbose hook transparency (
Hook JSON ...+ full rewritten JSON) changes what--verboseemits duringapm install, but the CLI docs currently describe verbose mode only in terms of file paths + diagnostic details. Please update the Starlight docs (e.g.docs/src/content/docs/reference/cli-commands.mdunderapm install) to mention that verbose mode also prints the full rewritten hook JSON so users can review deployed hook content.