@fayz-ai/cli
v0.12.0
Published
Fayz CLI — scaffold, validate, extract and upgrade repo-per-app projects
Readme
@fayz-ai/cli — fayz
The Fayz SDK command-line tool. Scaffolds repo-per-app projects, validates
manifests/architecture boundaries, assists code-config → manifest migration, and
provisions an app's Supabase database from its installed @fayz-ai/* packages.
fayz --help
fayz --versionCommands
| Command | What it does |
|---------|--------------|
| fayz create <storefront\|admin\|member> <name> | Scaffold a new repo-per-app project |
| fayz create plugin <name> | Scaffold an app-local (incubator) plugin |
| fayz doctor [dir] | Validate manifest + architecture boundaries |
| fayz extract [dir] | Assisted code-config → manifest migration |
| fayz db apply [dir] | Plan / apply the Supabase migration order |
| fayz db apply-bundle [dir] --bundle <manifest> | Validate / apply one exact reconciliation bundle |
| fayz db pool status | Show the registered projects and their migration ledgers |
| fayz db pool apply cluster-squash-test --app <dir> | Ledger-gated apply of an app's plan to the shared live cluster |
| fayz db fan-out --app <dir> | Apply an app's plan across active registry projects (currently the shared cluster only) |
| fayz db pool move-tenant | Plan or execute a backed-up, verified tenant move between registered projects |
fayz db apply
Provisions an app's Supabase database by resolving SQL from the app's installed
@fayz-ai/* packages (never a sibling ../../fayz-sdk checkout), so an external
developer with only published deps can provision a fresh project.
Working inside the monorepo? See
docs/delivery/LOCAL-DEV.mdfor how an app resolves@fayz-ai/*from local SDK source vs. published packages (FAYZ_SDK_SOURCE, the*:published-sdkscripts).
The plan is ordered:
- spine —
@fayz-ai/dbmigrations/*.sql(Ring-0saas_core) - drizzle — app
drizzle/*.sql(generated table DDL) - seed — app
supabase/seed-saas-core.sql - plugin — each enabled plugin's
src/migrations/*.sql, inapp.manifest.jsonorder - incubator — app-local
src/plugins/<name>/migrations/*.sql
Applying runs each step's SQL via the Supabase Management API
(POST /v1/projects/{ref}/database/query, bearer SUPABASE_PAT) in plan order,
then issues NOTIFY pgrst, 'reload schema' so PostgREST picks up new
functions/views. Because plugin/spine SQL is authored idempotent, re-running after
a failure is safe — apply stops at the first bad file and names the step/file.
Usage
# Print the ordered plan only — zero network, no env required:
fayz db apply ./my-app --dry-run
# Apply for real (prompts to confirm the target project ref):
fayz db apply ./my-app
# Non-interactive (CI): skip the prompt:
fayz db apply ./my-app --yes
# Reconcile a partially provisioned live database with one reviewed bundle:
fayz db apply-bundle ./my-app --bundle ./printing-v2.bundle.json --yes --allow-critical[dir] defaults to the current directory.
Flags
| Flag | Effect |
|------|--------|
| --dry-run | Print the ordered plan only; performs no network calls |
| --yes, -y | Skip the confirmation prompt (required in non-interactive shells) |
| --spine-only | Apply only the @fayz-ai/db spine |
| --plugins-only | Apply only plugin + incubator migrations |
| --only-plugins a,b | Restrict plugin and app-local incubator steps to the named ids |
| --allow-critical | Required with --yes for a bundle or a live/data-critical pool |
--spine-only and --plugins-only are mutually exclusive.
db apply-bundle is a separate fail-closed subcommand: older CLI binaries reject it instead of
silently interpreting it as a normal full-plan apply. It accepts --bundle <manifest> and no
discovery/filter flags. Its dry-run is offline; a real apply always requires both --yes and
--allow-critical.
The bundle manifest is strict and versioned: schemaVersion: 1, one exact bundle, and a non-empty
satisfies list. Every entry names an app-relative incubator SQL path plus its sha256-lf-v1
checksum. Unknown keys, traversal, duplicate ledger identities, and checksum drift are refused.
Environment contract
Required for a real apply (never for --dry-run); never defaulted:
| Variable | Alias | Where to get it |
|----------|-------|-----------------|
| SUPABASE_PROJECT_REF | SUPABASE_REF | Dashboard → Project Settings → General (also the project URL subdomain) |
| SUPABASE_PAT | SUPABASE_ACCESS_TOKEN | Dashboard → Account → Access Tokens → Generate new token |
Resolution order: process env → <app>/.env.local → <app>/.env. Files never
override an already-set process variable. If either credential is missing, apply
exits non-zero naming both required vars. In a non-interactive shell without
--yes, apply refuses rather than hangs waiting on stdin.
Database pools (Runner v2) — fayz db pool / fayz db fan-out
The registry can describe one or more Supabase pools; tenant_id + RLS remains
the isolation inside each active project. The shipped registry currently points
all apps at one shared live cluster and retains the former industry pools only
as DECOMMISSIONED history. Runner v2 adds a migration ledger and pool-aware
apply on top of the same plan builder.
The ledger (public._migrations)
Every applied SQL file is recorded with its canonical sha256-lf-v1 checksum
(plugin_id + file_name + checksum, service-role only, RLS deny-all through
PostgREST). On apply, each file is gated:
| Ledger state for the file | Action |
|---------------------------|--------|
| absent | apply, then record it |
| present, same checksum | skip (skip (applied)) |
| present, different checksum | HARD STOP — MigrationDriftError |
Never edit an applied migration. A drift stop means the file changed after it was applied to that pool; author a new migration file instead.
Canonical hashes decode strict UTF-8, remove a BOM, normalize CRLF/CR to LF, trim trailing
whitespace, and add one final newline; that exact canonical snapshot is what the runner executes.
Existing rows with a null/sha256-raw-v1 format retain compatibility with equivalent LF/CRLF/CR
and BOM representations. Bundle rows use satisfied_by_plugin_id +
satisfied_by_file_name; bundle SQL, its own ledger row, and all absent source rows commit in one
database transaction.
fayz db apply and the pool commands are ledger-gated.
After any canonical (sha256-lf-v1) apply, do not use a CLI older than this release against the
same project: legacy runners compare raw platform-specific bytes and can report false drift.
Operational automation must pin the minimum CLI version that introduced db apply-bundle.
Pool registry (two sources)
Pool refs/flags come from a registry with two interchangeable sources:
- Source A (default): the static
pools.config.jsonshipped with the CLI. Override with--pools-file <path>. Fields per pool:industry, name, ref, url, status (ACTIVE|PROVISIONING|DECOMMISSIONED), flags. Flags:canary,dataCritical,preserveBespoke. - Source B (later): the platform Prisma Postgres registry via
FAYZ_REGISTRY_URL/DATABASE_URL. The interface is defined (createPostgresPoolRegistry) but not yet wired — it currently throwsregistry not yet provisioned; use --pools-file. Wiring it means adding a Postgres client (pg); the CLI is kept dependency-free until then.
The access token comes from SUPABASE_PAT / SUPABASE_ACCESS_TOKEN. The
project ref is NOT read from env for pool commands — it comes from the pools
file per pool.
Commands
The operational target in the shipped registry is cluster-squash-test (industry
alias shared, ref klvfxzreepavcpyjiwla). Do not target the former industry
names such as restaurant: those entries are historical and an apply to a
DECOMMISSIONED entry is refused.
# Show each pool's ledger head (plugin_id → latest file/version). "NO LEDGER"
# means the table is absent (pool never provisioned). Add --app <dir> to compare
# against a locally-built plan head.
fayz db pool status [--pools-file f] [--app dir]
# Ledger-gated apply of ONE app's plan to the shared live cluster. The app
# determines the plugin set. Inspect the exact plan before every live apply.
fayz db pool apply cluster-squash-test --app <dir> --dry-run
fayz db pool apply cluster-squash-test --app <dir> --yes --allow-critical [--spine-only --plugins-only --only-plugins a,b]
# Fan-out remains available for alternate registries. With the shipped registry,
# it resolves only the one ACTIVE shared project; DECOMMISSIONED history is
# skipped. Projects are processed sequentially and FAIL-FAST.
fayz db fan-out --app <dir> [--industry all|<slug>] [--canary <pool>] [--yes] [--allow-critical]
# Tenant move: dry-run plans and counts; --yes writes a JSON backup before any
# mutation, copies parent-first, verifies the target, then deletes child-first.
# A live source or target also requires --allow-critical; target must be ACTIVE.
fayz db pool move-tenant --from <source> --to <target> --tenant <uuid> [--yes --allow-critical]Data-critical gate
A pool flagged live: true or dataCritical: true (real production data) refuses to be mutated
unless both --yes and --allow-critical are passed — in pool apply,
fan-out (any live/dataCritical project in the wave), and either side of
move-tenant. A move target must also be ACTIVE; historical sources remain
available for an explicit recovery.
