Skip to main content

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.

No shell? Use the hosted MCP connector instead. Graftport also serves this whole workflow as a hosted MCP server — add it to Claude / ChatGPT as a connector and sign in with your Graftport account, nothing to install. See docs/architecture/agent-mcp.md (engineering) or the user docs under Automation. This CLI remains the tool for shell-based coding agents and human operators.

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

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:

# 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:

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:

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:

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"

Checking the whole feed before publishing

validate samples. For a SQL mapping, --whole-feed runs the same draft over every row of the feed's columnar artefact instead:

graftport mappings validate <mapping_id> --jsonata draft.sql --whole-feed --pretty

It answers a different question from the sampled form — not "does this do what I meant" but "does it hold" — and the difference is the reading it gives back: failures counted by kind over the whole feed, so a code affecting three rows sits next to one affecting forty thousand instead of being buried under it. A required field missing on one row in five thousand is invisible in a sample of twenty and fatal in a load.

Server-side it is the run's own reader and evaluation, so the answer is the run's answer. It reads the whole feed, so it takes seconds rather than being instant, and it needs the feed to have been extracted at least once — a feed with no artefact says so and the sampled form still works. truncated in the output means the read hit its row cap, so a clean result is clean so far.

Exit code is the same gate as the sampled form: non-zero if anything failed.

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:

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:

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:

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:

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 for the tag → PyPI flow.

Metadata

Release files for graftport 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for graftport 0.2.1
File Size Uploaded Provenance
graftport-0.2.1.tar.gz 77.1 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for graftport 0.2.1
File Interpreter ABI Platform
graftport-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 168.8 kB

Release files / graftport-0.2.1.tar.gz

Download URL graftport-0.2.1.tar.gz
Size 77.1 kB
Tags Source
SHA-256 checksum
How to use checksums
efb1d3b1bdea279eef04ffdba3dee434c5ed215f8601e96347ce7ccbdf9965a8
BLAKE2b-256 checksum
How to use checksums
7b95a35f1ed42fc269c1a57c8bfd4e5beba1de1220b0cf3c4841059cb5f5dc9d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 6, 2026.

Transparency log

Release files / graftport-0.2.1-py3-none-any.whl

Download URL graftport-0.2.1-py3-none-any.whl
Size 91.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8a29e253f7a9a1f4fecf6f6329734995b32a6c2bd1ea113e71766f05eee025ed
BLAKE2b-256 checksum
How to use checksums
a737a977465c5eba3887b69da8c45015e113c518a33a5f876b694331b6d0de9c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 6, 2026.

Transparency log

Security for graftport 0.2.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for graftport-0.2.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 6, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page