CLI
Every command of the Intelligo CLI — create, add, doctor, migrate and upgrade — documented from its own source.
A scaffolded app has @intelligo-dev/cli as a dev dependency; run it with pnpm exec intelligo.
intelligo <command>
intelligo create [dir] Scaffold an app, then install the registry pages you pick
(--items a,b | --all, --yes, --no-install, --name <name>)
intelligo doctor Report configuration and migration-chain problems
intelligo migrate Apply the framework's migration chain to DATABASE_URL
intelligo migrate --check Compare the framework's and the app's migrations to a database
(--json: one object whose `state` is up_to_date | pending |
fresh | ahead | unmanaged | legacy)
intelligo admin grant <email> Make a signed-up user a platform admin (--force in production)
intelligo add <feature> Generate consumer-owned source (--force to overwrite)
intelligo upgrade --check Show what a template upgrade would change
intelligo sync [items…] Install registry pages from this release's registry
(names space- or comma-separated),
keeping seams and merging messages (--force replaces
hand-edited files; --check only reports, exit 1 on drift)create
A new application on the framework.
This is onboarding, not the product boundary: what it writes is the
consumer’s from the moment it lands, and the enduring relationship is the versioned packages, not this
scaffold. So it generates through the same manifest machinery as
add, which means the very first upgrade already knows which files
you have since edited.
In a terminal it asks for the project name when none is given, then
which registry pages to install (--items a,b or --all answer that
without asking). The pages are installed by intelligo sync — one
shadcn add per item, from the registry this CLI carries — after the
dependencies (from the root of a parent pnpm workspace when the app is
a member of one), and only once you approve the exact commands, or
pass --yes. --no-install stops after the scaffold and prints them
instead.
add
Generate consumer-owned source.
“Owned” is the operative word: the files land in the consumer’s repository and Intelligo stops deciding what is in them. The manifest records what was written and its hash so a later upgrade can tell an untouched file from one the consumer has made their own, and refuse to overwrite the latter.
| Feature | What it generates |
|---|---|
pnpm-standalone |
pnpm settings for an app that is its own workspace root: dependency build scripts declined, so pnpm 10+ installs without a prompt, and pnpm add allowed at the root for the shadcn CLI. intelligo create writes it when pnpm installs an app outside any workspace — pnpm-workspace.yaml, .npmrc |
admin-page |
Mount the Intelligo operational console at /admin, styled with the app’s tokens, for platform admins only — app/[locale]/admin/page.tsx |
maintenance |
A CRON_SECRET-gated GET /api/cron/maintenance that reconciles stale executions, drops expired reservations and rate-limit buckets, expires trials and prunes old jobs — scheduled every five minutes, in vercel.json when the app has none — app/api/cron/maintenance/route.ts |
vitest |
A Vitest setup for the app’s own tests: the @ alias, a server-only stub, and the @intelligo-dev/* packages inlined so the stub reaches them — vitest.config.ts, tests/stubs/server-only.ts |
doctor
Report the problems that are invisible until they are an incident.
migrate
Apply the framework’s migration chain.
A consumer application has two migration chains in one database:
the framework’s, shipped inside @intelligo-dev/core (its .sql
files and journal are in the package’s files), and its own, which
drizzle-kit generates from the tables the application owns. They
must not share a journal — drizzle-kit applies by timestamp, so a
framework migration published after the consumer generated one of
theirs would be silently skipped — and a consumer cannot write into
node_modules anyway. So the framework chain is applied by this
command, into drizzle’s default drizzle.__drizzle_migrations
table, and the consumer’s chain by drizzle-kit migrate from their
own drizzle.config.ts, into a table of its own (the scaffold sets
migrations.table to __app_migrations).
Migrations are selected by content hash (what migrate --check
compares), not by drizzle’s journal-timestamp rule, and are recorded
in the same table drizzle’s migrator writes — see
readPendingMigrations for why.
The one thing this refuses to do is guess. A database with the
framework’s tables but no migration records was provisioned with
db:push; applying the whole chain to it would fail part-way (not
every migration is IF NOT EXISTS-guarded) and leave the records
half-written. Baselining is the fix, and it is deliberately a manual
step — see packages/core/src/db/migrations/README.md.
migrate –check
Answers “would deploying this code against that database work?” without changing anything. Two failure modes matter and neither is visible from the code alone:
- migrations the database has not applied yet (deploying now runs code against an older schema);
- migrations the database has applied that this checkout does not contain (the database is ahead — usually a rollback in progress).
Drizzle records applied migrations in drizzle.__drizzle_migrations
by content hash. A database provisioned with db:push has the
schema but no rows there at all, which this reports distinctly:
“unmanaged” is a different problem from “behind”, and baselining is
the fix (see the migrations README).
The framework’s chain is one baseline. A database that ran the pre-1.0
chain holds its hashes, which legacy-chain.json (next to the
journal) lists: they are reported as legacy, not as unknown, and a
database holding all of them is adoptable — its schema is the
baseline’s, so migrate records the baseline without running it.
The exit code is 1 whenever anything is pending or the database is
ahead, which a brand-new database and a stale one share. A deploy
gate that must tell them apart reads migrate --check --json: one
JSON object on stdout, same exit code, whose state is
up_to_date— every migration is applied;pending— a migrated database is behind this checkout;fresh— no migration records and none of the framework’s tables: an empty database,migrateapplies the chain;unmanaged— the tables exist with no records (db:push);ahead— applied migrations this checkout does not contain;legacy— the pre-1.0 chain;adoptablesays whethermigratecan take it over.
Beside state it carries exitCode, chain, applied, pending,
unknown, legacy, legacyMissing and adoptable. state describes
the framework’s chain; app is the application’s own chain
(app-chain-check.ts) — { chain, applied, pending, unknown }, or
null when the app owns none — and exitCode covers both.
upgrade –check
Reports what a template upgrade would do, and does nothing. Upgrades never overwrite consumer source, so the interesting output is not “these templates changed” but “these changed AND you have edited them” — the set where the consumer has to make a decision.
sync
Keeps an app’s installed registry pages exactly what the framework
ships. Installing stays the shadcn CLI’s job — every item goes in with
shadcn add <item> --yes --overwrite — and this command adds what
shadcn cannot know:
- the version: items come from the registry bundled with this CLI, so
they match the
@intelligo-dev/*packages of the same release; - the order: an item that imports a sibling’s files lands after it
(requires.json
items); - the seams: files an item ships once for the deployment to own
(requires.json
seams) are put back after the install, and message files are merged key by key, the app’s copy winning; - the record: intelligo.manifest.json keeps each installed file’s
hash, so
--checkcan tell a file edited by hand from one a newer registry replaced. Scaffold files an install replaces (globals.css, the theme provider) leave theapp-scaffoldrecord for this one, soupgrade --checkstops calling them customized.
--check installs nothing and exits 1 when any installed file is
missing, edited or behind the registry — the gate CI runs.