# Section 04: install-addon Subcommand Implementation

## Overview

This section implements the `install-addon` subcommand logic inside `src/godotiq/cli.py`. The subcommand copies the bundled Godot addon files and the `GODOTIQ_RULES.md` prompt file into a target Godot project directory. It supports `--dry-run` to preview changes without writing, validates the target is a real Godot project, and reports whether each file was "added" (new) or "updated" (overwritten).

**Depends on:**
- **Section 01 (packaging-config):** `pyproject.toml` must have `force-include` for `godot-addon/addons/godotiq` mapped to `godotiq/addon`, and `src/godotiq/addon/__init__.py` must exist as a path anchor.
- **Section 03 (cli-module):** `src/godotiq/cli.py` must exist with argparse scaffolding that defines the `install-addon` subcommand and routes to an `install_addon()` function. This section fills in that function body.

## Files to Modify

| File | Action |
|------|--------|
| `src/godotiq/cli.py` | Modify -- implement the `install_addon()` function body |
| `tests/test_cli.py` | Extend -- add install-addon tests |

## Addon Files Inventory

The addon files (which Hatchling maps into `godotiq/addon/` inside the wheel) are:

- `plugin.cfg`
- `godotiq_plugin.gd`
- `godotiq_runtime.gd`
- `godotiq_debugger.gd`
- `godotiq_server.gd`
- `godotiq_logger.gd`
- `.gitkeep` (harmless, included but not critical)

The prompts file to copy as `GODOTIQ_RULES.md` lives at `src/godotiq/prompts/godot_development.md`, which at runtime resolves via `Path(__file__).resolve().parent / "prompts" / "godot_development.md"` (from `cli.py`'s perspective, use `Path(__file__).resolve().parent` to get the `godotiq` package directory).

## Tests FIRST

All tests go in `tests/test_cli.py`. These tests cover install-addon file copying, validation, dry-run, and overwrite behavior. They use `tmp_path` to create a fake Godot project directory with a `project.godot` file.

### Test: File Copying Correctness

```python
# tests/test_cli.py (install-addon section)

def _make_godot_project(tmp_path):
    """Create a minimal fake Godot project directory with a project.godot file."""
    (tmp_path / "project.godot").write_text("[gd_resource]\n")
    return tmp_path


def test_install_addon_copies_gd_files(tmp_path):
    """install_addon copies all .gd files to target/addons/godotiq/."""

def test_install_addon_copies_plugin_cfg(tmp_path):
    """install_addon copies plugin.cfg to target/addons/godotiq/."""

def test_install_addon_copies_godotiq_rules(tmp_path):
    """install_addon copies GODOTIQ_RULES.md to the target project root from godot_development.md."""

def test_install_addon_creates_addons_dir(tmp_path):
    """install_addon creates addons/godotiq/ directory if it does not exist."""
```

Each test calls `install_addon(str(project_dir))` and then asserts the expected files exist at the correct paths. For the `.gd` files test, assert that at least 5 `.gd` files are present in `target/addons/godotiq/`. For `GODOTIQ_RULES.md`, assert it exists at `target/GODOTIQ_RULES.md` and that its content matches the source `godot_development.md`.

### Test: Overwrite Behavior

```python
def test_install_addon_overwrites_existing_files(tmp_path, capsys):
    """install_addon overwrites existing files and reports 'updated' in output."""

def test_install_addon_reports_added_for_new_files(tmp_path, capsys):
    """install_addon reports 'added' for files that did not previously exist."""
```

The "updated" test should pre-create a file (e.g., `addons/godotiq/plugin.cfg` with dummy content), run `install_addon`, then confirm the file content changed and the captured stdout contains "updated". The "added" test runs on a fresh project and checks stdout for "added".

### Test: Validation Errors

```python
def test_install_addon_fails_nonexistent_dir():
    """install_addon raises SystemExit (or prints error and exits) for non-existent target."""

def test_install_addon_fails_no_project_godot(tmp_path):
    """install_addon raises SystemExit when target exists but has no project.godot file."""

def test_install_addon_resolves_relative_paths(tmp_path, monkeypatch):
    """install_addon resolves relative paths correctly."""
```

For the non-existent directory test, pass a path like `/nonexistent/path/abc123` and assert a `SystemExit` is raised. For the no-`project.godot` test, pass a valid `tmp_path` that lacks the file. For the relative paths test, use `monkeypatch.chdir()` and pass a relative path, then verify files appear in the resolved location.

### Test: Dry-Run Mode

```python
def test_install_addon_dry_run_creates_no_files(tmp_path, capsys):
    """--dry-run lists files but does not copy anything."""

def test_install_addon_dry_run_still_validates(tmp_path):
    """--dry-run still validates the target directory exists and has project.godot."""

def test_install_addon_dry_run_lists_files(tmp_path, capsys):
    """--dry-run prints the list of files that would be copied."""
```

Create a valid Godot project, call `install_addon(str(project_dir), dry_run=True)`, then assert that `addons/godotiq/` does NOT exist and no files were created. Verify captured stdout lists the file names. For validation, pass an invalid dir with `dry_run=True` and confirm it still errors.

### Test: Lazy Import (install-addon path does not import server)

```python
def test_install_addon_does_not_import_server(tmp_path, monkeypatch):
    """The install_addon code path does NOT import godotiq.server."""
```

Use `monkeypatch` to remove `godotiq.server` from `sys.modules` if present, call `install_addon()`, then assert `"godotiq.server"` is not in `sys.modules`. This confirms that the addon installation path avoids the heavyweight server import.

## Implementation Details

### `install_addon()` Function in `src/godotiq/cli.py`

The function signature:

```python
def install_addon(target_dir: str, dry_run: bool = False) -> None:
    """Copy addon files and GODOTIQ_RULES.md to a Godot project."""
```

#### Step-by-step logic:

1. **Resolve the target path:** `target = Path(target_dir).resolve()`. This handles relative paths, `~` expansion (if `expanduser()` is called first), and symlinks.

2. **Validate target directory exists:** If `target` does not exist or is not a directory, print a clear error message to stderr and call `sys.exit(1)`.

3. **Validate it is a Godot project:** Check for `target / "project.godot"`. If missing, print an error like `"Error: {target} does not appear to be a Godot project (no project.godot found)"` and exit with code 1.

4. **Locate source files:**
   - Addon files: `addon_src = Path(__file__).resolve().parent / "addon"` -- this points to the `godotiq/addon/` directory inside the installed package (populated by Hatchling's `force-include`).
   - Rules prompt: `rules_src = Path(__file__).resolve().parent / "prompts" / "godot_development.md"`.

5. **Build the copy manifest:** Iterate over all files in `addon_src` (filtering to `.gd` and `.cfg` extensions -- skip `.gitkeep`, `__init__.py`, and `__pycache__`). Each entry maps `(source_path, destination_path)`. Add the rules file as `(rules_src, target / "GODOTIQ_RULES.md")`.

6. **Dry-run handling:** If `dry_run` is `True`, print each destination path with a `[dry-run]` prefix and return without copying.

7. **Create destination directory:** `(target / "addons" / "godotiq").mkdir(parents=True, exist_ok=True)`.

8. **Copy files and report:** For each `(src, dst)` pair:
   - Check if `dst` already exists to determine "added" vs "updated" status.
   - Use `shutil.copy2(src, dst)` to copy with metadata preservation.
   - Print the status: e.g., `"  added: addons/godotiq/plugin.cfg"` or `"  updated: addons/godotiq/godotiq_server.gd"`. Use paths relative to the target directory for readability.

9. **Print summary:** After all copies, print a summary line like `"Done. 6 files added, 1 file updated."`.

#### Imports needed in `install_addon()`:

- `shutil` (stdlib) -- for `shutil.copy2`
- `pathlib.Path` (likely already imported at module scope in `cli.py`)
- `sys` (for `sys.exit`)

These are all stdlib imports, so they can safely live at module scope in `cli.py` without performance concerns. The critical lazy-import constraint only applies to `godotiq.server` (which triggers heavy MCP tool registration).

#### File extension filtering:

When iterating the addon source directory, use a set of allowed extensions:

```python
_ADDON_EXTENSIONS = {".gd", ".cfg"}
```

This excludes `__init__.py` (the path anchor), `.gitkeep`, `__pycache__/`, and any `.pyc` files that might appear in the installed package.

### Error Handling Style

Use `print()` to stderr and `sys.exit(1)` for user-facing errors (not exceptions). This gives clean CLI error messages without tracebacks. Example:

```python
if not target.is_dir():
    print(f"Error: directory does not exist: {target}", file=sys.stderr)
    sys.exit(1)
```

Tests should use `pytest.raises(SystemExit)` to catch these exits.

### Output Format

The output format for a successful run:

```
Installing GodotIQ addon to /path/to/godot/project ...
  added:   addons/godotiq/plugin.cfg
  added:   addons/godotiq/godotiq_plugin.gd
  added:   addons/godotiq/godotiq_runtime.gd
  added:   addons/godotiq/godotiq_debugger.gd
  added:   addons/godotiq/godotiq_server.gd
  added:   addons/godotiq/godotiq_logger.gd
  added:   GODOTIQ_RULES.md

Done. 7 files added, 0 files updated.
```

For dry-run mode:

```
[dry-run] Would install GodotIQ addon to /path/to/godot/project
  would copy: addons/godotiq/plugin.cfg
  would copy: addons/godotiq/godotiq_plugin.gd
  ...
  would copy: GODOTIQ_RULES.md
```

## Key Decisions

- **`shutil.copy2` over `shutil.copy`:** Preserves file metadata (timestamps). Useful so users can see when addon files were last updated.
- **Filter by extension, not by filename:** Future addon files with `.gd` or `.cfg` extensions are automatically included without code changes.
- **Relative paths in output:** Print `addons/godotiq/plugin.cfg` (relative to target) rather than full absolute paths for cleaner output.
- **`sys.exit(1)` for errors:** Gives clean CLI behavior without Python tracebacks. Tests use `pytest.raises(SystemExit)`.
- **No `.gitkeep` or `__init__.py` in copy:** These are packaging artifacts, not Godot addon files. Filter them out.

## Implementation Notes (Post-Build)

### Deviations from plan
- **Editable install fallback**: Added fallback to `godot-addon/addons/godotiq/` when addon_src doesn't contain .gd/.cfg files (editable installs don't populate force-include).
- **Added `expanduser()`**: Added `.expanduser()` per review feedback for tilde expansion.
- **Empty manifest guard**: Added error when zero addon files found (review-identified gap).
- **`addon_src.is_dir()` guard**: Added safety check before `iterdir()` call.

### Files created/modified
- `src/godotiq/cli.py` — modified (full install_addon implementation)
- `tests/test_cli.py` — extended (13 new install-addon tests: copying, overwrite, validation, dry-run, lazy import)

### Test results
- 1162 passed, 2 skipped (full suite, no regressions)
- 24 CLI tests pass (11 from section-03 + 13 new)
