Metadata-Version: 2.4
Name: graftport
Version: 0.1.1
Summary: CLI for AI-assisted JSONata mapping iteration against Graftport
Author: Graftport
License: Proprietary
Keywords: cli,graftport,jsonata,migration,shopify
Requires-Python: >=3.12
Requires-Dist: httpx>=0.28.0
Requires-Dist: typer>=0.12.0
Description-Content-Type: text/markdown

# graftport

CLI for AI-assisted Graftport migration engineering. Designed to be
driven by a coding agent (Claude Code, Cursor, Windsurf, Copilot
Workspace, …) over its shell tool — works anywhere a shell does, no
MCP support required.

The user creates the migration (project, credentials, resource scope)
in the Graftport UI; the agent then acts as a migration engineer:
investigate source data, iterate and publish mappings, run dry-runs
to evaluate their output, triage load failures, and judge whether
the migration is ready. **Cost-incurring actions (real loads,
cancels, retries) are composed and presented for human approval —
never executed autonomously.**

## Install

```sh
uv tool install graftport          # recommended (once published)
# or
pip install graftport
```

### Installing from this repo (before the PyPI release)

The CLI hasn't been published yet, so for now install it directly
from the monorepo. Run from the repo root:

```sh
# Editable install — picks up local edits without re-installing.
uv tool install --editable ./graftport

# Or with plain pip / pipx:
pipx install --editable ./graftport
# or
pip install -e ./graftport
```

After install, `graftport --version` should print `graftport 0.1.0`.
`uv tool uninstall graftport` (or `pipx uninstall graftport`) reverses
the install cleanly.

If you'd rather not install at all, you can run the CLI from a
checkout via uv:

```sh
cd graftport
uv run graftport --help
```

### Pointing at a local Graftport stack

`graftport auth login` defaults to the production URLs. For local
dev pass the dev URLs explicitly. With a local Supabase, the
FastAPI on `:8000`, and the Next.js app on `:3000`:

```sh
graftport auth login \
    --postgrest-url   http://127.0.0.1:54321/rest/v1 \
    --apikey          "$NEXT_PUBLIC_SUPABASE_ANON_KEY" \
    --validate-url    http://127.0.0.1:8000 \
    --app-url         http://127.0.0.1:3000
```

The browser flow opens `<app-url>/cli-auth`, waits for you to sign
in, and writes the Supabase session (including the refresh token) to
`~/.graftport/config.json`. The Supabase JWT carries the
`app.tenant_id` claim, so RLS scopes everything to your dev tenant.
To skip the browser flow on a fully headless setup, pass
`--access-token "<jwt>"` directly.

## Quick start

The hosted Graftport project URL and the public anon key are baked
into the wheel, so the only thing `auth login` needs is your browser:

```sh
graftport auth login               # opens the browser to sign in
graftport mappings list <migration_id>
graftport mappings show <mapping_id> --raw > current.jsonata
graftport source rows <migration_id> product --limit 5 > samples.json
# edit current.jsonata
graftport mappings validate <mapping_id> --jsonata current.jsonata --limit 50 --pretty
graftport mappings publish <mapping_id> --jsonata current.jsonata --notes "fix AMOUNT_MISMATCH"
```

## Command tree

| Group | Commands | State |
|---|---|---|
| `auth` | `login` / `status` / `logout` | local config |
| `migrations` | `list` / `show` / `resources` | read-only |
| `mappings` | `list` / `show` / `validate` | read-only |
| `mappings` | `publish` | agent-allowed |
| `runs` | `list` / `show` / `status` (`--watch`) / `estimate` | read-only |
| `runs` | `start --dry-run` | agent-allowed |
| `runs` | `start` (real) / `cancel` | **human-gated** |
| `records` | `failures` / `errors` / `show` / `loaded` | read-only |
| `records` | `retry` | **human-gated** |
| `source` | `rows` / `raw` | read-only |
| `skill` | (bare) / `install` | local files |

Every command emits JSON on stdout by default and accepts `--pretty`.
Exit codes: `0` ok, `1` error, `2` usage, `3` a human-gated action was
declined (or validation failed).

## Human-approval contract

State-changing actions split into two tiers:

**Agent-allowed** (no human gate, no `--yes` required):

- `mappings publish` — metadata write that flags downstream records
  for re-load on the *next* run, which is itself gated.
- `runs start --dry-run` — computes payloads without pushing to
  Shopify; zero load cost.

**Human-gated** (interactive `yes` required; `--yes` bypass for a
human operator scripting the CLI):

- `runs start` without `--dry-run` — costs Shopify API + platform
  credits per loaded record.
- `runs cancel` — destructive; may lose in-flight work.
- `records retry` — re-loads one record; costs.

The gated commands print the resolved action (and, for `start`, a
live cost estimate) to **stderr** and then block. The bundled skill
forbids the agent from ever passing `--yes` on a gated command — the
agent composes the command, presents it, and stops. A declined gate
exits `3`, distinct from a command failure (`1`).

The CLI consumes only the **existing** PostgREST views/RPCs and the
FastAPI `/api/validate` endpoint — no new backend endpoints or schema.

## The agent skill document

Every install of `graftport` ships with a Markdown skill document
describing the iterative workflow, the validation error codes, and
JSONata patterns that fix each one.

The fastest way to get it in front of your coding agent is to install
it directly. The skill teaches an agent how to use the CLI in
general, so it always installs **user-globally** — once per machine,
not per project:

```sh
graftport skill install --pretty
```

That auto-detects which coding agents are installed for your user and
writes the right file for each. Supported targets:

| Agent | Where the skill lands | Notes |
|---|---|---|
| Claude Code _and_ Claude Desktop | `~/.claude/skills/graftport-migration-engineer/` | One install serves both products — Anthropic unified the on-disk location. |
| Windsurf | `~/.codeium/windsurf/memories/global_rules.md` | Idempotent block-merge into your global rules file. |

This skill supersedes the earlier `iterating-graftport-mappings`
skill. On install for Claude, a stale
`~/.claude/skills/iterating-graftport-mappings/` directory is removed
automatically — but only when its `SKILL.md` frontmatter confirms it
is the old graftport skill. An unrelated skill that happens to sit at
that path is left untouched.

Targeted install:

```sh
graftport skill install --for claude         # just one agent
graftport skill install --for all            # write everything
graftport skill install --force              # overwrite a customised file
```

**Cursor, Copilot Workspace, and AGENTS.md** are not supported by
`install` — Cursor's User Rules live only in the Settings UI, Copilot
reads `.github/copilot-instructions.md` per-repo only, and AGENTS.md
is project-scoped by definition. For those, run
`graftport skill > skill.md` and place / paste the file by hand.

## Output

Every command emits JSON on stdout by default. `--pretty` switches to a
human-readable rendering. Exit code is `0` only when every sampled row
passes validation.

## Authentication

`graftport auth login` opens your browser, sends you through the same
sign-in the dashboard uses, and asks you to authorize the CLI on a
single "Authorize Graftport CLI" screen. It then stores the session
in `~/.graftport/config.json`:

- `postgrest_url` — the Supabase PostgREST root (`.../rest/v1`)
- `validate_url` — the FastAPI deployment that exposes `/api/validate`
- `app_url` — the Graftport web app used for the browser sign-in
- `apikey` — the project's anon key
- `access_token` — the Supabase access JWT
- `refresh_token` — used to silently mint a new access JWT when the
  current one expires (every CLI call refreshes on demand on 401)
- `expires_at` — epoch seconds for the access token

The URLs and anon key default to hosted Graftport, baked into the
wheel at publish time. Tokens are managed for you; you should only
need to re-run `graftport auth login` when the refresh token expires
or you explicitly `auth logout`.

### How the browser flow works

This is the standard RFC 8252 loopback OAuth pattern (the same
`gh auth login --web`, `vercel`, `supabase login`, and `gcloud auth
login` use):

1. The CLI picks a free `127.0.0.1` port and starts a one-shot HTTP
   server on it.
2. The CLI opens your browser to
   `<app-url>/cli-auth?state=…&port=…&v=1` (and prints the URL too,
   in case your browser doesn't open).
3. The web app makes sure you are signed in, asks you to authorize
   the CLI, and POSTs the resulting Supabase session to
   `http://127.0.0.1:<port>/callback`.
4. The CLI validates the `state`, writes the session to disk, and
   the browser tab shows "You can close this tab".

If you are on a server without a browser, pass `--no-browser` to
print the URL and open it on a different machine. The CLI keeps
listening on the same loopback port.

### Headless / CI

If you already have a JWT (CI pipeline, automation script, headless
container without an available browser), pass it directly and skip
the browser flow:

```sh
graftport auth login \
    --postgrest-url https://<your-project>.supabase.co/rest/v1 \
    --apikey        "$YOUR_PUBLIC_ANON_KEY" \
    --validate-url  https://<your-fastapi-deploy>.example.com \
    --access-token  "$YOUR_JWT"
```

Configs written this way have no `refresh_token`, so you'll need to
re-run `auth login` when the JWT expires.

### Self-hosted

Override every URL via flags or `GRAFTPORT_DEFAULT_*` environment
variables — useful for pointing the browser flow at a local Next.js
dev server:

```sh
graftport auth login \
    --postgrest-url https://<your-project>.supabase.co/rest/v1 \
    --apikey        "$YOUR_PUBLIC_ANON_KEY" \
    --validate-url  https://<your-fastapi-deploy>.example.com \
    --app-url       https://<your-web-app>.example.com
```

## Local development

The repo this CLI lives in (`bytetide-io/mono`) ships an older
local-only harness at `scripts/test_mapping.py` that connects to
Postgres directly with `psycopg2`. It's still there for cases where
you have local DB credentials and want to skip the HTTP round-trip,
but the canonical agent-facing tool is `graftport`.

## Cutting a release

See [`PUBLISHING.md`](./PUBLISHING.md) for the tag → PyPI flow.
