Command Line Interface for building metadata-driven applications with the ObjectStack Protocol.
Built on oclif — commands are auto-discovered, and plugins can extend the CLI without modifying the main package.
pnpm add -D @objectstack/cliThe CLI is available as objectstack or the shorter alias os.
# Initialize a new project
os init my-app
# Generate metadata
os generate object task
os generate view task
os generate flow task_changed --object task
# Validate configuration
os validate
# Start development server
os dev
# Compile for production
os compile| Command | Description |
|---|---|
os init [name] |
Initialize a new ObjectStack project — in a new directory of that name when name is given, otherwise in the current directory |
os dev [package] |
Start development mode — watch sources, rebuild the artifact, and restart the server on change |
os serve [config] |
Start the ObjectStack server with plugin auto-detection |
| Command | Description |
|---|---|
os compile [config] |
Compile configuration to a JSON artifact (dist/objectstack.json) |
os validate [config] |
Validate configuration against the ObjectStack Protocol schema |
os info [config] |
Display metadata summary (objects, fields, apps, agents, etc.) |
| Command | Description |
|---|---|
os generate <type> <name> |
Generate metadata files (alias: os g) |
os create <type> [name] |
Scaffold a standalone kernel code plugin project (the Plugin contract, built by tsc) from a built-in template |
Available generate types: object, view, action, flow, dashboard, app, skill, picklist
picklist writes a shared option list (src/picklists/<name>.picklist.ts, with
definePicklist). A select field names it with Field.select({ picklist: '<name>' })
in place of its own options, and the server serves that field with the list's options.
agent is retired (ADR-0063 §2): agents are platform-internal, so a scaffolded
src/agents/*.ts validated, published and was then filtered out of the runtime
catalog without a word. os g agent now says so and points at skills — the
third-party extension primitive — which are authored as src/skills/<name>.skill.ts
with defineSkill, and which os g skill <name> scaffolds for you.
Every type is written as NAME.TYPE.ts — customer.object.ts, customer.view.ts,
lead_qualification.skill.ts — and the infix comes from the metadata type registry,
which declares each type's file convention (*.object.ts, *.skill.ts, …). The
metadata loader discovers files by globbing exactly those patterns, so a scaffold
matching none of them type-checks, validates and publishes with nothing reporting
that it was skipped. Files generated before this convention landed are not renamed
and keep loading through their barrel index.ts.
Push a locally-built package to ObjectStack Cloud and (optionally) install it into one of your environments in a single command. The commands below do not share one session or one flag spelling — see Credentials and server URL.
| Command | Description |
|---|---|
os cloud login |
Authenticate to ObjectStack Cloud (browser or --email/--password); writes ~/.objectstack/cloud.json |
os cloud whoami / os cloud logout |
Inspect / clear the stored cloud session |
os environments create --org <id> --name <n> |
Provision a new environment (was os projects create before the v5.0 project → environment rename; ADR-0006, no aliases) |
os environments list / show <id> |
List / inspect your environments |
os package publish [artifact] |
Publish dist/objectstack.json as a versioned package in your org |
Typical flow (build → publish → install into an environment, seeding sample data):
os compile # → dist/objectstack.json
os cloud login # one-time; the session os package publish and os environments read
os environments create --org "$ORG" --name "Dev" --activate
os package publish --env <env-id> --install --seed-sample-dataos environments runs on either stored session. With no --url it talks to
the server of the os login session (credentials.json) when there is one,
else to the server of the os cloud login session (cloud.json), with that
session's token. A --url (or OS_CLOUD_URL) picks the session that names
that server, credentials.json's first — so with both stored, a --url naming
the cloud uses the cloud session. A --url neither file names gets
credentials.json's token as before, never cloud.json's. With no session and
no --token / OS_TOKEN, it exits 1 with Authentication required.
os package publish registers a sys_package (keyed by a reverse-domain
--manifest-id, derived from the artifact when omitted), snapshots the
artifact as a new --version, and — with --env <id> --install — installs
that version into the environment. Useful flags: --visibility private|org| marketplace, --note, and for marketplace listings --submit (request
review) or --auto-approve (platform admins only). Set OS_CLOUD_URL to
target a non-default control plane, e.g. a staging cloud. os cloud login,
os package publish and os environments read it; the flag is --server on
os package publish and --url on the other two.
Two stored sessions exist, and each command authenticates with one of them:
| Command | Server URL | Token | Stored session |
|---|---|---|---|
os cloud login |
-u, --url (env OS_CLOUD_URL, default https://cloud.objectos.ai) |
none — -e, --email / -p, --password, or the browser device flow |
writes ~/.objectstack/cloud.json |
os cloud whoami / os cloud logout |
— | — | reads / deletes ~/.objectstack/cloud.json |
os package publish, os plugin publish |
-s, --server (env OS_CLOUD_URL); else the URL in cloud.json; else https://cloud.objectos.ai |
-t, --token (env OS_CLOUD_API_KEY, then OS_TOKEN) |
~/.objectstack/cloud.json — the os cloud login session |
os environments list / show / create / bind / switch |
-u, --url (env OS_CLOUD_URL); else the URL of the stored session it uses; else http://localhost:3000 |
-t, --token (env OS_TOKEN) |
No --url: ~/.objectstack/credentials.json (the os login session), else ~/.objectstack/cloud.json (the os cloud login session). With --url: the file that names that server, credentials.json first; when neither does, credentials.json as before. cloud.json's token is never sent to another URL |
os package install is not a cloud command: it installs into a running runtime
(-r, --runtime, env OS_RUNTIME_URL, default http://localhost:3000) and signs
in there with --email / --password (env OS_RUNTIME_EMAIL /
OS_RUNTIME_PASSWORD).
Runtime plugins (declared in objectstack.config.ts plugins) are loaded automatically by os serve / os dev. Runtime plugins are bundled into the build artifact. To distribute a build, publish it as a package with os package publish (see Cloud — publish & install); the os environments bind <id> --artifact dist/objectstack.json path still binds an artifact directly into an environment without going through the package registry.
A code-bearing plugin — a directory carrying an objectstack.plugin.json manifest — is packaged and shipped through the os plugin command group (ADR-0025 §3.4, build → sign → publish):
| Command | Description |
|---|---|
os plugin build [dir] |
Compile a plugin into a signed-ready .osplugin artifact (--entry, --out, --minify) |
os plugin sign <artifact> --key <pem> |
Sign a built .osplugin with a publisher Ed25519 key, writing a detached <artifact>.sig |
os plugin publish [artifact] |
Publish a signed .osplugin to ObjectStack Cloud |
The group has no install: ADR-0025 records the code-plugin install half (download, verify, materialize, load) as not yet implemented. os plugin (singular) is unrelated to os plugins (plural), oclif's plugin manager, which this package does not ship — see os plugins and os help.
| Command | Description |
|---|---|
os test [files] |
Run Quality Protocol test scenarios against a running server |
os doctor |
Check development environment health |
os lint [config] |
Check configuration for style and convention issues |
os diff [before] [after] |
Compare two configurations and detect breaking changes |
| Command | Description |
|---|---|
os explain [schema] |
Display human-readable explanation of an ObjectStack schema |
The CLI looks for objectstack.config.ts (or .js, .mjs) in the current directory:
import { defineStack } from '@objectstack/spec';
import { project, task } from './src/objects';
export default defineStack({
manifest: {
id: 'com.example.my-app',
namespace: 'my_app',
version: '1.0.0',
type: 'app',
name: 'My App',
},
objects: [project, task],
});dev, start, and serve all load .env files following the
Vite / Next.js convention, in this order (later files override earlier):
.env.env.${NODE_ENV}—developmentforos dev,productionforos start(override withNODE_ENV=...).env.local.env.${NODE_ENV}.local
Any variable already in the process environment wins. Typical project layout:
.env # checked in — safe defaults
.env.development # checked in — dev URLs, demo creds
.env.production # checked in — production URLs
.env.local # gitignored — secrets, personal overrides
Common variables: OS_DATABASE_URL, OS_DATABASE_DRIVER,
OS_HOME, AUTH_SECRET, PORT.
--version— Show version number--help— Show help (os --help, oros <command> --helpfor one command)
There are no short forms: os -h and os -v exit 2 with command -h not found /
command -v not found. -v is a command's own flag instead — --verbose on os dev,
os serve, os start and os doctor, --version <semver> on os package publish and
os package install.
-t, --template <template>— Template:app(default),plugin(a metadata package — declarative objects another stack loads, not the kernel code pluginos create pluginemits),empty--no-install— Skip dependency installation
-o, --output <path>— Output path (default:dist/objectstack.json)--json— Output compile result as JSON (for CI pipelines)
--strict— Treat warnings as errors--json— Output result as JSON
-p, --port <port>— Server port. Resolution:--port›$OS_PORT›$PORT›3000. With--deva busy port auto-hops to the next free one; in production mode it's a hard error (never silently drifts).--dev— Run in development mode (load devPlugins, pretty logging)--ui— Enable the bundled Console portal at/_console/when@object-ui/consoleis installed (default: true)--no-server— Skip starting HTTP server plugin
-d, --dir <directory>— Override target directory
This package ships no oclif plugin manager: its package.json declares no oclif.plugins and does not depend on @oclif/plugin-plugins or @oclif/plugin-help. So os plugins (install, uninstall, update, link, and the rest) and os help are not registered commands; each exits 2 with command … not found. Use os --help or os <command> --help for help. To add third-party CLI commands, see oclif Plugin System.
--json— Output as JSON
-v, --verbose— Show fix suggestions for warnings--scan-deprecations— Scan for deprecated patterns
The CLI is built on oclif, and oclif's plugin system is its only command-extension mechanism: a third-party package (e.g., cloud commands, marketplace tools) ships oclif Command classes and is loaded as an oclif plugin, without modifying this package. The os binary this package publishes declares no plugins and ships no plugin manager, so there is no os plugins install.
- Create an oclif plugin package with its own
oclifconfig inpackage.json - Export oclif Command classes from the plugin's
src/commands/directory - Load the plugin through an
osdistribution you build: a package whose ownpackage.jsonlists the plugin in bothoclif.pluginsanddependencies. oclif's core-plugin loader matchesoclif.pluginsnames only againstdependencies; a name listed underdevDependenciesalone never loads.
1. Configure the plugin's package.json:
{
"name": "@acme/plugin-marketplace",
"oclif": {
"commands": {
"strategy": "pattern",
"target": "./dist/commands",
"glob": "**/*.js"
}
}
}2. Create oclif Command classes:
// src/commands/marketplace/search.ts
import { Args, Command, Flags } from '@oclif/core';
export default class MarketplaceSearch extends Command {
static override description = 'Search marketplace applications';
static override args = {
query: Args.string({ description: 'Search query', required: true }),
};
async run() {
const { args } = await this.parse(MarketplaceSearch);
// Implementation...
}
}3. Load it through your os distribution, then use it:
List @acme/plugin-marketplace in both oclif.plugins and dependencies of the distribution's package.json (see How Plugin Extension Works). Its commands then appear in that distribution's os --help:
os marketplace search "crm"| Before (Commander.js) | After (oclif) |
|---|---|
Plugins declared in objectstack.config.ts |
Plugins listed in an os distribution's oclif.plugins and dependencies |
Custom loadPluginCommands mechanism |
oclif's built-in plugin discovery |
contributes.commands in manifest |
oclif.commands in package.json |
Commander.js new Command(...) exports |
oclif class extends Command exports |
| Project config determines CLI commands | CLI commands available without project init |
os init # 1. Create project
os generate object customer # 2. Add a Customer object
os generate object order # 3. Add an Order object
os generate view customer # 4. Add a list view
os validate # 5. Validate everything
os dev # 6. Start dev server
os compile # 7. Build for production
os environments bind <id> --artifact dist/objectstack.json # 8. Bind to a Cloud environment
@objectstack/cli (oclif)
├── bin/run.js # Entry point (os / objectstack)
├── src/commands/ # Auto-discovered command classes
│ ├── init.ts # os init
│ ├── dev.ts # os dev
│ ├── serve.ts # os serve
│ ├── compile.ts # os compile
│ ├── validate.ts # os validate
│ ├── generate.ts # os generate (alias: g)
│ ├── environments/ # os environments <subcommand>
│ │ ├── list.ts
│ │ ├── show.ts
│ │ ├── create.ts
│ │ ├── switch.ts
│ │ └── bind.ts
│ └── ...
├── src/utils/ # Shared utilities
└── package.json # oclif config under "oclif" key
@objectstack/cli is a command-line tool first, and its exports map is
deliberately sealed: a deep dist/ path is not a supported import and an
internal refactor may move it without notice. What an out-of-repo consumer may
resolve is exactly this map — a subpath is added here on purpose, with a
minor changeset, never discovered by reaching into dist/:
| Subpath | What it is for |
|---|---|
@objectstack/cli |
The command classes bin/run.js loads — the oclif entry. |
@objectstack/cli/console |
Exactly three Console SPA mounting helpers — resolveConsolePath, hasConsoleDist, createConsoleStaticPlugin — consumed by cloud's objectos-runtime node server to mount the Console. The drift guards and the rest of utils/console.ts are not on this subpath (#16046). |
@objectstack/cli/hook-body |
The hook-body extractor os build and os lint apply, for an app harness that must run the same body-only lowering the build ships (below). |
@objectstack/cli/package.json |
The manifest itself, for the ordinary tooling idiom of reading a dependency's own version. |
import { extractHookBody, HookBodyExtractionError } from '@objectstack/cli/hook-body';
import type { ExtractedBody, HookBodyRefusalKind } from '@objectstack/cli/hook-body';
// The metadata-only source `os build` ships for this handler — hand it to the
// runtime's QuickJS runner in a test and you execute what production executes.
const body: ExtractedBody = extractHookBody(handler, 'hooks.account.beforeInsert');
body.source; // the lowered function body
body.capabilities; // the capability tokens inferred from it
// A handler that is no longer shippable body-only is refused with the SAME
// classification `os lint` reports, so a test can assert the kind, not prose.
try {
extractHookBody(leakyHandler, 'hooks.account.afterUpdate');
} catch (e) {
if (e instanceof HookBodyExtractionError) {
const kind: HookBodyRefusalKind = e.kind; // 'unparseable' | 'forbidden-token' | 'free-identifiers'
e.freeIdentifiers; // the module-scope names the handler reached for
e.nodeOnlyIdentifiers; // the subset only the Node host provides
}
}An app that wants to assert "my hooks are still metadata-only" needs the
platform's own extractor: a local reimplementation passes its own tests while
diverging from the rule the build actually applies. os lint's
hook-body/not-lowerable rule answers the pass/fail question; this entry hands
a test the lowered source to run. The four names above are the whole surface
— the entry re-exports them and nothing else.
objectstack serve (and os dev) auto-mount a baseline slate of
service plugins for every preset except --preset minimal:
| Capability | Plugin | Why default |
|---|---|---|
queue |
QueueServicePlugin |
Background retries (mail, audit batching) need a queue. |
job |
JobServicePlugin |
Scheduled jobs (cleanup, reports, cron). |
cache |
CacheServicePlugin |
In-memory adapter; performance baseline. |
settings |
SettingsServicePlugin |
Mail / storage / branding / feature-flag UI + persistence. |
email |
EmailServicePlugin |
Auth callbacks (verify, reset, magic-link) hard-depend. |
storage |
StorageServicePlugin |
Avatars, attachments, exports. Local fallback in dev. |
Apps no longer need to list these in requires: [...].
Opt-out: run objectstack serve --preset minimal to skip the slate
and only load core + your explicit requires.
Built-in Settings manifests: mail, storage, branding,
feature_flags are pre-registered. Both mail and storage expose a
"Test connection" action that exercises the live transport / adapter
end-to-end. Switching the storage adapter does not migrate
previously uploaded files — the plugin logs a warning on every swap.
Storage warning: when no storage: block is configured the plugin
falls back to the local driver under .objectstack/data/uploads/. In
non-dev mode a single console.warn fires at boot — switch to S3/GCS/
Azure for production by setting config.storage, OS_STORAGE_* env,
or via the Settings hub (namespace=storage).
Apache-2.0. See LICENSING.md.