CLI for AI-assisted JSONata mapping iteration against Graftport
Project description
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
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"
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 startwithout--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/validateapp_url— the Graftport web app used for the browser sign-inapikey— the project's anon keyaccess_token— the Supabase access JWTrefresh_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):
- The CLI picks a free
127.0.0.1port and starts a one-shot HTTP server on it. - 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). - 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. - 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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file graftport-0.1.0.tar.gz.
File metadata
- Download URL: graftport-0.1.0.tar.gz
- Upload date:
- Size: 42.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
edb13d0e2ef0dc5147161c5bb7d14cfd4605dae4faf7e5bb339b2d0b6e86bb4a
|
|
| MD5 |
6c0a0e851154f6ffc18300fb850fa0d9
|
|
| BLAKE2b-256 |
141621d05a6878eafd32ba79d6ec6be836ebd13623c8e0e37d954d1cbe31abd6
|
Provenance
The following attestation bundles were made for graftport-0.1.0.tar.gz:
Publisher:
publish-graftport.yml on bytetide-io/mono
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
graftport-0.1.0.tar.gz -
Subject digest:
edb13d0e2ef0dc5147161c5bb7d14cfd4605dae4faf7e5bb339b2d0b6e86bb4a - Sigstore transparency entry: 1577383784
- Sigstore integration time:
-
Permalink:
bytetide-io/mono@6a30b6edaa81018ff692db7640fae05de0565953 -
Branch / Tag:
refs/tags/graftport-v0.1.0 - Owner: https://github.com/bytetide-io
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-graftport.yml@6a30b6edaa81018ff692db7640fae05de0565953 -
Trigger Event:
push
-
Statement type:
File details
Details for the file graftport-0.1.0-py3-none-any.whl.
File metadata
- Download URL: graftport-0.1.0-py3-none-any.whl
- Upload date:
- Size: 52.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5196182231d18d86434141919c14dc9d989c5281cbefa597464c612639ca69ef
|
|
| MD5 |
df1eb468e5933d03b04ae7c56f947e6e
|
|
| BLAKE2b-256 |
c8c450fdf1116c8272ac36ce5dea07b2533aedb7bbd9f91e9825e4262c3ff203
|
Provenance
The following attestation bundles were made for graftport-0.1.0-py3-none-any.whl:
Publisher:
publish-graftport.yml on bytetide-io/mono
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
graftport-0.1.0-py3-none-any.whl -
Subject digest:
5196182231d18d86434141919c14dc9d989c5281cbefa597464c612639ca69ef - Sigstore transparency entry: 1577384045
- Sigstore integration time:
-
Permalink:
bytetide-io/mono@6a30b6edaa81018ff692db7640fae05de0565953 -
Branch / Tag:
refs/tags/graftport-v0.1.0 - Owner: https://github.com/bytetide-io
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-graftport.yml@6a30b6edaa81018ff692db7640fae05de0565953 -
Trigger Event:
push
-
Statement type: