invar-tools 1.6.0__py3-none-any.whl → 1.7.1__py3-none-any.whl
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.
- invar/core/utils.py +3 -1
- invar/shell/claude_hooks.py +90 -0
- invar/shell/commands/guard.py +2 -0
- invar/shell/commands/init.py +303 -384
- invar/shell/commands/uninstall.py +479 -0
- invar/shell/contract_coverage.py +4 -1
- invar/shell/templates.py +36 -99
- invar/templates/commands/audit.md +6 -0
- invar/templates/commands/guard.md +6 -0
- invar/templates/config/pre-commit.yaml.jinja +2 -0
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.1.dist-info}/METADATA +55 -45
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.1.dist-info}/RECORD +17 -19
- invar/templates/aider.conf.yml.template +0 -31
- invar/templates/cursorrules.template +0 -40
- invar/templates/pre-commit-config.yaml.template +0 -44
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.1.dist-info}/WHEEL +0 -0
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.1.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.1.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.1.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.1.dist-info}/licenses/NOTICE +0 -0
invar/core/utils.py
CHANGED
|
@@ -83,7 +83,7 @@ def get_combined_status(
|
|
|
83
83
|
return "passed"
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
@pre(lambda data, source: source in ("pyproject", "invar", "default"))
|
|
86
|
+
@pre(lambda data, source: source in ("pyproject", "invar", "invar_dir", "default"))
|
|
87
87
|
@post(lambda result: isinstance(result, dict))
|
|
88
88
|
def extract_guard_section(data: dict[str, Any], source: str) -> dict[str, Any]:
|
|
89
89
|
"""
|
|
@@ -94,6 +94,8 @@ def extract_guard_section(data: dict[str, Any], source: str) -> dict[str, Any]:
|
|
|
94
94
|
{'x': 1}
|
|
95
95
|
>>> extract_guard_section({"guard": {"y": 2}}, "invar")
|
|
96
96
|
{'y': 2}
|
|
97
|
+
>>> extract_guard_section({"guard": {"z": 3}}, "invar_dir")
|
|
98
|
+
{'z': 3}
|
|
97
99
|
>>> extract_guard_section({}, "default")
|
|
98
100
|
{}
|
|
99
101
|
>>> extract_guard_section({"guard": 0}, "invar") # Non-dict value returns empty
|
invar/shell/claude_hooks.py
CHANGED
|
@@ -29,6 +29,26 @@ HOOKS_SUBDIR = ".claude/hooks"
|
|
|
29
29
|
DISABLED_MARKER = ".invar_disabled"
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
# Marker for identifying Invar hooks in settings
|
|
33
|
+
INVAR_HOOK_MARKER = ".claude/hooks/"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# @shell_orchestration: Tightly coupled to Claude Code settings.local.json format
|
|
37
|
+
def is_invar_hook(hook_entry: dict) -> bool:
|
|
38
|
+
"""Check if a hook entry is an Invar hook.
|
|
39
|
+
|
|
40
|
+
Works with both new format ({"hooks": [...]}) and legacy format.
|
|
41
|
+
"""
|
|
42
|
+
# All hook types now use {"hooks": [...]} format
|
|
43
|
+
if "hooks" in hook_entry:
|
|
44
|
+
return any(
|
|
45
|
+
INVAR_HOOK_MARKER in h.get("command", "")
|
|
46
|
+
for h in hook_entry.get("hooks", [])
|
|
47
|
+
)
|
|
48
|
+
# Legacy format fallback: {"type": "command", "command": "..."}
|
|
49
|
+
return INVAR_HOOK_MARKER in hook_entry.get("command", "")
|
|
50
|
+
|
|
51
|
+
|
|
32
52
|
def get_templates_path() -> Path:
|
|
33
53
|
"""Get the path to hook templates."""
|
|
34
54
|
return Path(__file__).parent.parent / "templates" / "hooks"
|
|
@@ -100,6 +120,68 @@ def generate_hook_content(
|
|
|
100
120
|
|
|
101
121
|
|
|
102
122
|
# @shell_complexity: Hook installation with user hook merging
|
|
123
|
+
|
|
124
|
+
def _register_hooks_in_settings(project_path: Path) -> Result[bool, str]:
|
|
125
|
+
"""
|
|
126
|
+
Register hooks in .claude/settings.local.json.
|
|
127
|
+
|
|
128
|
+
Claude Code requires explicit hook registration - hooks are NOT auto-discovered
|
|
129
|
+
from the .claude/hooks/ directory.
|
|
130
|
+
|
|
131
|
+
Uses merge strategy:
|
|
132
|
+
- Preserves user's existing hooks
|
|
133
|
+
- Only adds/updates Invar hooks (identified by .claude/hooks/ path)
|
|
134
|
+
"""
|
|
135
|
+
import json
|
|
136
|
+
|
|
137
|
+
settings_path = project_path / ".claude" / "settings.local.json"
|
|
138
|
+
|
|
139
|
+
def build_invar_hook(hook_type: str) -> dict:
|
|
140
|
+
"""Build Invar hook entry for a hook type."""
|
|
141
|
+
hook_cmd = {
|
|
142
|
+
"type": "command",
|
|
143
|
+
"command": f".claude/hooks/{hook_type}.sh",
|
|
144
|
+
}
|
|
145
|
+
if hook_type in ("PreToolUse", "PostToolUse"):
|
|
146
|
+
# These need a matcher - use "*" to match all tools
|
|
147
|
+
return {
|
|
148
|
+
"matcher": "*",
|
|
149
|
+
"hooks": [hook_cmd],
|
|
150
|
+
}
|
|
151
|
+
# UserPromptSubmit, Stop don't use matchers but still need hooks wrapper
|
|
152
|
+
return {
|
|
153
|
+
"hooks": [hook_cmd],
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
existing = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
|
158
|
+
|
|
159
|
+
# Get existing hooks or create empty dict
|
|
160
|
+
existing_hooks = existing.get("hooks", {})
|
|
161
|
+
|
|
162
|
+
# Merge each hook type
|
|
163
|
+
for hook_type in HOOK_TYPES:
|
|
164
|
+
existing_list = existing_hooks.get(hook_type, [])
|
|
165
|
+
|
|
166
|
+
# Filter out old Invar hooks, keep user hooks
|
|
167
|
+
user_hooks = [h for h in existing_list if not is_invar_hook(h)]
|
|
168
|
+
|
|
169
|
+
# Append new Invar hook
|
|
170
|
+
user_hooks.append(build_invar_hook(hook_type))
|
|
171
|
+
|
|
172
|
+
existing_hooks[hook_type] = user_hooks
|
|
173
|
+
|
|
174
|
+
existing["hooks"] = existing_hooks
|
|
175
|
+
|
|
176
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
settings_path.write_text(json.dumps(existing, indent=2))
|
|
178
|
+
return Success(True)
|
|
179
|
+
|
|
180
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
181
|
+
return Failure(f"Failed to update settings: {e}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# @shell_complexity: Multi-file installation with backup/merge logic for user hooks
|
|
103
185
|
def install_claude_hooks(
|
|
104
186
|
project_path: Path,
|
|
105
187
|
console: Console,
|
|
@@ -189,9 +271,17 @@ source "$(dirname "$0")/invar.{hook_type}.sh" "$@"
|
|
|
189
271
|
installed.append(hook_type)
|
|
190
272
|
|
|
191
273
|
if installed:
|
|
274
|
+
# Register hooks in settings.local.json (Claude Code requires explicit registration)
|
|
275
|
+
reg_result = _register_hooks_in_settings(project_path)
|
|
276
|
+
if isinstance(reg_result, Failure):
|
|
277
|
+
console.print(f" [yellow]Warning:[/yellow] {reg_result.failure()}")
|
|
278
|
+
else:
|
|
279
|
+
console.print(" [green]Registered[/green] hooks in .claude/settings.local.json")
|
|
280
|
+
|
|
192
281
|
console.print("\n [bold green]✓ Claude Code hooks installed[/bold green]")
|
|
193
282
|
console.print(" [dim]Auto-escape: pytest --pdb, pytest --cov, vendor/[/dim]")
|
|
194
283
|
console.print(" [dim]Manual escape: INVAR_ALLOW_PYTEST=1[/dim]")
|
|
284
|
+
console.print(" [yellow]⚠ Restart Claude Code session for hooks to take effect[/yellow]")
|
|
195
285
|
|
|
196
286
|
if failed:
|
|
197
287
|
return Failure(f"Failed to install hooks: {', '.join(failed)}")
|
invar/shell/commands/guard.py
CHANGED
|
@@ -521,9 +521,11 @@ from invar.shell.commands.init import init
|
|
|
521
521
|
from invar.shell.commands.mutate import mutate # DX-28
|
|
522
522
|
from invar.shell.commands.sync_self import sync_self # DX-49
|
|
523
523
|
from invar.shell.commands.test import test, verify
|
|
524
|
+
from invar.shell.commands.uninstall import uninstall # DX-69
|
|
524
525
|
from invar.shell.commands.update import update
|
|
525
526
|
|
|
526
527
|
app.command()(init)
|
|
528
|
+
app.command()(uninstall) # DX-69: Remove Invar from project
|
|
527
529
|
app.command()(update)
|
|
528
530
|
app.command()(test)
|
|
529
531
|
app.command()(verify)
|