sdxc

[ Getting started ]

The sdxc command

Start a project from a template, generate files where the layout puts them, copy UI components to edit, upgrade with a prompt for your agent, and run your app's own commands.

Last updated 2026-10-15

The sdxc command writes a new project, adds controllers, jobs, models and the rest where the app layout puts them, copies @sdxc/ui components into your project when you want to edit one, and moves the project to a newer release with a prompt that tells an AI agent what changed for the code you have.

It ships in @sdxc/kit. Install it globally so you can type the commands as written here; your app imports each package it uses by its own name, such as @sdxc/result:

npm add -g @sdxc/kit

Every command runs from the project root, the nearest folder with a package.json, and sdxc <command> --help lists its arguments and flags. On a terminal a command asks for what you left out, and every question has a flag, so a script or an agent passes the answer up front and never waits on one.

Start a project

sdxc create my-app

create writes the built-in app template into my-app: a Remix v3 Worker on Cloudflare in the folder layout the generators below expect, with cloudflare.config.ts, a Vite+ configuration, a test runner, its own command line in bootstrap/cli.ts, each @sdxc package it uses pinned to its latest release, and remix pinned to the version those releases need. The target folder must be missing or empty apart from a .git folder, so git init before create works too; without a directory, it writes into the current one.

Any GitHub repository is a template as well, with no manifest to declare. Name it as org/repo, with a folder inside it and a branch, tag or commit when you need them:

sdxc create blog --template acme/templates/apps/blog#main --no-git

GITHUB_TOKEN or GH_TOKEN in the environment authenticates the download, which a private repository needs. create writes files and runs nothing from them: it installs no dependencies and runs no scripts, and prints the commands to run next with the project's package manager. When it made the folder, it asks whether to initialize git there; --git and --no-git answer in advance, and without a terminal the answer is no.

Configure the command line

Every setting has a default, so a project needs no configuration file until it changes one. sdxc init writes .config/sdxc.jsonc with every key spelled out, and a $schema your editor completes them from:

.config/sdxc.jsonc
{
	// Where `sdxc ui add` writes components
	"ui": { "directory": "resources/components/ui", "tests": false },
	"make": {
		// The route contract in routes/ that `sdxc make controller` adds to
		"routes": "web",
		"migrations": { "default": "database/migrations" },
		"tests": true,
	},
	// Run once on every file a command wrote or edited, with the paths appended
	"format": ["bunx", "vp", "fmt"],
}

The file is JSON with comments, so each setting can say why it is set. format is the one most projects set: a generated file and a copied component then come out in your project's style. sdxc.jsonc at the project root is read too, when you prefer it there.

Generate files

sdxc make <kind> <name> writes one file of that kind where the layout puts it, with a test beside it:

KindFile
controllerapp/http/controllers/<name>.tsx
middlewareapp/http/middleware/<name>.ts
validatorapp/http/validators/<name>.ts
jobapp/jobs/<name>.ts
modelapp/models/<name>.ts
migrationdatabase/migrations/<number>_<Name>.sql
factorydatabase/factories/<name>.ts
serviceapp/services/<name>.ts
policyapp/policies/<name>.ts
initializerapp/initializers/<name>.ts
emailapp/emails/<name>.tsx
mcpapp/mcp/tools/<name>.ts
commandapp/commands/<name>.ts

A slash in the name is a folder inside the kind's folder, so features stay together: sdxc make job trials/expire writes app/jobs/trials/expire.ts.

A controller, a job, an MCP tool or resource and a command are also declared and mapped. The command adds the route, job, tool or command to its declaration module and maps the new handler in the initializer that serves it, through lazy(), so the handler answers as soon as the command finishes:

sdxc make controller sponsors
sdxc make controller ping --method post --path /api/ping
sdxc make controller signup --method form
sdxc make job reports/daily --cron "0 8 * * *"
sdxc make mcp search-posts
sdxc make mcp article --resource
sdxc make command users/grant

--method takes get, post, put, patch, delete or form, a page with an action beside it, and --routes picks a route contract other than make.routes. --cron declares a scheduled job instead of one with input. When either edit cannot be placed, such as a project whose initializer is laid out differently, make writes the handler, leaves the other files as they are, and prints both edits for you to make, so a declaration never lands without its mapping.

A migration is named like the files already in its chain and sorts after all of them. A chain of 0004-add-feeds.sql files gets 0005-add-index.sql, a chain of 20261008100000_add_feeds.sql files gets the current UTC time as 20261015090807_add_index.sql, and an empty chain starts at 0001_AddIndex.sql. --chain picks a chain from make.migrations for an app with more than one database. --no-test skips the test for one run, make.tests: false skips it everywhere, and --dry-run prints what would be written.

When the generated code imports an @sdxc package your package.json does not list yet, make adds it at the same release as the packages you have, the newest version published on or before that date, and prints the install command to run:

Added @sdxc/jobs@2026.10.12 to package.json; run bun install

Make the generated files your own

Each kind is written from a stub, and a stub in make.stubs replaces the built-in one of the same name. Copy the built-in stubs to start from:

sdxc make stubs controller job

A stub is the file's text with {{name}}-style placeholders. Edit only the ones you want to change; every other kind keeps the built-in stub.

Copy a UI component to edit it

@sdxc/ui components are imported from the package, so a release updates them for you. When a design needs a component changed, copy it into the project:

sdxc ui add dialog

The copy lands in ui.directory, and every file in the project that imported @sdxc/ui/dialog, or Dialog from @sdxc/ui, now imports the copy. A component that reaches others keeps importing them from the package; --with-deps copies those as well.

The command records each copy, the release it came from and a hash of each file in .config/sdxc.lock.jsonl. Commit that file and leave it to the command line, which reads it to tell an edited copy from an untouched one:

sdxc ui list            # every component, marking copies that are edited or behind
sdxc ui diff dialog     # how the copy differs from the installed version
sdxc ui update dialog   # take the installed version where the copy is untouched
sdxc ui remove dialog   # delete the copy and point imports back at @sdxc/ui/dialog

update replaces only the files you have not edited and names the rest; --force replaces those too.

Upgrade

sdxc upgrade

upgrade moves every @sdxc package in package.json to the latest release, or to the release you name, each to its newest version published on or before that date. It asks first when the release has breaking changes in packages your code imports, moves remix along when that release needs a different one, and installs. Then it prints a prompt for an AI agent: the changes between the two releases to the packages your code imports, breaking ones first with their migration steps, the components you copied whose source changed since, and your project's own check, typecheck and test scripts to run once the edits are done.

The prompt goes to stdout, so it pipes into your agent's command line or clipboard, and --output upgrade.md writes it to a file instead. --no-install changes package.json only and prints the commands to run next. To write the prompt again later, name the release you moved from:

sdxc upgrade --prompt --from 2026.9.4

The prompt comes from the new release's own changelog: upgrade runs that release of @sdxc/kit to write it, so the notes match what you installed. Install and pin covers why a release is one exact date.

Run your app's commands

An app created from the template has a command line of its own, in bootstrap/cli.ts, for work that belongs to the app but to no request: seeding, backfills, granting a role. sdxc run runs it from any folder in the project, with Bun for a project on Bun and Node otherwise:

sdxc run about
sdxc run users grant ada@example.com --role admin
sdxc run --help

Every word after run reaches your app's command line as typed, --help included, and its exit code is sdxc run's. Your app's command line writes those commands.

Where to go next

Written by Sergio Xalambrí. Follow @sergiodxa for new packages, or sponsor the work.