invar-tools 1.7.0__py3-none-any.whl → 1.8.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.
@@ -2,108 +2,217 @@
2
2
  Init command for Invar.
3
3
 
4
4
  Shell module: handles project initialization.
5
- DX-21B: Added --claude flag for Claude Code integration.
6
- DX-55: Unified idempotent init command with smart merge.
7
- DX-56: Uses unified template sync engine for file generation.
8
- DX-57: Added Claude Code hooks installation.
5
+ DX-70: Simplified init with interactive menus and safe merge behavior.
9
6
  """
10
7
 
11
8
  from __future__ import annotations
12
9
 
13
- import shutil
14
- import subprocess
10
+ import sys
15
11
  from pathlib import Path
16
12
 
17
13
  import typer
18
14
  from returns.result import Failure, Success
19
15
  from rich.console import Console
16
+ from rich.panel import Panel
20
17
 
21
18
  from invar.core.sync_helpers import SyncConfig
22
- from invar.core.template_parser import ClaudeMdState
23
- from invar.shell.claude_hooks import (
24
- install_claude_hooks,
25
- sync_claude_hooks,
26
- )
27
- from invar.shell.commands.merge import (
28
- ProjectState,
29
- detect_project_state,
30
- )
19
+ from invar.shell.claude_hooks import install_claude_hooks
31
20
  from invar.shell.commands.template_sync import sync_templates
32
21
  from invar.shell.mcp_config import (
33
- detect_available_methods,
34
22
  generate_mcp_json,
35
- get_method_by_name,
36
23
  get_recommended_method,
37
24
  )
38
- from invar.shell.template_engine import generate_from_manifest
25
+ from invar.shell.pi_hooks import install_pi_hooks
39
26
  from invar.shell.templates import (
40
27
  add_config,
41
28
  create_directories,
42
- detect_agent_configs,
43
29
  install_hooks,
44
30
  )
45
31
 
46
32
  console = Console()
47
33
 
48
34
 
49
- # @shell_complexity: Claude init with config file detection
50
- def run_claude_init(path: Path) -> bool:
51
- """
52
- Run 'claude /init' to generate intelligent CLAUDE.md.
53
-
54
- Returns True if successful, False otherwise.
35
+ # =============================================================================
36
+ # File Categories (DX-70)
37
+ # =============================================================================
38
+
39
+ FILE_CATEGORIES: dict[str, list[tuple[str, str]]] = {
40
+ "required": [
41
+ ("INVAR.md", "Protocol and contract rules"),
42
+ (".invar/", "Config, context, examples"),
43
+ ],
44
+ "optional": [
45
+ (".pre-commit-config.yaml", "Verification before commit"),
46
+ ("src/core/", "Pure logic directory"),
47
+ ("src/shell/", "I/O operations directory"),
48
+ ],
49
+ "claude": [
50
+ ("CLAUDE.md", "Agent instructions"),
51
+ (".claude/skills/", "Workflow automation"),
52
+ (".claude/commands/", "User commands (/audit, /guard)"),
53
+ (".claude/hooks/", "Tool guidance (+ settings.local.json)"),
54
+ (".mcp.json", "MCP server config"),
55
+ ],
56
+ "generic": [
57
+ ("AGENT.md", "Universal agent instructions"),
58
+ ],
59
+ "pi": [
60
+ ("CLAUDE.md", "Agent instructions (Pi compatible)"),
61
+ (".claude/skills/", "Workflow automation (Pi compatible)"),
62
+ (".pi/hooks/", "Pi-specific hooks"),
63
+ ],
64
+ }
65
+
66
+ AGENT_CONFIGS: dict[str, dict[str, str]] = {
67
+ "claude": {"name": "Claude Code", "category": "claude"},
68
+ "pi": {"name": "Pi Coding Agent", "category": "pi"},
69
+ "generic": {"name": "Other (AGENT.md)", "category": "generic"},
70
+ }
71
+
72
+
73
+ # =============================================================================
74
+ # Interactive Prompts (DX-70)
75
+ # =============================================================================
76
+
77
+
78
+ def _is_interactive() -> bool:
79
+ """Check if running in an interactive terminal."""
80
+ return sys.stdin.isatty() and sys.stdout.isatty()
81
+
82
+
83
+ # @shell_orchestration: Style configuration for questionary UI library
84
+ def _get_prompt_style():
85
+ """Get custom style for questionary prompts.
86
+
87
+ Simple design:
88
+ - Pointer (») indicates current row
89
+ - Checkbox (●/○) indicates selected state
90
+ - All text in default color, no reverse
55
91
  """
56
- if not shutil.which("claude"):
57
- console.print(
58
- "[yellow]Warning:[/yellow] 'claude' CLI not found. "
59
- "Install Claude Code: https://claude.ai/code"
60
- )
61
- console.print("[dim]Skipping claude /init, will create basic CLAUDE.md[/dim]")
62
- return False
63
-
64
- console.print("\n[bold]Running claude /init...[/bold]")
65
- try:
66
- # Don't capture output - claude /init is interactive and needs user input
67
- result = subprocess.run(
68
- ["claude", "/init"],
69
- cwd=path,
70
- timeout=120,
71
- )
72
- if result.returncode == 0:
73
- console.print("[green]claude /init completed successfully[/green]")
74
- return True
75
- else:
76
- console.print("[yellow]Warning:[/yellow] claude /init failed")
77
- return False
78
- except subprocess.TimeoutExpired:
79
- console.print("[yellow]Warning:[/yellow] claude /init timed out")
80
- return False
81
- except Exception as e:
82
- console.print(f"[yellow]Warning:[/yellow] claude /init error: {e}")
83
- return False
92
+ from questionary import Style
93
+
94
+ return Style([
95
+ ("pointer", "fg:cyan bold"), # Pointer: cyan bold
96
+ ("highlighted", "noreverse"), # Current row: no reverse
97
+ ("selected", "noreverse"), # Selected items: no reverse
98
+ ("text", "noreverse"), # Normal text: no reverse
99
+ ])
100
+
101
+
102
+ # @shell_complexity: Interactive prompt with cursor selection
103
+ def _prompt_agent_selection() -> list[str]:
104
+ """Prompt user to select code agent using cursor navigation."""
105
+ import questionary
106
+
107
+ console.print("\n[bold]Select code agent:[/bold]")
108
+ console.print("[dim]Use arrow keys to move, enter to select[/dim]\n")
109
+
110
+ choices = [
111
+ questionary.Choice("Claude Code (recommended)", value="claude"),
112
+ questionary.Choice("Pi Coding Agent", value="pi"),
113
+ questionary.Choice("Other (AGENT.md)", value="generic"),
114
+ ]
115
+
116
+ selected = questionary.select(
117
+ "",
118
+ choices=choices,
119
+ instruction="",
120
+ style=_get_prompt_style(),
121
+ ).ask()
122
+
123
+ # Handle Ctrl+C
124
+ if not selected:
125
+ return ["claude"] # Default to Claude Code
126
+ return [selected]
127
+
128
+
129
+ # @shell_complexity: Interactive file selection with cursor navigation
130
+ def _prompt_file_selection(agents: list[str]) -> dict[str, bool]:
131
+ """Prompt user to select optional files using cursor navigation."""
132
+ import questionary
133
+
134
+ # Build available files
135
+ available: dict[str, list[tuple[str, str]]] = {
136
+ "optional": FILE_CATEGORIES["optional"],
137
+ }
138
+ for agent in agents:
139
+ config = AGENT_CONFIGS.get(agent)
140
+ if config:
141
+ category = config["category"]
142
+ available[category] = FILE_CATEGORIES.get(category, [])
143
+
144
+ # Show header
145
+ console.print("\n[bold]File Selection:[/bold]")
146
+ console.print("[dim]Existing files will be MERGED (your content preserved).[/dim]\n")
147
+
148
+ # Required files (always installed)
149
+ console.print("[bold]Required (always installed):[/bold]")
150
+ for file, desc in FILE_CATEGORIES["required"]:
151
+ console.print(f" [green]✓[/green] {file:30} {desc}")
152
+
153
+ console.print()
154
+ console.print("[dim]Use arrow keys to move, space to toggle, enter to confirm[/dim]\n")
155
+
156
+ # Build choices with categories as separators
157
+ choices: list[questionary.Choice | questionary.Separator] = []
158
+ file_list: list[str] = []
159
+
160
+ for category, files in available.items():
161
+ if category == "required":
162
+ continue
163
+ category_name = category.capitalize()
164
+ if category == "claude":
165
+ category_name = "Claude Code"
166
+ elif category == "pi":
167
+ category_name = "Pi Coding Agent"
168
+ choices.append(questionary.Separator(f"── {category_name} ──"))
169
+ for file, desc in files:
170
+ choices.append(
171
+ questionary.Choice(f"{file:28} {desc}", value=file, checked=True)
172
+ )
173
+ file_list.append(file)
174
+
175
+ selected = questionary.checkbox(
176
+ "Select files to install:",
177
+ choices=choices,
178
+ instruction="",
179
+ style=_get_prompt_style(),
180
+ ).ask()
181
+
182
+ # Handle Ctrl+C or empty result
183
+ if selected is None:
184
+ return dict.fromkeys(file_list, True) # Default: all selected
185
+
186
+ # Build result dict
187
+ return {f: f in selected for f in file_list}
188
+
189
+
190
+ def _show_execution_output(
191
+ created: list[str],
192
+ merged: list[str],
193
+ skipped: list[str],
194
+ ) -> None:
195
+ """Display execution results."""
196
+ console.print()
197
+ for file in created:
198
+ console.print(f" [green]✓[/green] {file:30} [dim]created[/dim]")
199
+ for file in merged:
200
+ console.print(f" [cyan]↻[/cyan] {file:30} [dim]merged[/dim]")
201
+ for file in skipped:
202
+ console.print(f" [dim]○[/dim] {file:30} [dim]skipped[/dim]")
84
203
 
85
204
 
86
- # @shell_complexity: MCP config with method selection and validation
87
- def configure_mcp_with_method(
88
- path: Path, mcp_method: str | None
89
- ) -> None:
90
- """Configure MCP server with specified or detected method."""
91
- import json
205
+ # =============================================================================
206
+ # MCP Configuration
207
+ # =============================================================================
92
208
 
93
- # Determine method to use
94
- if mcp_method:
95
- config = get_method_by_name(mcp_method)
96
- if config is None:
97
- console.print(f"[yellow]Warning:[/yellow] Method '{mcp_method}' not available")
98
- config = get_recommended_method()
99
- console.print(f"[dim]Using fallback: {config.description}[/dim]")
100
- else:
101
- config = get_recommended_method()
102
209
 
103
- console.print("\n[bold]Configuring MCP server...[/bold]")
104
- console.print(f" Method: {config.description}")
210
+ # @shell_complexity: MCP config merge with existing file handling
211
+ def _configure_mcp(path: Path) -> bool:
212
+ """Configure MCP server with recommended method."""
213
+ import json
105
214
 
106
- # Generate and write .mcp.json
215
+ config = get_recommended_method()
107
216
  mcp_json_path = path / ".mcp.json"
108
217
  mcp_content = generate_mcp_json(config)
109
218
 
@@ -111,310 +220,238 @@ def configure_mcp_with_method(
111
220
  try:
112
221
  existing = json.loads(mcp_json_path.read_text())
113
222
  if "mcpServers" in existing and "invar" in existing.get("mcpServers", {}):
114
- console.print("[dim]Skipped[/dim] .mcp.json (invar already configured)")
115
- return
223
+ return False # Already configured
116
224
  # Add invar to existing config
117
225
  if "mcpServers" not in existing:
118
226
  existing["mcpServers"] = {}
119
227
  existing["mcpServers"]["invar"] = mcp_content["mcpServers"]["invar"]
120
228
  mcp_json_path.write_text(json.dumps(existing, indent=2))
121
- console.print("[green]Updated[/green] .mcp.json (added invar)")
229
+ return True
122
230
  except (json.JSONDecodeError, OSError):
123
- console.print("[yellow]Warning:[/yellow] .mcp.json exists but couldn't update")
231
+ return False
124
232
  else:
125
233
  mcp_json_path.write_text(json.dumps(mcp_content, indent=2))
126
- console.print("[green]Created[/green] .mcp.json")
234
+ return True
127
235
 
128
236
 
129
- def show_available_mcp_methods() -> None:
130
- """Display available MCP execution methods."""
131
- methods = detect_available_methods()
132
- console.print("\n[bold]Available MCP methods:[/bold]")
133
- for i, method in enumerate(methods):
134
- marker = "[green]→[/green]" if i == 0 else " "
135
- console.print(f" {marker} {method.method.value}: {method.description}")
237
+ # =============================================================================
238
+ # Main Init Command (DX-70)
239
+ # =============================================================================
136
240
 
137
241
 
138
- # @shell_complexity: Project init with config detection and template setup
242
+ # @shell_complexity: Main CLI entry point with interactive flow and file generation
139
243
  def init(
140
- path: Path = typer.Argument(Path(), help="Project root directory"),
141
- claude: bool = typer.Option(
142
- False, "--claude", help="Run 'claude /init' and integrate with Claude Code"
143
- ),
144
- mcp_method: str = typer.Option(
145
- None,
146
- "--mcp-method",
147
- help="MCP execution method: uvx (recommended), command, or python",
148
- ),
149
- dirs: bool = typer.Option(
150
- None, "--dirs/--no-dirs", help="Create src/core and src/shell directories"
151
- ),
152
- hooks: bool = typer.Option(
153
- True, "--hooks/--no-hooks", help="Install pre-commit hooks (default: ON)"
154
- ),
155
- claude_hooks: bool = typer.Option(
156
- None, "--claude-hooks/--no-claude-hooks",
157
- help="Install Claude Code hooks (default: ON when --claude, DX-57)"
158
- ),
159
- skills: bool = typer.Option(
160
- True, "--skills/--no-skills", help="Create .claude/skills/ (default: ON, use --no-skills for Cursor)"
161
- ),
162
- yes: bool = typer.Option(
163
- False, "--yes", "-y", help="Accept defaults without prompting"
244
+ path: Path = typer.Argument(
245
+ Path(),
246
+ help="Project root directory (default: current directory)",
164
247
  ),
165
- check: bool = typer.Option(
166
- False, "--check", help="Preview changes without applying (DX-55)"
248
+ claude: bool = typer.Option(
249
+ False,
250
+ "--claude",
251
+ help="Auto-select Claude Code, skip all prompts",
167
252
  ),
168
- force: bool = typer.Option(
169
- False, "--force", help="Update even if already current (DX-55)"
253
+ pi: bool = typer.Option(
254
+ False,
255
+ "--pi",
256
+ help="Auto-select Pi Coding Agent, skip all prompts",
170
257
  ),
171
- reset: bool = typer.Option(
172
- False, "--reset", help="Dangerous: discard all user content (DX-55)"
258
+ preview: bool = typer.Option(
259
+ False,
260
+ "--preview",
261
+ help="Show what would be done (dry run)",
173
262
  ),
174
263
  ) -> None:
175
264
  """
176
- Initialize or update Invar configuration (idempotent).
265
+ Initialize or update Invar configuration.
177
266
 
178
- DX-55: This command is idempotent - safe to run multiple times.
179
- It detects current state and does the right thing:
267
+ DX-70: Simplified init with interactive selection and safe merge.
180
268
 
181
269
  \b
182
- - New project: Full setup
183
- - Existing project: Update managed regions, preserve user content
184
- - Corrupted/overwritten: Smart recovery with content preservation
270
+ Quick setup options:
271
+ - --claude Auto-select Claude Code (MCP + hooks + skills)
272
+ - --pi Auto-select Pi (shares CLAUDE.md + skills, adds Pi hooks)
185
273
 
186
- Works with or without pyproject.toml:
274
+ \b
275
+ This command is safe - it always MERGES with existing files:
276
+ - File doesn't exist → Create
277
+ - File exists → Merge (update invar regions, preserve your content)
278
+ - Never overwrites user content
279
+ - Never deletes files
187
280
 
188
281
  \b
189
- - If pyproject.toml exists: adds tool.invar section
190
- - Otherwise: creates invar.toml
191
-
192
- Use --check to preview changes without applying.
193
- Use --force to update even if already current.
194
- Use --reset to discard all user content (dangerous).
195
- Use --claude to run 'claude /init' first.
196
- Use --mcp-method to specify MCP execution method (uvx, command, python).
197
- Use --dirs to always create directories, --no-dirs to skip.
198
- Use --no-hooks to skip pre-commit hooks installation.
199
- Use --no-claude-hooks to skip Claude Code hooks (DX-57).
200
- Use --no-skills to skip .claude/skills/ creation (for Cursor users).
201
- Use --yes to accept defaults without prompting.
282
+ For full reset, use: invar uninstall && invar init
202
283
  """
203
284
  from invar import __version__
204
285
 
205
- # DX-55: Detect project state first
206
- state = detect_project_state(path)
286
+ # Mutual exclusivity check
287
+ if claude and pi:
288
+ console.print("[red]Error:[/red] Cannot use --claude and --pi together.")
289
+ raise typer.Exit(1)
207
290
 
208
- # --check mode: preview only
209
- if check:
210
- _show_check_preview(state, path, __version__)
211
- return
291
+ # Resolve path
292
+ if path == Path():
293
+ path = Path.cwd()
294
+ path = path.resolve()
212
295
 
213
- # --reset mode: dangerous full reset
214
- if reset:
215
- if not yes and not typer.confirm(
216
- "[red]This will DELETE all user customizations. Continue?[/red]",
217
- default=False,
218
- ):
219
- console.print("[yellow]Cancelled[/yellow]")
220
- return
221
- # Fall through to full init with reset flag
222
- state = ProjectState(
223
- initialized=False,
224
- claude_md_state=ClaudeMdState(state="absent"),
225
- version="",
226
- needs_update=True,
227
- )
296
+ # Header
297
+ if claude:
298
+ console.print(f"\n[bold]Invar v{__version__} - Quick Setup (Claude Code)[/bold]")
299
+ elif pi:
300
+ console.print(f"\n[bold]Invar v{__version__} - Quick Setup (Pi)[/bold]")
301
+ else:
302
+ console.print(f"\n[bold]Invar v{__version__} - Project Setup[/bold]")
303
+ console.print("=" * 45)
304
+ console.print("[dim]Existing files will be MERGED (your content preserved).[/dim]")
228
305
 
229
- # DX-55: Handle based on detected state
230
- action = state.action if not force else "update"
231
-
232
- if action == "none" and not force:
233
- # DX-55: Check for missing required files before declaring "no changes needed"
234
- missing_files = []
235
- if skills:
236
- skill_files = [
237
- ".claude/skills/develop/SKILL.md",
238
- ".claude/skills/investigate/SKILL.md",
239
- ".claude/skills/propose/SKILL.md",
240
- ".claude/skills/review/SKILL.md",
241
- ]
242
- for skill_file in skill_files:
243
- if not (path / skill_file).exists():
244
- missing_files.append(skill_file)
245
-
246
- if not missing_files:
247
- console.print(f"[green]✓[/green] Invar v{__version__} configured (no changes needed)")
248
- console.print("[dim]Use --force to refresh managed regions[/dim]")
249
- return
250
- else:
251
- # Recreate missing files
252
- console.print(f"[yellow]Detected:[/yellow] {len(missing_files)} missing file(s)")
253
- result = generate_from_manifest(path, syntax="cli", files_to_generate=missing_files)
254
- if isinstance(result, Success):
255
- for generated_file in result.unwrap():
256
- console.print(f"[green]Restored[/green] {generated_file}")
257
- console.print(f"[green]✓[/green] Invar v{__version__} configured")
258
- return
259
-
260
- # DX-21B: Run claude /init if requested (before sync)
261
- # DX-69: sync_templates() will merge claude's CLAUDE.md with invar template
306
+ # Determine agents and files
262
307
  if claude:
263
- run_claude_init(path)
308
+ # Quick mode: Claude Code defaults
309
+ agents = ["claude"]
310
+ selected_files: dict[str, bool] = {}
311
+ for category in ["optional", "claude"]:
312
+ for file, _ in FILE_CATEGORIES.get(category, []):
313
+ selected_files[file] = True
314
+ elif pi:
315
+ # Quick mode: Pi defaults
316
+ agents = ["pi"]
317
+ selected_files = {}
318
+ for category in ["optional", "pi"]:
319
+ for file, _ in FILE_CATEGORIES.get(category, []):
320
+ selected_files[file] = True
321
+ else:
322
+ # Interactive mode
323
+ if not _is_interactive():
324
+ console.print("[yellow]Non-interactive terminal detected. Use --claude or --pi for quick setup.[/yellow]")
325
+ raise typer.Exit(1)
326
+
327
+ agents = _prompt_agent_selection()
328
+ selected_files = _prompt_file_selection(agents)
329
+
330
+ # Preview mode
331
+ if preview:
332
+ console.print("\n[bold]Preview - Would create/update:[/bold]")
333
+ console.print("\n[bold]Required:[/bold]")
334
+ for file, desc in FILE_CATEGORIES["required"]:
335
+ console.print(f" [green]✓[/green] {file:30} {desc}")
336
+
337
+ console.print("\n[bold]Selected:[/bold]")
338
+ for file, selected in selected_files.items():
339
+ if selected:
340
+ console.print(f" [green]✓[/green] {file}")
341
+ else:
342
+ console.print(f" [dim]○[/dim] {file} [dim](skipped)[/dim]")
343
+
344
+ console.print("\n[dim]Run without --preview to apply.[/dim]")
345
+ return
346
+
347
+ # Execute
348
+ console.print("\n[bold]Creating files...[/bold]")
349
+
350
+ created: list[str] = []
351
+ merged: list[str] = []
352
+ skipped: list[str] = []
264
353
 
354
+ # Add config file (.invar/config.toml or pyproject.toml)
265
355
  config_result = add_config(path, console)
266
356
  if isinstance(config_result, Failure):
267
357
  console.print(f"[red]Error:[/red] {config_result.failure()}")
268
358
  raise typer.Exit(1)
269
- config_added = config_result.unwrap()
270
359
 
271
- # DX-56: Use unified sync engine for file generation
272
- console.print("\n[bold]Creating Invar files...[/bold]")
273
-
274
- # Check for project-additions.md
275
- has_project_additions = (path / ".invar" / "project-additions.md").exists()
360
+ # Ensure .invar directory exists
361
+ invar_dir = path / ".invar"
362
+ if not invar_dir.exists():
363
+ invar_dir.mkdir()
276
364
 
277
- # Build skip patterns for --no-skills
365
+ # Build skip patterns based on selection
278
366
  skip_patterns: list[str] = []
279
- if not skills:
367
+ if not selected_files.get(".claude/skills/", True):
280
368
  skip_patterns.append(".claude/skills/*")
369
+ if not selected_files.get(".claude/commands/", True):
370
+ skip_patterns.append(".claude/commands/*")
371
+ if not selected_files.get(".pre-commit-config.yaml", True):
372
+ skip_patterns.append(".pre-commit-config.yaml")
281
373
 
374
+ # Run template sync
282
375
  sync_config = SyncConfig(
283
376
  syntax="cli",
284
- inject_project_additions=has_project_additions,
285
- force=force,
286
- check=False, # Already handled above
287
- reset=reset,
377
+ inject_project_additions=(path / ".invar" / "project-additions.md").exists(),
378
+ force=False,
379
+ check=False,
380
+ reset=False,
288
381
  skip_patterns=skip_patterns,
289
382
  )
290
383
 
291
- # DX-56: Run unified sync engine (handles DX-55 state detection internally)
292
384
  result = sync_templates(path, sync_config)
293
- if isinstance(result, Failure):
294
- console.print(f"[yellow]Warning:[/yellow] {result.failure()}")
295
- else:
385
+ if isinstance(result, Success):
296
386
  report = result.unwrap()
297
- for file in report.created:
298
- console.print(f"[green]Created[/green] {file}")
299
- for file in report.updated:
300
- console.print(f"[cyan]Updated[/cyan] {file}")
301
- for error in report.errors:
302
- console.print(f"[yellow]Warning:[/yellow] {error}")
303
-
304
- # Create .invar directory structure (for proposals template - not in manifest)
305
- invar_dir = path / ".invar"
306
- if not invar_dir.exists():
307
- invar_dir.mkdir()
387
+ created.extend(report.created)
388
+ merged.extend(report.updated)
308
389
 
309
- # Create proposals directory for protocol governance
390
+ # Create proposals directory
310
391
  proposals_dir = invar_dir / "proposals"
311
392
  if not proposals_dir.exists():
312
393
  proposals_dir.mkdir()
313
394
  from invar.shell.templates import copy_template
314
- result = copy_template("proposal.md.template", proposals_dir, "TEMPLATE.md")
315
- if isinstance(result, Success) and result.unwrap():
316
- console.print("[green]Created[/green] .invar/proposals/TEMPLATE.md")
317
395
 
318
- # Agent detection (DX-69: simplified, only Claude Code supported)
319
- console.print("\n[bold]Checking for agent configurations...[/bold]")
320
- agent_result = detect_agent_configs(path)
321
- if isinstance(agent_result, Success):
322
- agent_status = agent_result.unwrap()
323
- if agent_status.get("claude") == "configured":
324
- console.print(" [green]✓[/green] claude: already configured")
396
+ copy_template("proposal.md.template", proposals_dir, "TEMPLATE.md")
397
+
398
+ # Configure MCP if Claude selected
399
+ if "claude" in agents and selected_files.get(".mcp.json", True):
400
+ if _configure_mcp(path):
401
+ created.append(".mcp.json")
402
+
403
+ # Create directories if selected
404
+ if selected_files.get("src/core/", True):
405
+ create_directories(path, console)
406
+
407
+ # Install pre-commit hooks if selected
408
+ if selected_files.get(".pre-commit-config.yaml", True):
409
+ install_hooks(path, console)
325
410
 
326
- # Configure MCP server (DX-16, DX-21B)
327
- configure_mcp_with_method(path, mcp_method)
411
+ # Install Claude hooks if selected
412
+ if "claude" in agents and selected_files.get(".claude/hooks/", True):
413
+ install_claude_hooks(path, console)
328
414
 
329
- # Show available methods if user might want to change
330
- if not mcp_method and not yes:
331
- show_available_mcp_methods()
415
+ # Install Pi hooks if selected
416
+ if "pi" in agents and selected_files.get(".pi/hooks/", True):
417
+ install_pi_hooks(path, console)
332
418
 
333
419
  # Create MCP setup guide
334
420
  mcp_setup = invar_dir / "mcp-setup.md"
335
421
  if not mcp_setup.exists():
336
422
  from invar.shell.templates import _MCP_SETUP_TEMPLATE
423
+
337
424
  mcp_setup.write_text(_MCP_SETUP_TEMPLATE)
338
- console.print("[green]Created[/green] .invar/mcp-setup.md (setup guide)")
339
425
 
340
- # Handle directory creation based on --dirs flag
341
- if dirs is not False:
342
- create_directories(path, console)
426
+ # Track skipped files
427
+ for file, selected in selected_files.items():
428
+ if not selected:
429
+ skipped.append(file)
343
430
 
344
- # Install pre-commit hooks if requested
345
- if hooks:
346
- install_hooks(path, console)
431
+ # Show results
432
+ _show_execution_output(created, merged, skipped)
347
433
 
348
- # DX-57: Handle Claude Code hooks
349
- # Determine if we should install/update Claude hooks
350
- should_install_claude_hooks = (
351
- claude_hooks is True # Explicitly requested
352
- or (claude_hooks is None and claude) # Default ON when --claude
353
- )
354
- should_skip_claude_hooks = claude_hooks is False
434
+ # Completion message
435
+ console.print(f"\n[bold green]✓ Initialized Invar v{__version__}[/bold green]")
355
436
 
356
- if should_install_claude_hooks and not should_skip_claude_hooks:
357
- # Install Claude hooks
358
- install_claude_hooks(path, console)
359
- elif not should_skip_claude_hooks:
360
- # Check if hooks already installed and need sync
361
- claude_hooks_dir = path / ".claude" / "hooks"
362
- if (claude_hooks_dir / "invar.UserPromptSubmit.sh").exists():
363
- # Sync existing hooks (idempotent update)
364
- sync_claude_hooks(path, console)
365
-
366
- if not config_added and not (path / "INVAR.md").exists():
367
- console.print("[yellow]Invar already configured.[/yellow]")
368
-
369
- # DX-55: Summary based on action taken
370
- if action == "full_init":
371
- console.print(f"\n[bold green] Initialized Invar v{__version__}[/bold green]")
372
- console.print("[dim]Note: If you run 'claude /init' later, just run 'invar init' again.[/dim]")
373
- elif action == "recover":
374
- console.print(f"\n[bold green]✓ Recovered Invar v{__version__}[/bold green]")
375
- console.print("[dim]Review the merged content in CLAUDE.md[/dim]")
376
- elif action == "update" or force:
377
- console.print(f"\n[bold green]✓ Updated Invar v{__version__}[/bold green]")
378
- console.print("[dim]Refreshed managed regions, preserved user content[/dim]")
379
- else:
380
- console.print("\n[bold green]Invar initialized successfully![/bold green]")
381
-
382
- if claude:
383
- console.print("[dim]Next: Review CLAUDE.md and start coding with Claude Code[/dim]")
384
-
385
-
386
- # @shell_complexity: Preview display requires multiple state-specific branches
387
- def _show_check_preview(state: ProjectState, path: Path, version: str) -> None:
388
- """Show preview of what would change (--check mode)."""
389
- console.print(f"\n[bold]Invar v{version} - Preview Mode[/bold]\n")
390
-
391
- console.print(f"Project state: [cyan]{state.claude_md_state.state}[/cyan]")
392
- console.print(f"Initialized: [cyan]{state.initialized}[/cyan]")
393
- console.print(f"Current version: [cyan]{state.version or 'N/A'}[/cyan]")
394
- console.print(f"Needs update: [cyan]{state.needs_update}[/cyan]")
395
- console.print(f"Action: [cyan]{state.action}[/cyan]\n")
396
-
397
- match state.action:
398
- case "none":
399
- console.print("[green]No changes needed[/green]")
400
- case "full_init":
401
- console.print("Would create:")
402
- console.print(" - INVAR.md")
403
- console.print(" - CLAUDE.md")
404
- console.print(" - .invar/context.md")
405
- console.print(" - .claude/skills/")
406
- console.print(" - .claude/hooks/ (DX-57, with --claude)")
407
- console.print(" - .pre-commit-config.yaml")
408
- case "update":
409
- console.print("Would update:")
410
- console.print(f" - CLAUDE.md (managed section v{state.version} → v{version})")
411
- console.print(" - .claude/skills/* (refresh)")
412
- console.print(" - .claude/hooks/* (refresh, if installed)")
413
- case "recover":
414
- console.print("[yellow]Would recover:[/yellow]")
415
- console.print(" - CLAUDE.md (restore regions, preserve content)")
416
- case "create":
417
- console.print("Would create:")
418
- console.print(" - CLAUDE.md")
419
-
420
- console.print("\n[dim]Run 'invar init' to apply.[/dim]")
437
+ # Show agent-specific tips
438
+ if "claude" in agents:
439
+ console.print()
440
+ console.print(
441
+ Panel(
442
+ "[dim]If you run [bold]claude /init[/bold] afterward, "
443
+ "run [bold]invar init[/bold] again to restore protocol.[/dim]",
444
+ title="📌 Tip",
445
+ border_style="dim",
446
+ )
447
+ )
448
+ elif "pi" in agents:
449
+ console.print()
450
+ console.print(
451
+ Panel(
452
+ "[dim]Pi reads CLAUDE.md and .claude/skills/ directly.\n"
453
+ "Run [bold]pi[/bold] to start USBV workflow is auto-enabled.[/dim]",
454
+ title="📌 Tip",
455
+ border_style="dim",
456
+ )
457
+ )