# Distribution, Installation & Auto-Update Strategy

## Overview

qc-trace has two audiences with different install needs:

| Audience | What they run | Install complexity |
|----------|--------------|-------------------|
| **Users** (developers using AI CLIs) | Daemon only | One-liner curl, zero config |
| **Contributors** (developing qc-trace itself) | Full stack: Postgres, ingest server, daemon, dashboard | Manual dev setup (see [README](../README.md)) |

This doc covers the **user** install path.

---

## Table of Contents

- [Install Flow](#install-flow)
- [What the Installer Does](#what-the-installer-does)
- [Shared Instances & Identity](#shared-instances--identity)
- [Package Distribution (PyPI)](#package-distribution-pypi)
- [Auto-Update Strategy](#auto-update-strategy)
- [Service Management](#service-management)
- [Uninstall](#uninstall)
- [Verification](#verification)
- [Port Selection](#port-selection)
- [Installer Script Reference](#installer-script-reference)

---

## Install Flow

```
curl -fsSL https://quickcall.dev/trace/install.sh | sh
```

That's it. User restarts their terminal (or `source ~/.zshrc`) and the daemon is running.

**Idempotent:** If already installed, the script detects the existing installation and exits cleanly — no duplicate services, no config overwrites.

---

## What the Installer Does

```
Step 1: Check for existing installation
  - Daemon already running? (check PID file / systemctl / launchctl)
  - Service already registered?
  - If yes → print "already installed, version X" → exit 0

Step 2: Pre-flight checks
  - Python 3.11+ exists? (fail with clear message if not)
  - curl exists?
  - Not running as root? (warn)

Step 3: Install uv (if not present)
  - curl -LsSf https://astral.sh/uv/install.sh | sh
  - uv is a single static binary, no deps

Step 4: Configure shell
  - Detect .zshrc or .bashrc
  - Add marker-delimited config block:
      # >>> quickcall-trace >>>
      export PATH="$HOME/.local/bin:$PATH"
      alias quickcall-start="uv cache clean qc-trace >/dev/null 2>&1; uvx qc-trace@latest"
      # <<< quickcall-trace <<<
  - Idempotent: re-running updates the block in-place

Step 5: Install service (user-level, no root)
  - macOS: write plist to ~/Library/LaunchAgents/com.quickcall.traced.plist
  - Linux: write systemd user unit to ~/.config/systemd/user/quickcall.service
  - Service ExecStart uses: uvx --no-cache qc-trace@latest
  - Set QC_TRACE_INGEST_URL env var in the service definition

Step 6: Start daemon immediately
  - macOS: launchctl load <plist>
  - Linux: systemctl --user enable --now quickcall

Step 7: Verify
  - Send a heartbeat message to the central ingest server
  - Confirm 200 response
  - Print success or failure
```

---

## Shared Instances & Identity

### The setup

Teams often share EC2 instances. Each developer SSHs in with their own user account and works in their own directories (e.g. `/nikhil/repo-a/`, `/sagar/repo-b/`). Each user has a separate `$HOME`.

### How it works

- **Each developer installs independently** — runs `curl | sh` on their own account
- **One daemon per user** — watches that user's `~/.claude/`, `~/.codex/`, `~/.gemini/`, `~/.cursor/`
- **No interference** — daemons are user-level services with per-user PID files and state
- **If already installed** — the install script detects it and exits cleanly
- **Same flow on personal machines** — no special handling for shared vs personal

### Developer attribution

Sessions are attributed to developers via two signals already captured:

| Signal | Source | Example |
|--------|--------|---------|
| **Project path** | `raw_file_path` in session data | `~/.claude/projects/-nikhil-repo-a/session.jsonl` → developer is `nikhil` |
| **Git email** | `git config user.email` from the repo | `nikhil@company.com` — tracked in session metadata |

No per-developer configuration needed. No `QC_TRACE_USER` env var. The daemon captures what's already there.

### What this means for the dashboard

- Filter sessions by git email to see one developer's work
- Filter by project path / repo name for project-level views
- Session IDs isolate individual conversations regardless of shared infrastructure

---

## Package Distribution (PyPI)

### Why PyPI

- `install.sh` uses `uv tool install qc-trace` for the CLI + `uvx qc-trace@latest` for the service
- Version resolution handled automatically
- The daemon is pure Python stdlib — zero third-party dependencies
- The wheel is tiny (< 100KB)

### Public vs Private

| Option | Command | Trade-off |
|--------|---------|-----------|
| Public PyPI | `uvx qc-trace@latest` | Code is public |
| Private PyPI (CodeArtifact, Gemfury, etc.) | `uvx --index-url https://... qc-trace@latest` | Users need index URL baked into installer |
| GitHub Releases | `uvx qc-trace@latest --find-links https://github.com/.../releases/...` | Ugly, no version resolution |

**Recommendation:** Start with public PyPI. Move to private if/when the code contains proprietary logic.

### Publishing

```bash
# Bump version in pyproject.toml
uv build
twine upload dist/*
```

Or via GitHub Actions release workflow (already in `.github/workflows/release.yml`).

---

## Auto-Update Strategy

### How it works

The daemon uses `uvx qc-trace@latest` as its entry point. Combined with `uv cache clean`, this means:

1. **Every daemon restart** (crash, reboot, manual) fetches the latest version from PyPI
2. **No persistent install** — `uvx` creates a temporary venv, runs the package, cleans up
3. **No cache bloat** — `uv cache clean qc-trace` runs before each invocation
4. **No version pinning** — `@latest` always resolves to newest

### Self-check (Phase 2)

For faster update rollout without waiting for a restart, add a version check to the daemon loop:

```
Daemon main loop:
  poll files -> push -> sleep 5s
  every 6 hours -> GET https://pypi.org/pypi/qc-trace/json
    compare response["info"]["version"] vs qc_trace.__version__
    if newer -> log "update available, restarting" -> exit(0)
  launchd/systemd auto-restarts -> uvx fetches new version
```

This is ~20 lines of code using `urllib.request` (already imported in the daemon). Updates land within 6 hours even if the daemon never crashes.

### PyPI version check (stdlib only)

```python
import json
import urllib.request
from qc_trace import __version__

def check_for_update(package: str = "qc-trace") -> str | None:
    """Return newer version string if available, else None."""
    url = f"https://pypi.org/pypi/{package}/json"
    try:
        with urllib.request.urlopen(url, timeout=10) as resp:
            data = json.loads(resp.read())
            latest = data["info"]["version"]
            if latest != __version__:
                return latest
    except Exception:
        pass  # network issues should never crash the daemon
    return None
```

---

## Service Management

### macOS (launchd)

User-level plist at `~/Library/LaunchAgents/com.quickcall.traced.plist`:

```xml
<key>ProgramArguments</key>
<array>
    <string>/bin/sh</string>
    <string>-c</string>
    <string>
        export PATH="$HOME/.local/bin:$PATH"
        uv cache clean qc-trace 2>/dev/null
        exec uvx --no-cache qc-trace@latest daemon
    </string>
</array>

<key>EnvironmentVariables</key>
<dict>
    <key>QC_TRACE_INGEST_URL</key>
    <string>https://trace.quickcall.dev/ingest</string>
</dict>

<key>RunAtLoad</key>
<true/>

<key>KeepAlive</key>
<true/>
```

- `RunAtLoad` — starts on login
- `KeepAlive` — auto-restarts on crash (which triggers update)

### Linux (systemd user unit)

User-level unit at `~/.config/systemd/user/quickcall.service` (no root required):

```ini
[Unit]
Description=qc-trace daemon
After=network-online.target

[Service]
Type=simple
Environment=QC_TRACE_INGEST_URL=https://trace.quickcall.dev/ingest
ExecStartPre=/bin/sh -c 'uv cache clean qc-trace 2>/dev/null; true'
ExecStart=%h/.local/bin/uvx --no-cache qc-trace@latest daemon
Restart=on-failure
RestartSec=10

[Install]
WantedBy=default.target
```

Enable with `systemctl --user enable --now quickcall` — no `sudo`, no root.

Requires `loginctl enable-linger $USER` for the service to run without an active login session (the installer handles this).

---

## Uninstall

```bash
curl -fsSL https://quickcall.dev/trace/uninstall.sh | sh
```

Or built-in:

```bash
quickcall uninstall
```

What it does:

1. Stop and remove launchd/systemd service
2. Kill daemon process if running
3. Remove shell config block (between markers)
4. `uv cache clean qc-trace`
5. Preserve `~/.quickcall-trace/` data directory (user can `rm -rf` manually)

---

## Verification

### Heartbeat on install

The installer's final step sends a test message to the central ingest server:

```bash
curl -s -X POST "$QC_TRACE_INGEST_URL" \
  -H 'Content-Type: application/json' \
  -d "[{
    \"id\": \"install-$(hostname)-$(date +%s)\",
    \"session_id\": \"install-verify\",
    \"source\": \"qc_trace_install\",
    \"msg_type\": \"system\",
    \"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",
    \"content\": \"install ok — $(whoami)@$(hostname) — $(uname -s) — python $(python3 --version 2>&1)\",
    \"source_schema_version\": 1
  }]"
```

This shows up immediately in the central dashboard with the username and hostname.

### Ongoing monitoring (Phase 2)

Add version and device info to every push the daemon makes. The central dashboard shows:

| User | Device | OS | Daemon Version | Last Seen | Status |
|------|--------|----|---------------|-----------|--------|
| nikhil | shared-ec2-1 | Linux | 0.2.1 | 2 min ago | healthy |
| sagar | sagar-mbp | Darwin | 0.2.1 | 5 min ago | healthy |
| sagar | gpu-instance | Linux | 0.2.0 | 3 hours ago | outdated |

---

## Port Selection

Default port: **19777**

Why:
- Port 7777 conflicts with Cursor IDE
- Ports 3000-9999 conflict with common dev servers (React, Vite, Flask, Rails, etc.)
- Port 19777 is in an uncommon range, not registered with IANA
- Memorable (echoes 7777)

Configurable via:
- `QC_TRACE_PORT` — server side
- `QC_TRACE_INGEST_URL` — daemon side (full URL override)

For user installs, the port doesn't matter — the daemon points to a remote URL (`https://trace.quickcall.dev/ingest`), not localhost.

---

## Installer Script Reference

### Key design decisions

1. **Idempotent** — detects existing installation, exits cleanly
2. **User-level only** — no root, no sudo, no system-wide changes
3. **uv over pip** — single binary, no bootstrap problems, handles venvs automatically
4. **`uv tool install` for CLI + `uvx @latest` for service** — persistent `quickcall` command for interactive use, auto-updating service via uvx
5. **`uv cache clean` before run** — prevents stale cache issues
6. **Marker-delimited shell config** — idempotent, safe to re-run, clean removal
7. **Service-managed daemon** — runs on boot, auto-restarts on crash, no user intervention
8. **Heartbeat verification** — confirms install worked end-to-end

### What the user needs pre-installed

- Python 3.11+ (the only hard requirement)
- Everything else is auto-installed by the script

### What gets installed

```
~/.local/bin/uv              # uv binary (if not already present)
~/.local/bin/uvx             # uvx binary (comes with uv)
~/.quickcall-trace/                 # daemon data directory
  state.json                 # file processing state
  quickcall.pid              # PID file
  quickcall.log              # stdout log
  quickcall.err              # stderr log

# macOS:
~/Library/LaunchAgents/com.quickcall.traced.plist

# Linux:
~/.config/systemd/user/quickcall.service
```

Shell config additions (between markers):
```bash
# >>> quickcall-trace >>>
export PATH="$HOME/.local/bin:$PATH"
alias quickcall-start="uv cache clean qc-trace >/dev/null 2>&1; uvx qc-trace@latest"
# <<< quickcall-trace <<<
```
