invar-tools 1.7.1__py3-none-any.whl → 1.10.0__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/__init__.py +8 -0
- invar/core/language.py +88 -0
- invar/core/models.py +106 -0
- invar/core/patterns/detector.py +6 -1
- invar/core/patterns/p0_exhaustive.py +15 -3
- invar/core/patterns/p0_literal.py +15 -3
- invar/core/patterns/p0_newtype.py +15 -3
- invar/core/patterns/p0_nonempty.py +15 -3
- invar/core/patterns/p0_validation.py +15 -3
- invar/core/patterns/registry.py +5 -1
- invar/core/patterns/types.py +5 -1
- invar/core/property_gen.py +4 -0
- invar/core/rules.py +84 -18
- invar/core/sync_helpers.py +27 -1
- invar/core/template_helpers.py +32 -0
- invar/core/ts_parsers.py +286 -0
- invar/core/ts_sig_parser.py +307 -0
- invar/node_tools/MANIFEST +7 -0
- invar/node_tools/__init__.py +51 -0
- invar/node_tools/fc-runner/cli.js +77 -0
- invar/node_tools/quick-check/cli.js +28 -0
- invar/node_tools/ts-analyzer/cli.js +480 -0
- invar/shell/claude_hooks.py +35 -12
- invar/shell/commands/guard.py +36 -1
- invar/shell/commands/init.py +133 -7
- invar/shell/commands/perception.py +157 -33
- invar/shell/commands/skill.py +187 -0
- invar/shell/commands/template_sync.py +65 -13
- invar/shell/commands/uninstall.py +77 -12
- invar/shell/commands/update.py +6 -14
- invar/shell/contract_coverage.py +1 -0
- invar/shell/fs.py +66 -13
- invar/shell/pi_hooks.py +213 -0
- invar/shell/prove/guard_ts.py +899 -0
- invar/shell/skill_manager.py +353 -0
- invar/shell/template_engine.py +28 -4
- invar/shell/templates.py +4 -4
- invar/templates/claude-md/python/critical-rules.md +33 -0
- invar/templates/claude-md/python/quick-reference.md +24 -0
- invar/templates/claude-md/typescript/critical-rules.md +40 -0
- invar/templates/claude-md/typescript/quick-reference.md +24 -0
- invar/templates/claude-md/universal/check-in.md +25 -0
- invar/templates/claude-md/universal/skills.md +73 -0
- invar/templates/claude-md/universal/workflow.md +55 -0
- invar/templates/commands/{audit.md → audit.md.jinja} +18 -1
- invar/templates/config/AGENT.md.jinja +256 -0
- invar/templates/config/CLAUDE.md.jinja +16 -209
- invar/templates/config/context.md.jinja +19 -0
- invar/templates/examples/{README.md → python/README.md} +2 -0
- invar/templates/examples/{conftest.py → python/conftest.py} +1 -1
- invar/templates/examples/{contracts.py → python/contracts.py} +81 -4
- invar/templates/examples/python/core_shell.py +227 -0
- invar/templates/examples/python/functional.py +613 -0
- invar/templates/examples/typescript/README.md +31 -0
- invar/templates/examples/typescript/contracts.ts +163 -0
- invar/templates/examples/typescript/core_shell.ts +374 -0
- invar/templates/examples/typescript/functional.ts +601 -0
- invar/templates/examples/typescript/workflow.md +95 -0
- invar/templates/hooks/PostToolUse.sh.jinja +10 -1
- invar/templates/hooks/PreToolUse.sh.jinja +38 -0
- invar/templates/hooks/Stop.sh.jinja +1 -1
- invar/templates/hooks/UserPromptSubmit.sh.jinja +7 -0
- invar/templates/hooks/pi/invar.ts.jinja +82 -0
- invar/templates/manifest.toml +8 -6
- invar/templates/onboard/assessment.md.jinja +214 -0
- invar/templates/onboard/patterns/python.md +347 -0
- invar/templates/onboard/patterns/typescript.md +452 -0
- invar/templates/onboard/roadmap.md.jinja +168 -0
- invar/templates/protocol/INVAR.md.jinja +51 -0
- invar/templates/protocol/python/architecture-examples.md +41 -0
- invar/templates/protocol/python/contracts-syntax.md +56 -0
- invar/templates/protocol/python/markers.md +44 -0
- invar/templates/protocol/python/tools.md +24 -0
- invar/templates/protocol/python/troubleshooting.md +38 -0
- invar/templates/protocol/typescript/architecture-examples.md +52 -0
- invar/templates/protocol/typescript/contracts-syntax.md +73 -0
- invar/templates/protocol/typescript/markers.md +48 -0
- invar/templates/protocol/typescript/tools.md +65 -0
- invar/templates/protocol/typescript/troubleshooting.md +104 -0
- invar/templates/protocol/universal/architecture.md +36 -0
- invar/templates/protocol/universal/completion.md +14 -0
- invar/templates/protocol/universal/contracts-concept.md +37 -0
- invar/templates/protocol/universal/header.md +17 -0
- invar/templates/protocol/universal/session.md +17 -0
- invar/templates/protocol/universal/six-laws.md +10 -0
- invar/templates/protocol/universal/usbv.md +14 -0
- invar/templates/protocol/universal/visible-workflow.md +25 -0
- invar/templates/skills/develop/SKILL.md.jinja +98 -3
- invar/templates/skills/extensions/_registry.yaml +93 -0
- invar/templates/skills/extensions/acceptance/SKILL.md +383 -0
- invar/templates/skills/extensions/invar-onboard/SKILL.md +448 -0
- invar/templates/skills/extensions/invar-onboard/patterns/python.md +347 -0
- invar/templates/skills/extensions/invar-onboard/patterns/typescript.md +452 -0
- invar/templates/skills/extensions/invar-onboard/templates/assessment.md.jinja +214 -0
- invar/templates/skills/extensions/invar-onboard/templates/roadmap.md.jinja +168 -0
- invar/templates/skills/extensions/security/SKILL.md +382 -0
- invar/templates/skills/extensions/security/patterns/_common.yaml +126 -0
- invar/templates/skills/extensions/security/patterns/python.yaml +155 -0
- invar/templates/skills/extensions/security/patterns/typescript.yaml +194 -0
- invar/templates/skills/investigate/SKILL.md.jinja +15 -0
- invar/templates/skills/propose/SKILL.md.jinja +33 -0
- invar/templates/skills/review/SKILL.md.jinja +346 -71
- {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/METADATA +326 -19
- invar_tools-1.10.0.dist-info/RECORD +173 -0
- invar/templates/examples/core_shell.py +0 -127
- invar/templates/protocol/INVAR.md +0 -310
- invar_tools-1.7.1.dist-info/RECORD +0 -112
- /invar/templates/examples/{workflow.md → python/workflow.md} +0 -0
- {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.7.1.dist-info → invar_tools-1.10.0.dist-info}/licenses/NOTICE +0 -0
invar/shell/claude_hooks.py
CHANGED
|
@@ -15,6 +15,8 @@ from typing import TYPE_CHECKING
|
|
|
15
15
|
from jinja2 import Environment, FileSystemLoader
|
|
16
16
|
from returns.result import Failure, Result, Success
|
|
17
17
|
|
|
18
|
+
from invar.core.language import detect_language_from_markers
|
|
19
|
+
|
|
18
20
|
if TYPE_CHECKING:
|
|
19
21
|
from rich.console import Console
|
|
20
22
|
|
|
@@ -32,6 +34,9 @@ DISABLED_MARKER = ".invar_disabled"
|
|
|
32
34
|
# Marker for identifying Invar hooks in settings
|
|
33
35
|
INVAR_HOOK_MARKER = ".claude/hooks/"
|
|
34
36
|
|
|
37
|
+
# Regex pattern for extracting protocol version from hook files
|
|
38
|
+
PROTOCOL_VERSION_PATTERN = r"Protocol: v([\d.]+)"
|
|
39
|
+
|
|
35
40
|
|
|
36
41
|
# @shell_orchestration: Tightly coupled to Claude Code settings.local.json format
|
|
37
42
|
def is_invar_hook(hook_entry: dict) -> bool:
|
|
@@ -102,11 +107,16 @@ def generate_hook_content(
|
|
|
102
107
|
syntax = detect_syntax(project_path)
|
|
103
108
|
guard_cmd = "invar_guard" if syntax == "mcp" else "invar guard"
|
|
104
109
|
|
|
110
|
+
# Detect project language from marker files
|
|
111
|
+
markers = frozenset(f.name for f in project_path.iterdir() if f.is_file())
|
|
112
|
+
language = detect_language_from_markers(markers)
|
|
113
|
+
|
|
105
114
|
# Build context for template
|
|
106
115
|
context = {
|
|
107
116
|
"protocol_version": PROTOCOL_VERSION,
|
|
108
117
|
"generated_date": datetime.now().strftime("%Y-%m-%d"),
|
|
109
118
|
"guard_cmd": guard_cmd,
|
|
119
|
+
"language": language,
|
|
110
120
|
}
|
|
111
121
|
|
|
112
122
|
# For UserPromptSubmit, add the full INVAR.md content
|
|
@@ -138,9 +148,11 @@ def _register_hooks_in_settings(project_path: Path) -> Result[bool, str]:
|
|
|
138
148
|
|
|
139
149
|
def build_invar_hook(hook_type: str) -> dict:
|
|
140
150
|
"""Build Invar hook entry for a hook type."""
|
|
151
|
+
# Use $CLAUDE_PROJECT_DIR for portable paths that work regardless of cwd
|
|
152
|
+
# Claude Code provides this env var to all hooks (see code.claude.com/docs/hooks)
|
|
141
153
|
hook_cmd = {
|
|
142
154
|
"type": "command",
|
|
143
|
-
"command": f"
|
|
155
|
+
"command": f'"$CLAUDE_PROJECT_DIR"/.claude/hooks/{hook_type}.sh',
|
|
144
156
|
}
|
|
145
157
|
if hook_type in ("PreToolUse", "PostToolUse"):
|
|
146
158
|
# These need a matcher - use "*" to match all tools
|
|
@@ -239,20 +251,24 @@ def install_claude_hooks(
|
|
|
239
251
|
# Create merged wrapper
|
|
240
252
|
merged = f'''#!/bin/bash
|
|
241
253
|
# Merged hook: User + Invar (DX-57)
|
|
242
|
-
#
|
|
243
|
-
|
|
244
|
-
|
|
254
|
+
# Ensure correct working directory regardless of where Claude Code invokes from
|
|
255
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
256
|
+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
257
|
+
if ! cd "$PROJECT_ROOT" 2>/dev/null; then
|
|
258
|
+
echo "[invar] Warning: Could not cd to $PROJECT_ROOT" >&2
|
|
259
|
+
exit 0 # Don't block Claude Code
|
|
260
|
+
fi
|
|
245
261
|
|
|
246
|
-
# Run user hook first
|
|
247
|
-
if [[ -f "$
|
|
248
|
-
source "$
|
|
262
|
+
# Run user hook first (higher priority)
|
|
263
|
+
if [[ -f "$SCRIPT_DIR/{hook_type}.sh.user_backup" ]]; then
|
|
264
|
+
source "$SCRIPT_DIR/{hook_type}.sh.user_backup" "$@"
|
|
249
265
|
USER_EXIT=$?
|
|
250
266
|
[[ $USER_EXIT -ne 0 ]] && exit $USER_EXIT
|
|
251
267
|
fi
|
|
252
268
|
|
|
253
269
|
# Run Invar hook
|
|
254
|
-
if [[ -f "$
|
|
255
|
-
source "$
|
|
270
|
+
if [[ -f "$SCRIPT_DIR/invar.{hook_type}.sh" ]]; then
|
|
271
|
+
source "$SCRIPT_DIR/invar.{hook_type}.sh" "$@"
|
|
256
272
|
fi
|
|
257
273
|
'''
|
|
258
274
|
wrapper_hook.write_text(merged)
|
|
@@ -262,7 +278,14 @@ fi
|
|
|
262
278
|
# No user hook, create simple wrapper
|
|
263
279
|
wrapper = f'''#!/bin/bash
|
|
264
280
|
# Invar hook wrapper (DX-57)
|
|
265
|
-
|
|
281
|
+
# Ensure correct working directory regardless of where Claude Code invokes from
|
|
282
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
283
|
+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
284
|
+
if ! cd "$PROJECT_ROOT" 2>/dev/null; then
|
|
285
|
+
echo "[invar] Warning: Could not cd to $PROJECT_ROOT" >&2
|
|
286
|
+
exit 0 # Don't block Claude Code
|
|
287
|
+
fi
|
|
288
|
+
source "$SCRIPT_DIR/invar.{hook_type}.sh" "$@"
|
|
266
289
|
'''
|
|
267
290
|
wrapper_hook.write_text(wrapper)
|
|
268
291
|
wrapper_hook.chmod(0o755)
|
|
@@ -311,7 +334,7 @@ def sync_claude_hooks(
|
|
|
311
334
|
try:
|
|
312
335
|
existing_content = check_hook.read_text()
|
|
313
336
|
# Extract version from header comment
|
|
314
|
-
version_match = re.search(
|
|
337
|
+
version_match = re.search(PROTOCOL_VERSION_PATTERN, existing_content)
|
|
315
338
|
old_version = version_match.group(1) if version_match else "unknown"
|
|
316
339
|
|
|
317
340
|
if old_version != PROTOCOL_VERSION:
|
|
@@ -456,7 +479,7 @@ def hooks_status(
|
|
|
456
479
|
# Try to get version
|
|
457
480
|
try:
|
|
458
481
|
content = invar_hook.read_text()
|
|
459
|
-
match = re.search(
|
|
482
|
+
match = re.search(PROTOCOL_VERSION_PATTERN, content)
|
|
460
483
|
if match:
|
|
461
484
|
status[f"{hook_type}_version"] = match.group(1)
|
|
462
485
|
except OSError:
|
invar/shell/commands/guard.py
CHANGED
|
@@ -158,6 +158,8 @@ def guard(
|
|
|
158
158
|
Use --suggest to get functional pattern suggestions (NewType, Validation, etc.).
|
|
159
159
|
Use --contracts-only (-c) to check contract coverage without running tests (DX-63).
|
|
160
160
|
"""
|
|
161
|
+
# LX-06: Language detection and dispatch
|
|
162
|
+
from invar.shell.commands.init import detect_language
|
|
161
163
|
from invar.shell.guard_helpers import (
|
|
162
164
|
collect_files_to_check,
|
|
163
165
|
handle_changed_mode,
|
|
@@ -168,7 +170,38 @@ def guard(
|
|
|
168
170
|
)
|
|
169
171
|
from invar.shell.testing import VerificationLevel
|
|
170
172
|
|
|
171
|
-
|
|
173
|
+
project_language = detect_language(path if path.is_dir() else find_project_root(path))
|
|
174
|
+
|
|
175
|
+
# Dispatch to language-specific guard if not Python
|
|
176
|
+
if project_language == "typescript":
|
|
177
|
+
from invar.shell.prove.guard_ts import run_typescript_guard
|
|
178
|
+
|
|
179
|
+
ts_result = run_typescript_guard(path if path.is_dir() else find_project_root(path))
|
|
180
|
+
match ts_result:
|
|
181
|
+
case Success(result):
|
|
182
|
+
if json_output or agent:
|
|
183
|
+
import json as json_mod
|
|
184
|
+
|
|
185
|
+
from invar.shell.prove.guard_ts import format_typescript_guard_v2
|
|
186
|
+
|
|
187
|
+
output = format_typescript_guard_v2(result)
|
|
188
|
+
console.print(json_mod.dumps(output, indent=2))
|
|
189
|
+
else:
|
|
190
|
+
console.print(f"[bold]TypeScript Guard[/bold] ({project_language})")
|
|
191
|
+
if result.status == "passed":
|
|
192
|
+
console.print("[green]✓ PASSED[/green]")
|
|
193
|
+
elif result.status == "skipped":
|
|
194
|
+
console.print("[yellow]⚠ SKIPPED[/yellow] (no TypeScript tools available)")
|
|
195
|
+
else:
|
|
196
|
+
console.print(f"[red]✗ FAILED[/red] ({result.error_count} errors)")
|
|
197
|
+
for v in result.violations[:10]: # Show first 10
|
|
198
|
+
console.print(f" {v.file}:{v.line}: [{v.severity}] {v.message}")
|
|
199
|
+
raise typer.Exit(0 if result.status == "passed" else 1)
|
|
200
|
+
case Failure(err):
|
|
201
|
+
console.print(f"[red]Error:[/red] {err}")
|
|
202
|
+
raise typer.Exit(1)
|
|
203
|
+
|
|
204
|
+
# DX-65: Handle single file mode (Python only from here)
|
|
172
205
|
single_file_mode = path.is_file()
|
|
173
206
|
single_file: Path | None = None
|
|
174
207
|
if single_file_mode:
|
|
@@ -519,6 +552,7 @@ def rules(
|
|
|
519
552
|
from invar.shell.commands.hooks import app as hooks_app # DX-57
|
|
520
553
|
from invar.shell.commands.init import init
|
|
521
554
|
from invar.shell.commands.mutate import mutate # DX-28
|
|
555
|
+
from invar.shell.commands.skill import app as skill_app # LX-07
|
|
522
556
|
from invar.shell.commands.sync_self import sync_self # DX-49
|
|
523
557
|
from invar.shell.commands.test import test, verify
|
|
524
558
|
from invar.shell.commands.uninstall import uninstall # DX-69
|
|
@@ -531,6 +565,7 @@ app.command()(test)
|
|
|
531
565
|
app.command()(verify)
|
|
532
566
|
app.command()(mutate) # DX-28: Mutation testing
|
|
533
567
|
app.add_typer(hooks_app, name="hooks") # DX-57: Claude Code hooks management
|
|
568
|
+
app.add_typer(skill_app, name="skill") # LX-07: Extension skills management
|
|
534
569
|
|
|
535
570
|
# DX-56: Create dev subcommand group for developer commands
|
|
536
571
|
dev_app = typer.Typer(
|
invar/shell/commands/init.py
CHANGED
|
@@ -15,13 +15,14 @@ from returns.result import Failure, Success
|
|
|
15
15
|
from rich.console import Console
|
|
16
16
|
from rich.panel import Panel
|
|
17
17
|
|
|
18
|
-
from invar.core.sync_helpers import SyncConfig
|
|
18
|
+
from invar.core.sync_helpers import VALID_LANGUAGES, SyncConfig
|
|
19
19
|
from invar.shell.claude_hooks import install_claude_hooks
|
|
20
20
|
from invar.shell.commands.template_sync import sync_templates
|
|
21
21
|
from invar.shell.mcp_config import (
|
|
22
22
|
generate_mcp_json,
|
|
23
23
|
get_recommended_method,
|
|
24
24
|
)
|
|
25
|
+
from invar.shell.pi_hooks import install_pi_hooks
|
|
25
26
|
from invar.shell.templates import (
|
|
26
27
|
add_config,
|
|
27
28
|
create_directories,
|
|
@@ -55,15 +56,72 @@ FILE_CATEGORIES: dict[str, list[tuple[str, str]]] = {
|
|
|
55
56
|
"generic": [
|
|
56
57
|
("AGENT.md", "Universal agent instructions"),
|
|
57
58
|
],
|
|
59
|
+
"pi": [
|
|
60
|
+
("CLAUDE.md", "Agent instructions (Pi compatible)"),
|
|
61
|
+
(".claude/skills/", "Workflow automation (Pi compatible)"),
|
|
62
|
+
(".pi/hooks/", "Pi-specific hooks"),
|
|
63
|
+
],
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
AGENT_CONFIGS: dict[str, dict[str, str]] = {
|
|
61
67
|
"claude": {"name": "Claude Code", "category": "claude"},
|
|
68
|
+
"pi": {"name": "Pi Coding Agent", "category": "pi"},
|
|
62
69
|
"generic": {"name": "Other (AGENT.md)", "category": "generic"},
|
|
63
|
-
# Future: "cursor", "windsurf", etc.
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
|
|
73
|
+
# =============================================================================
|
|
74
|
+
# Language Detection (LX-05)
|
|
75
|
+
# =============================================================================
|
|
76
|
+
|
|
77
|
+
from invar.core.language import (
|
|
78
|
+
FUTURE_LANGUAGES,
|
|
79
|
+
detect_language_from_markers,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Marker files to check for language detection
|
|
83
|
+
LANGUAGE_MARKERS: frozenset[str] = frozenset({
|
|
84
|
+
"pyproject.toml", "setup.py", # Python
|
|
85
|
+
"tsconfig.json", "package.json", # TypeScript
|
|
86
|
+
"Cargo.toml", # Rust (future)
|
|
87
|
+
"go.mod", # Go (future)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def detect_language(path: Path) -> str:
|
|
92
|
+
"""Detect project language from marker files (Shell wrapper).
|
|
93
|
+
|
|
94
|
+
This is the Shell wrapper that handles I/O. The actual detection
|
|
95
|
+
logic is in core.language.detect_language_from_markers.
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
>>> from pathlib import Path
|
|
99
|
+
>>> import tempfile
|
|
100
|
+
>>> with tempfile.TemporaryDirectory() as d:
|
|
101
|
+
... p = Path(d)
|
|
102
|
+
... (p / "pyproject.toml").touch()
|
|
103
|
+
... detect_language(p)
|
|
104
|
+
'python'
|
|
105
|
+
|
|
106
|
+
>>> with tempfile.TemporaryDirectory() as d:
|
|
107
|
+
... p = Path(d)
|
|
108
|
+
... (p / "tsconfig.json").touch()
|
|
109
|
+
... detect_language(p)
|
|
110
|
+
'typescript'
|
|
111
|
+
|
|
112
|
+
>>> with tempfile.TemporaryDirectory() as d:
|
|
113
|
+
... p = Path(d)
|
|
114
|
+
... detect_language(p) # Empty dir defaults to python
|
|
115
|
+
'python'
|
|
116
|
+
"""
|
|
117
|
+
# Collect present markers (I/O operation)
|
|
118
|
+
present_markers = frozenset(
|
|
119
|
+
marker for marker in LANGUAGE_MARKERS if (path / marker).exists()
|
|
120
|
+
)
|
|
121
|
+
# Delegate to pure core function
|
|
122
|
+
return detect_language_from_markers(present_markers)
|
|
123
|
+
|
|
124
|
+
|
|
67
125
|
# =============================================================================
|
|
68
126
|
# Interactive Prompts (DX-70)
|
|
69
127
|
# =============================================================================
|
|
@@ -103,6 +161,7 @@ def _prompt_agent_selection() -> list[str]:
|
|
|
103
161
|
|
|
104
162
|
choices = [
|
|
105
163
|
questionary.Choice("Claude Code (recommended)", value="claude"),
|
|
164
|
+
questionary.Choice("Pi Coding Agent", value="pi"),
|
|
106
165
|
questionary.Choice("Other (AGENT.md)", value="generic"),
|
|
107
166
|
]
|
|
108
167
|
|
|
@@ -156,6 +215,8 @@ def _prompt_file_selection(agents: list[str]) -> dict[str, bool]:
|
|
|
156
215
|
category_name = category.capitalize()
|
|
157
216
|
if category == "claude":
|
|
158
217
|
category_name = "Claude Code"
|
|
218
|
+
elif category == "pi":
|
|
219
|
+
category_name = "Pi Coding Agent"
|
|
159
220
|
choices.append(questionary.Separator(f"── {category_name} ──"))
|
|
160
221
|
for file, desc in files:
|
|
161
222
|
choices.append(
|
|
@@ -241,6 +302,17 @@ def init(
|
|
|
241
302
|
"--claude",
|
|
242
303
|
help="Auto-select Claude Code, skip all prompts",
|
|
243
304
|
),
|
|
305
|
+
pi: bool = typer.Option(
|
|
306
|
+
False,
|
|
307
|
+
"--pi",
|
|
308
|
+
help="Auto-select Pi Coding Agent, skip all prompts",
|
|
309
|
+
),
|
|
310
|
+
language: str | None = typer.Option(
|
|
311
|
+
None,
|
|
312
|
+
"--language",
|
|
313
|
+
"-l",
|
|
314
|
+
help="Target language (auto-detected if not specified): python, typescript",
|
|
315
|
+
),
|
|
244
316
|
preview: bool = typer.Option(
|
|
245
317
|
False,
|
|
246
318
|
"--preview",
|
|
@@ -252,6 +324,11 @@ def init(
|
|
|
252
324
|
|
|
253
325
|
DX-70: Simplified init with interactive selection and safe merge.
|
|
254
326
|
|
|
327
|
+
\b
|
|
328
|
+
Quick setup options:
|
|
329
|
+
- --claude Auto-select Claude Code (MCP + hooks + skills)
|
|
330
|
+
- --pi Auto-select Pi (shares CLAUDE.md + skills, adds Pi hooks)
|
|
331
|
+
|
|
255
332
|
\b
|
|
256
333
|
This command is safe - it always MERGES with existing files:
|
|
257
334
|
- File doesn't exist → Create
|
|
@@ -264,31 +341,65 @@ def init(
|
|
|
264
341
|
"""
|
|
265
342
|
from invar import __version__
|
|
266
343
|
|
|
344
|
+
# Mutual exclusivity check
|
|
345
|
+
if claude and pi:
|
|
346
|
+
console.print("[red]Error:[/red] Cannot use --claude and --pi together.")
|
|
347
|
+
raise typer.Exit(1)
|
|
348
|
+
|
|
267
349
|
# Resolve path
|
|
268
350
|
if path == Path():
|
|
269
351
|
path = Path.cwd()
|
|
270
352
|
path = path.resolve()
|
|
271
353
|
|
|
354
|
+
# LX-05: Language detection and validation
|
|
355
|
+
if language is None:
|
|
356
|
+
detected = detect_language(path)
|
|
357
|
+
# Fall back to python for unsupported detected languages
|
|
358
|
+
if detected in FUTURE_LANGUAGES:
|
|
359
|
+
console.print(
|
|
360
|
+
f"[yellow]Note:[/yellow] {detected} project detected. "
|
|
361
|
+
f"Using python templates (most similar). "
|
|
362
|
+
f"Native {detected} support coming soon."
|
|
363
|
+
)
|
|
364
|
+
language = "python"
|
|
365
|
+
else:
|
|
366
|
+
language = detected
|
|
367
|
+
else:
|
|
368
|
+
# Validate explicitly provided language
|
|
369
|
+
if language not in VALID_LANGUAGES:
|
|
370
|
+
valid = ", ".join(sorted(VALID_LANGUAGES))
|
|
371
|
+
console.print(f"[red]Error:[/red] Invalid language '{language}'. Must be one of: {valid}")
|
|
372
|
+
raise typer.Exit(1)
|
|
373
|
+
|
|
272
374
|
# Header
|
|
273
375
|
if claude:
|
|
274
376
|
console.print(f"\n[bold]Invar v{__version__} - Quick Setup (Claude Code)[/bold]")
|
|
377
|
+
elif pi:
|
|
378
|
+
console.print(f"\n[bold]Invar v{__version__} - Quick Setup (Pi)[/bold]")
|
|
275
379
|
else:
|
|
276
380
|
console.print(f"\n[bold]Invar v{__version__} - Project Setup[/bold]")
|
|
277
381
|
console.print("=" * 45)
|
|
278
|
-
console.print("[dim]Existing files will be MERGED
|
|
382
|
+
console.print(f"[dim]Language: {language} | Existing files will be MERGED.[/dim]")
|
|
279
383
|
|
|
280
384
|
# Determine agents and files
|
|
281
385
|
if claude:
|
|
282
|
-
# Quick mode:
|
|
386
|
+
# Quick mode: Claude Code defaults
|
|
283
387
|
agents = ["claude"]
|
|
284
388
|
selected_files: dict[str, bool] = {}
|
|
285
389
|
for category in ["optional", "claude"]:
|
|
286
390
|
for file, _ in FILE_CATEGORIES.get(category, []):
|
|
287
391
|
selected_files[file] = True
|
|
392
|
+
elif pi:
|
|
393
|
+
# Quick mode: Pi defaults
|
|
394
|
+
agents = ["pi"]
|
|
395
|
+
selected_files = {}
|
|
396
|
+
for category in ["optional", "pi"]:
|
|
397
|
+
for file, _ in FILE_CATEGORIES.get(category, []):
|
|
398
|
+
selected_files[file] = True
|
|
288
399
|
else:
|
|
289
400
|
# Interactive mode
|
|
290
401
|
if not _is_interactive():
|
|
291
|
-
console.print("[yellow]Non-interactive terminal detected. Use --claude for quick setup.[/yellow]")
|
|
402
|
+
console.print("[yellow]Non-interactive terminal detected. Use --claude or --pi for quick setup.[/yellow]")
|
|
292
403
|
raise typer.Exit(1)
|
|
293
404
|
|
|
294
405
|
agents = _prompt_agent_selection()
|
|
@@ -338,9 +449,10 @@ def init(
|
|
|
338
449
|
if not selected_files.get(".pre-commit-config.yaml", True):
|
|
339
450
|
skip_patterns.append(".pre-commit-config.yaml")
|
|
340
451
|
|
|
341
|
-
# Run template sync
|
|
452
|
+
# Run template sync (LX-05: pass language for template rendering)
|
|
342
453
|
sync_config = SyncConfig(
|
|
343
454
|
syntax="cli",
|
|
455
|
+
language=language,
|
|
344
456
|
inject_project_additions=(path / ".invar" / "project-additions.md").exists(),
|
|
345
457
|
force=False,
|
|
346
458
|
check=False,
|
|
@@ -379,6 +491,10 @@ def init(
|
|
|
379
491
|
if "claude" in agents and selected_files.get(".claude/hooks/", True):
|
|
380
492
|
install_claude_hooks(path, console)
|
|
381
493
|
|
|
494
|
+
# Install Pi hooks if selected
|
|
495
|
+
if "pi" in agents and selected_files.get(".pi/hooks/", True):
|
|
496
|
+
install_pi_hooks(path, console)
|
|
497
|
+
|
|
382
498
|
# Create MCP setup guide
|
|
383
499
|
mcp_setup = invar_dir / "mcp-setup.md"
|
|
384
500
|
if not mcp_setup.exists():
|
|
@@ -397,7 +513,7 @@ def init(
|
|
|
397
513
|
# Completion message
|
|
398
514
|
console.print(f"\n[bold green]✓ Initialized Invar v{__version__}[/bold green]")
|
|
399
515
|
|
|
400
|
-
# Show
|
|
516
|
+
# Show agent-specific tips
|
|
401
517
|
if "claude" in agents:
|
|
402
518
|
console.print()
|
|
403
519
|
console.print(
|
|
@@ -408,3 +524,13 @@ def init(
|
|
|
408
524
|
border_style="dim",
|
|
409
525
|
)
|
|
410
526
|
)
|
|
527
|
+
elif "pi" in agents:
|
|
528
|
+
console.print()
|
|
529
|
+
console.print(
|
|
530
|
+
Panel(
|
|
531
|
+
"[dim]Pi reads CLAUDE.md and .claude/skills/ directly.\n"
|
|
532
|
+
"Run [bold]pi[/bold] to start — USBV workflow is auto-enabled.[/dim]",
|
|
533
|
+
title="📌 Tip",
|
|
534
|
+
border_style="dim",
|
|
535
|
+
)
|
|
536
|
+
)
|
|
@@ -36,44 +36,20 @@ def run_map(path: Path, top_n: int, json_output: bool) -> Result[None, str]:
|
|
|
36
36
|
Run the map command.
|
|
37
37
|
|
|
38
38
|
Scans project and generates perception map with reference counts.
|
|
39
|
+
LX-06: Supports TypeScript projects (basic symbol listing).
|
|
39
40
|
"""
|
|
40
41
|
if not path.exists():
|
|
41
42
|
return Failure(f"Path does not exist: {path}")
|
|
42
43
|
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
sources: dict[str, str] = {}
|
|
44
|
+
# LX-06: Detect language and dispatch
|
|
45
|
+
from invar.shell.commands.init import detect_language
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
rel_path = str(py_file.relative_to(path))
|
|
51
|
-
# Skip empty files (e.g., __init__.py)
|
|
52
|
-
if not content.strip():
|
|
53
|
-
continue
|
|
54
|
-
file_info = parse_source(content, rel_path)
|
|
55
|
-
if file_info:
|
|
56
|
-
file_infos.append(file_info)
|
|
57
|
-
sources[rel_path] = content
|
|
58
|
-
except (OSError, UnicodeDecodeError) as e:
|
|
59
|
-
console.print(f"[yellow]Warning:[/yellow] {py_file}: {e}")
|
|
60
|
-
continue
|
|
61
|
-
|
|
62
|
-
if not file_infos:
|
|
63
|
-
return Failure("No Python files found")
|
|
64
|
-
|
|
65
|
-
# Build perception map
|
|
66
|
-
perception_map = build_perception_map(file_infos, sources, str(path.absolute()))
|
|
67
|
-
|
|
68
|
-
# Output
|
|
69
|
-
if json_output:
|
|
70
|
-
output = format_map_json(perception_map, top_n)
|
|
71
|
-
console.print(json.dumps(output, indent=2))
|
|
72
|
-
else:
|
|
73
|
-
output = format_map_text(perception_map, top_n)
|
|
74
|
-
console.print(output)
|
|
47
|
+
project_language = detect_language(path)
|
|
48
|
+
if project_language == "typescript":
|
|
49
|
+
return _run_map_typescript(path, top_n, json_output)
|
|
75
50
|
|
|
76
|
-
|
|
51
|
+
# Python path (original logic)
|
|
52
|
+
return _run_map_python(path, top_n, json_output)
|
|
77
53
|
|
|
78
54
|
|
|
79
55
|
# @shell_complexity: Signature extraction with symbol filtering
|
|
@@ -83,6 +59,7 @@ def run_sig(target: str, json_output: bool) -> Result[None, str]:
|
|
|
83
59
|
|
|
84
60
|
Extracts signatures from a file or specific symbol.
|
|
85
61
|
Target format: "path/to/file.py" or "path/to/file.py::symbol_name"
|
|
62
|
+
LX-06: Supports TypeScript files (.ts, .tsx).
|
|
86
63
|
"""
|
|
87
64
|
# Parse target
|
|
88
65
|
if "::" in target:
|
|
@@ -95,12 +72,26 @@ def run_sig(target: str, json_output: bool) -> Result[None, str]:
|
|
|
95
72
|
if not file_path.exists():
|
|
96
73
|
return Failure(f"File not found: {file_path}")
|
|
97
74
|
|
|
98
|
-
# Read
|
|
75
|
+
# Read file content
|
|
99
76
|
try:
|
|
100
77
|
content = file_path.read_text(encoding="utf-8")
|
|
101
78
|
except (OSError, UnicodeDecodeError) as e:
|
|
102
79
|
return Failure(f"Failed to read {file_path}: {e}")
|
|
103
80
|
|
|
81
|
+
# LX-06: Detect file type and dispatch to appropriate parser
|
|
82
|
+
suffix = file_path.suffix.lower()
|
|
83
|
+
if suffix in (".ts", ".tsx"):
|
|
84
|
+
return _run_sig_typescript(content, file_path, symbol_name, json_output)
|
|
85
|
+
|
|
86
|
+
# Python path (original logic)
|
|
87
|
+
return _run_sig_python(content, file_path, symbol_name, json_output)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# @shell_complexity: Python sig orchestration with error handling and output modes
|
|
91
|
+
def _run_sig_python(
|
|
92
|
+
content: str, file_path: Path, symbol_name: str | None, json_output: bool
|
|
93
|
+
) -> Result[None, str]:
|
|
94
|
+
"""Run sig for Python files."""
|
|
104
95
|
# Handle empty files
|
|
105
96
|
if not content.strip():
|
|
106
97
|
file_info = FileInfo(path=str(file_path), lines=0, symbols=[], imports=[], source="")
|
|
@@ -125,3 +116,136 @@ def run_sig(target: str, json_output: bool) -> Result[None, str]:
|
|
|
125
116
|
console.print(output)
|
|
126
117
|
|
|
127
118
|
return Success(None)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# @shell_complexity: TypeScript sig orchestration with error handling and output modes
|
|
122
|
+
def _run_sig_typescript(
|
|
123
|
+
content: str, file_path: Path, symbol_name: str | None, json_output: bool
|
|
124
|
+
) -> Result[None, str]:
|
|
125
|
+
"""Run sig for TypeScript files (LX-06)."""
|
|
126
|
+
from invar.core.ts_sig_parser import (
|
|
127
|
+
extract_ts_signatures,
|
|
128
|
+
format_ts_signatures_json,
|
|
129
|
+
format_ts_signatures_text,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Handle empty files consistently with Python path
|
|
133
|
+
symbols = [] if not content.strip() else extract_ts_signatures(content)
|
|
134
|
+
|
|
135
|
+
# Filter by symbol name if specified
|
|
136
|
+
if symbol_name:
|
|
137
|
+
symbols = [s for s in symbols if s.name == symbol_name]
|
|
138
|
+
if not symbols:
|
|
139
|
+
return Failure(f"Symbol '{symbol_name}' not found in {file_path}")
|
|
140
|
+
|
|
141
|
+
# Output
|
|
142
|
+
if json_output:
|
|
143
|
+
output = format_ts_signatures_json(symbols, str(file_path))
|
|
144
|
+
console.print(json.dumps(output, indent=2))
|
|
145
|
+
else:
|
|
146
|
+
output = format_ts_signatures_text(symbols, str(file_path))
|
|
147
|
+
console.print(output)
|
|
148
|
+
|
|
149
|
+
return Success(None)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# @shell_complexity: Python map with perception map building
|
|
153
|
+
def _run_map_python(path: Path, top_n: int, json_output: bool) -> Result[None, str]:
|
|
154
|
+
"""Run map for Python projects (original logic)."""
|
|
155
|
+
# Collect all files and their sources
|
|
156
|
+
file_infos: list[FileInfo] = []
|
|
157
|
+
sources: dict[str, str] = {}
|
|
158
|
+
|
|
159
|
+
for py_file in discover_python_files(path):
|
|
160
|
+
try:
|
|
161
|
+
content = py_file.read_text(encoding="utf-8")
|
|
162
|
+
rel_path = str(py_file.relative_to(path))
|
|
163
|
+
# Skip empty files (e.g., __init__.py)
|
|
164
|
+
if not content.strip():
|
|
165
|
+
continue
|
|
166
|
+
file_info = parse_source(content, rel_path)
|
|
167
|
+
if file_info:
|
|
168
|
+
file_infos.append(file_info)
|
|
169
|
+
sources[rel_path] = content
|
|
170
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
171
|
+
console.print(f"[yellow]Warning:[/yellow] {py_file}: {e}")
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
if not file_infos:
|
|
175
|
+
return Failure("No Python files found")
|
|
176
|
+
|
|
177
|
+
# Build perception map
|
|
178
|
+
perception_map = build_perception_map(file_infos, sources, str(path.absolute()))
|
|
179
|
+
|
|
180
|
+
# Output
|
|
181
|
+
if json_output:
|
|
182
|
+
output = format_map_json(perception_map, top_n)
|
|
183
|
+
console.print(json.dumps(output, indent=2))
|
|
184
|
+
else:
|
|
185
|
+
output = format_map_text(perception_map, top_n)
|
|
186
|
+
console.print(output)
|
|
187
|
+
|
|
188
|
+
return Success(None)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# @shell_complexity: TypeScript map with file discovery and symbol extraction
|
|
192
|
+
def _run_map_typescript(path: Path, top_n: int, json_output: bool) -> Result[None, str]:
|
|
193
|
+
"""Run map for TypeScript projects (LX-06).
|
|
194
|
+
|
|
195
|
+
MVP: Lists symbols without reference counting (Phase 2 can add references).
|
|
196
|
+
"""
|
|
197
|
+
from invar.core.ts_sig_parser import TSSymbol, extract_ts_signatures
|
|
198
|
+
from invar.shell.fs import discover_typescript_files
|
|
199
|
+
|
|
200
|
+
all_symbols: list[tuple[str, TSSymbol]] = []
|
|
201
|
+
|
|
202
|
+
for ts_file in discover_typescript_files(path):
|
|
203
|
+
try:
|
|
204
|
+
content = ts_file.read_text(encoding="utf-8")
|
|
205
|
+
rel_path = str(ts_file.relative_to(path))
|
|
206
|
+
if not content.strip():
|
|
207
|
+
continue
|
|
208
|
+
symbols = extract_ts_signatures(content)
|
|
209
|
+
for sym in symbols:
|
|
210
|
+
all_symbols.append((rel_path, sym))
|
|
211
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
212
|
+
console.print(f"[yellow]Warning:[/yellow] {ts_file}: {e}")
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
if not all_symbols:
|
|
216
|
+
return Failure("No TypeScript symbols found (files may be empty or contain no exportable symbols)")
|
|
217
|
+
|
|
218
|
+
# Sort by kind priority (function/class first), then by name
|
|
219
|
+
kind_order = {"function": 0, "class": 1, "interface": 2, "type": 3, "const": 4, "method": 5}
|
|
220
|
+
all_symbols.sort(key=lambda x: (kind_order.get(x[1].kind, 99), x[1].name))
|
|
221
|
+
|
|
222
|
+
# Limit to top_n
|
|
223
|
+
display_symbols = all_symbols[:top_n] if top_n > 0 else all_symbols
|
|
224
|
+
|
|
225
|
+
# Output
|
|
226
|
+
if json_output:
|
|
227
|
+
output = {
|
|
228
|
+
"language": "typescript",
|
|
229
|
+
"total_symbols": len(all_symbols),
|
|
230
|
+
"symbols": [
|
|
231
|
+
{
|
|
232
|
+
"name": sym.name,
|
|
233
|
+
"kind": sym.kind,
|
|
234
|
+
"file": file_path,
|
|
235
|
+
"line": sym.line,
|
|
236
|
+
"signature": sym.signature,
|
|
237
|
+
}
|
|
238
|
+
for file_path, sym in display_symbols
|
|
239
|
+
],
|
|
240
|
+
}
|
|
241
|
+
console.print(json.dumps(output, indent=2))
|
|
242
|
+
else:
|
|
243
|
+
console.print("[bold]TypeScript Symbol Map[/bold]")
|
|
244
|
+
console.print(f"Total symbols: {len(all_symbols)}\n")
|
|
245
|
+
for file_path, sym in display_symbols:
|
|
246
|
+
console.print(f"[{sym.kind}] {sym.name}")
|
|
247
|
+
console.print(f" {file_path}:{sym.line}")
|
|
248
|
+
console.print(f" {sym.signature}")
|
|
249
|
+
console.print()
|
|
250
|
+
|
|
251
|
+
return Success(None)
|