@intelligo-dev/cli
The Intelligo CLI: scaffold an AI SaaS app, generate owned source, and check its migrations and upgrades.
Install
pnpm add -D @intelligo-dev/clicreate needs no install — run it with pnpm dlx. The scaffold adds the CLI
to the new application, which is where intelligo comes from afterwards.
Commands
pnpm dlx @intelligo-dev/cli create my-app # a registry-ready Next.js app, plus the pages you pick
pnpm dlx @intelligo-dev/cli create my-app --items chat,billing-settings --yes # no questions
pnpm dlx @intelligo-dev/cli create . --name "Acme Audit" # into the current, empty directory (a .git may be there)
intelligo add <feature> # generate consumer-owned source (admin-page, maintenance, vitest)
intelligo doctor # what is misconfigured, and why it matters
intelligo migrate # apply the framework chain
intelligo migrate --check [--json] # compare the chain with the database, change nothing
intelligo admin grant <email> # make a signed-up user a platform admin
intelligo upgrade --check # what a template upgrade would change
intelligo sync [items…] # install registry pages from this release, seams kept
intelligo sync --check # exit 1 when an installed page drifted from the registrysync: installed pages stay the registry’s
A page arrives as source, through shadcn add, and nothing stops it drifting
afterwards — an edit here, a release skipped there. intelligo sync installs
items from the registry bundled with this CLI, so the pages match the
@intelligo-dev/* packages of the same version, in dependency order. Files an
item ships once for you to own (lib/*-config, lib/nav-config.ts… — the
seams in the registry’s requires.json) are put back after the install, and
message files are merged key by key with your copy winning. The result is
recorded in intelligo.manifest.json.
Name the items once — intelligo sync intelligo app-shell chat … — and
afterwards a bare intelligo sync updates them. It refuses to overwrite a file
you edited by hand unless you pass --force: move the change into a seam
first. intelligo sync --check installs nothing, lists every file that is
missing, edited, outdated, messages-behind or locale-behind, and
exits 1 on any — the gate to run in CI.
Other locales
The registry ships English (messages/en/<item>.json). When the app’s
i18n/routing.ts lists more locales, sync --check compares each other
locale’s copy of every shipped namespace — messages/<locale>/<item>.json —
with the app’s English file on disk (the merged copy, your own keys
included) and reports a missing file or missing keys as locale-behind,
with the keys. sync never writes another locale: after it adds English
keys, translate them, and the check passes again.
create installs the pages you pick the same way: the dependencies first
(from the root of a parent pnpm workspace when the new app is one of its
members, so the workspace keeps one lockfile), then intelligo sync of the
design-system base and every item, one shadcn add each. Scaffold files an
item replaces — app/globals.css, the theme provider, lib/utils.ts — move
from the app-scaffold record to the registry’s (the feature’s handedOver
list), so upgrade --check does not report them as yours forever.
The chosen items are recorded in intelligo.manifest.json before anything is
installed, and sync treats a file the scaffold wrote and nobody edited as the
registry’s to replace, so when the install is declined or fails, a bare
intelligo sync picks up where create stopped. The project name is the
directory’s unless --name says otherwise; its slug is the package name, the
billing product and the default page title. The scaffold carries its own
.gitignore. Under pnpm, an app outside any workspace also gets a
pnpm-workspace.yaml that declines the dependency build scripts pnpm 11 would
stop the install over and allows the shadcn CLI’s pnpm add at the root (with
an .npmrc saying the same to pnpm 9). An app created inside a pnpm workspace
gets neither; its next.config.mjs reads the workspace root’s .env.local and
.env instead, as doctor and migrate do. When another package in that workspace already
has the app’s name (a root named after the product, say), the app’s package is
@<scope>/<directory> instead, the scope being the root’s scope or name.
Where doctor, migrate, upgrade and admin read env from
They run outside Next, so they load the env files themselves, never
overriding a variable that is already set. Highest precedence first: the
shell, the app’s .env.local, the app’s .env, then — when the app is a
member of a pnpm workspace — the workspace root’s .env.local and .env. An
app in a monorepo whose next.config falls back to the repository root’s
.env is therefore checked and migrated against the same DATABASE_URL it
boots with.
admin grant: the first platform admin
PLATFORM_ADMIN_EMAILS promotes an address only once it is verified, and a
development setup without an email provider verifies none (the console
provider logs the verification link instead, outside production).
intelligo admin grant <email> writes users.role for a user who has signed
up and records an admin.platform_admin.granted audit event, in one
transaction. With NODE_ENV=production it refuses unless --force is passed.
Why migrate is not drizzle-kit migrate
Drizzle applies migrations in journal-timestamp order and skips anything
numbered out of sequence, so a migration published behind the last applied
timestamp would never run and nothing would say so. intelligo migrate selects pending work by content hash, applies it in one transaction,
and records it in the same table drizzle uses so both tools agree afterwards.
It also refuses a database that has the schema but no records — the state
drizzle-kit push leaves behind. Applying the chain to tables that already
exist fails part-way; --check reports the database as unmanaged instead, to be
baselined first.
migrate --check exits 1 whenever anything is pending or the database is ahead,
which an empty database and a stale one share. A deploy gate that must tell them
apart reads --json: one object on stdout whose state is up_to_date,
pending, fresh (empty database), unmanaged, ahead or legacy, beside
exitCode, chain, applied, pending, unknown, legacy, legacyMissing
and adoptable.
add maintenance and its schedule
The route reconciles executions that are ten minutes stale, so it is meant to
run every five minutes. add maintenance writes that schedule to a new
vercel.json; an existing one is never touched — the entry to add is printed
instead. Vercel Hobby runs a cron at most once a day and refuses a deployment
that asks for more: there, or on any other host, call the route from your own
scheduler with Authorization: Bearer $CRON_SECRET.
Entry points
@intelligo-dev/cli