Skip to content

Latest commit

 

History

History
1049 lines (816 loc) · 45.1 KB

File metadata and controls

1049 lines (816 loc) · 45.1 KB
title Command Line Interface
description Complete guide for using the ObjectStack CLI to build metadata-driven applications

@objectstack/cli

Command Line Interface for building metadata-driven applications with the ObjectStack Protocol.

Installation

pnpm add -D @objectstack/cli

The CLI is available as objectstack or the shorter alias os. Installed as a dev dependency, the bins are project-local — invoke them as npx os … / pnpm exec os … or via your package scripts.

Your First App in 2 Minutes

Create a project

npm create objectstack@latest my-app
cd my-app

This scaffolds a working project with objectstack.config.ts, a sample object, and all dependencies installed — plus the AI skills bundle and an AGENTS.md for coding agents. (os init is the CLI's own scaffolder for plugin skeletons and bare configs — see below.)

Add more metadata

os generate object customer    # Add a Customer object
os generate action approve     # Add an action
os generate flow onboarding    # Add an automation flow

Launch the dev server

os dev --ui

Open http://localhost:3000/_console/ — you'll see the Console UI with a data browser, metadata explorer, and API documentation. Sign in with the seeded dev admin (admin@objectos.ai / admin123) — os dev provisions it automatically on an empty database. The boot banner also prints the app's MCP endpoint (/api/v1/mcp) so a coding agent can connect to the running app.

Validate & Build

os validate          # Check schema + CEL predicates + widget bindings (no artifact)
os compile           # Build production artifact → dist/objectstack.json
**These commands *are* the AI build loop.** In day-to-day work, Claude Code writes the metadata and runs two of them for you: `os validate` is **the gate** (it rejects predicate/schema/binding mistakes that fail silently at runtime), and `os dev --ui` is **the human verify surface** (the Console, where you confirm the app matches intent). See [Build with Claude Code](/docs/getting-started/build-with-claude-code) for the full loop. `os dev --ui` starts a dev server with the bundled Console UI, auto-loads ObjectQL, an in-memory driver when appropriate, and the Hono HTTP server.

Commands

Development

Command Alias Description
os init [name] Initialize a new ObjectStack project in the current directory
os dev [package] Start development mode with hot reload
os serve [config] Start the ObjectStack server with plugin auto-detection
os db clean Reclaim SQLite free space with a one-time VACUUM (ADR-0057)

os init

Scaffolds a new ObjectStack project with configuration, TypeScript setup, and initial metadata files.

Which scaffolder? For a new app, prefer npm create objectstack@latest — it also derives your namespace, pins the framework packages to the current release, and installs the AI skills bundle + AGENTS.md. Reach for os init when you want a plugin skeleton or a bare config in an existing directory.

os init my-app                    # Create with default "app" template
os init my-plugin -t plugin       # Create a plugin project
os init blank -t empty            # Minimal config only
os init my-app --no-install       # Skip dependency installation

Options:

  • -t, --template <template> — Template: app (default), plugin, empty
  • --no-install — Skip automatic dependency installation
  • -p, --package-manager <npm|pnpm|yarn|bun> — Package manager to use (auto-detected from the environment)

Templates:

Template What it creates
app Full application with objects, views, barrel imports
plugin Reusable plugin package with objects
empty Minimal project with just objectstack.config.ts

os dev

Starts development mode. Three usage shapes:

  1. With local source (objectstack.config.ts in cwd) — auto-compiles to dist/objectstack.json if missing, then delegates to os serve --dev.
  2. With a pre-built artifact (--artifact <path|url>) — skips auto-compile and boots the artifact directly. No objectstack.config.ts needed in cwd. Useful for trying out a published app in dev mode without cloning its source.
  3. Monorepo root (cwd has pnpm-workspace.yaml) — orchestrates pnpm -r dev across packages.
os dev                     # Auto-compile cwd config, then start
os dev my-package          # Workspace package (monorepo orchestration mode)
os dev --ui -v             # Dev server with Console UI + verbose

# Boot a remote artifact in dev mode (no local config needed)
os dev --artifact https://raw.githubusercontent.com/<org>/<repo>/main/dist/objectstack.json

# Override storage / auth on the fly
os dev --database file:./data/test.db --auth-secret $(openssl rand -hex 32)

Options (the runtime overrides mirror os start — each flag overrides the matching env var):

Flag Env equivalent Purpose
-a, --artifact <path|url> OS_ARTIFACT_PATH Boot a pre-built artifact directly; skips auto-compile
-d, --database <url> OS_DATABASE_URL file:… / libsql:// / postgres:// / mongodb:// / memory://
--database-driver <kind> OS_DATABASE_DRIVER Force sqlite | turso | postgres | mongodb | memory
--database-auth-token <t> OS_DATABASE_AUTH_TOKEN libsql/Turso token
--auth-secret <s> OS_AUTH_SECRET Override the dev-fallback secret
--environment-id <id> OS_ENVIRONMENT_ID Environment identifier (default env_local)
-p, --port <n> OS_PORT / PORT Listen port (default 3000). In dev a busy port auto-hops to the next free one; the banner shows the actual port.
--ui Force Console UI on (already on by default in dev)
--compile Force compiling objectstack.config.tsdist/objectstack.json before starting (auto when the artifact is missing; ignored with --artifact)
--fresh Ephemeral OS_HOME in the OS tempdir (clean DB, uploads, storage), auto-deleted on exit; implies --seed-admin
--seed-admin / --no-seed-admin Seed a dev admin (admin@objectos.ai / admin123) on an empty DB — default on; override with --admin-email / --admin-password
-v, --verbose Verbose output

By default os dev keeps your data between restarts in a project-local SQLite file at .objectstack/data/dev.db (created on first run). Pass --database, set OS_DATABASE_URL, or use --fresh for a throwaway run.

With a file-backed SQLite database, dev also provisions a sibling <db>.telemetry.<ext> file registered as the telemetry datasource — lifecycle-classed system data (activity streams, job runs, notifications, audit) lands there instead of the business DB (ADR-0057). Opt out with OS_TELEMETRY_DB=0, or point it elsewhere (any mode, including serve) with OS_TELEMETRY_DB=<path>.

os serve

Starts the ObjectStack server with automatic plugin discovery:

  • Auto-loads ObjectQL Engine when objects are defined
  • Auto-loads InMemory Driver in dev mode
  • Auto-loads App Plugin for metadata
  • Auto-loads Hono HTTP Server for REST APIs
  • Auto-loads the auth tier plugins (@objectstack/plugin-auth, @objectstack/plugin-security, @objectstack/plugin-audit) when the preset includes the auth tier and the user did not pin them in objectstack.config.ts
os serve                   # Default: port 3000
os serve -p 4000           # Custom port
os serve --dev             # Development mode (pretty logs, devPlugins)
os serve --dev --ui        # Dev mode with Console UI
os serve --no-server       # Skip HTTP server (kernel only)
os serve --preset minimal  # Skip auto-loaded auth/i18n/ui plugins

Options:

  • -p, --port <port> — Server port (default: env OS_PORT or 3000)
  • --dev — Development mode (loads devPlugins, pretty logging)
  • --ui / --no-ui — Toggle Console UI at /_console/ (default on)
  • --server / --no-server — Toggle HTTP server plugin
  • --prebuilt — Skip esbuild / bundle-require and load the config as native ESM (use this in production builds where the config is already pre-compiled)
  • --preset minimal | default | full — Override the auto-registration tier (see below)

Tier presets

os serve decides which optional plugins to auto-register from a tier list. Any plugin already present in config.plugins always wins; tiers only gate the automatic registration of optional plugins.

Preset Tiers Auto-loaded optional plugins
minimal core none
default (default) core, i18n, ui, ai, auth i18n service, Console UI, AI service, Auth + Security + Audit
full core, i18n, ui, ai, auth currently an alias of default — same tiers, no additional plugins

The auth tier requires OS_AUTH_SECRET to be set; otherwise AuthPlugin is skipped with a yellow warning and the /api/v1/auth/* endpoints will return 404. (In --dev mode the CLI falls back to an insecure local secret so login works out of the box.) To take full control, set tiers on the stack config:

import { defineStack } from '@objectstack/spec';

export default defineStack({
  manifest: { /* ... */ },
  tiers: ['core'],            // disable all optional auto-registration
  plugins: [
    // ... only what you explicitly want
  ],
});

os db clean

Reclaims SQLite free space with a one-time VACUUM (ADR-0057 §3.4). The platform reclaims space incrementally (auto_vacuum=INCREMENTAL), but that setting only takes effect on a fresh database — files created before it stay pinned at their high-water mark until one full VACUUM rebuilds them. Non-destructive: every row survives; free pages return to the OS. Cleans the telemetry sibling too when one exists.

os db clean                                      # default: the per-project dev DB
os db clean --database file:./data/app.db        # explicit target

Options:

  • -d, --database <url> — SQLite database URL/path (defaults to $OS_DATABASE_URL, then the per-project dev DB)

Console UI

Launch the development server with the Console UI:

os dev --ui                # Default: port 3000
os serve --dev --ui -p 4000

The Console UI is a metadata-driven admin interface that provides object exploration, package management, and runtime metadata diagnostics.

Architecture:

┌─────────────────────────────────────────┐
│          os dev --ui (:3000)            │
├─────────────────────────────────────────┤
│  Hono Server                            │
│  ├─ /api/v1/*     → ObjectStack API     │
│  ├─ /_console/*   → Console SPA         │
│  └─ /*            → custom routes       │
└─────────────────────────────────────────┘

The prebuilt Console bundle ships with the framework packages and is served at /_console/ — no separate frontend install or build step is needed.

Production

os start

Boots a production server directly from a compiled objectstack.json artifact — no objectstack.config.ts required. This is the canonical "deploy a built ObjectStack app" command: hand a server one JSON file (or a URL pointing at one) and it runs. When the cwd does contain an objectstack.config.ts and no artifact exists yet, os start auto-compiles it first; with no config and no artifact at all it boots an empty kernel with the Console + marketplace, so you can install apps interactively.

# Quick start — load ./dist/objectstack.json with sqlite at file:<home>/data/objectstack.db
os start

# Pick everything via flags (no env vars needed)
os start \
  --artifact ./build/myapp.json \
  --database file:./data/prod.db \
  --auth-secret $(openssl rand -hex 32) \
  --port 8080

# Remote artifact + Turso/libSQL backing store
os start \
  --artifact https://cdn.example.com/app.json \
  --database libsql://my-db.turso.io \
  --database-auth-token $TURSO_TOKEN

# Postgres
os start --database "postgres://user:pass@host:5432/mydb"

# Pure env-var style still works (Docker / Fly / k8s friendly)
OS_ARTIFACT_PATH=./build/myapp.json \
OS_DATABASE_URL=file:./data/prod.db \
OS_AUTH_SECRET=… \
os start

Options (all override the matching env var):

Flag Env equivalent Purpose
-a, --artifact <path|url> OS_ARTIFACT_PATH File path or http(s):// URL to the compiled artifact
-d, --database <url> OS_DATABASE_URL file:… / libsql:// / postgres:// / mongodb:// / memory://
--database-driver <kind> OS_DATABASE_DRIVER Force sqlite | turso | postgres | mongodb | memory when the URL is ambiguous
--database-auth-token <token> OS_DATABASE_AUTH_TOKEN Auth token for libsql/Turso
--auth-secret <secret> OS_AUTH_SECRET / AUTH_SECRET Secret for @objectstack/plugin-auth. If neither the flag nor the env var is set, os start auto-generates one and persists it at <home>/auth-secret
--home <dir> OS_HOME Home directory for persistent state (default <cwd>/.objectstack when an objectstack.config.ts is present, otherwise ~/.objectstack)
--environment-id <id> OS_ENVIRONMENT_ID Environment identifier (default env_local)
-p, --port <port> OS_PORT / PORT Listen port (default 3000). Production fails loudly if the port is busy — see note below.
--ui / --no-ui Mount the Console portal at /_console/. Enabled by default (so you can install marketplace apps); pass --no-ui to disable it.
-v, --verbose Verbose output

Port conflicts: production never auto-shifts. Unlike os dev (which hops to the next free port for local convenience), os start exits with an error if its resolved port is in use. A silently drifted port would break your reverse-proxy upstream, OS_AUTH_URL callbacks, and OS_TRUSTED_ORIGINS (CORS). Pin the port explicitly (OS_PORT=8080 os start) and keep OS_AUTH_URL / OS_TRUSTED_ORIGINS in sync when you change it.

Resolution priority (artifact): --artifact > OS_ARTIFACT_PATH > <cwd>/dist/objectstack.json > <home>/dist/objectstack.json > auto-compile from objectstack.config.ts (when present) > empty kernel. Resolution priority (database): --database > OS_DATABASE_URL > DATABASE_URL (legacy) > file:<home>/data/objectstack.db.

A **named** artifact (`--artifact` or `OS_ARTIFACT_PATH`) does *not* participate in that fall-through: it is used as given, and a local path that does not exist **fails the boot** — naming the path and which of the two named it — instead of quietly continuing down the list. You asked for a specific artifact, so booting something else (or an empty kernel) would hide the typo behind a running server. The fall-through applies to the **conventional** locations only. Remote (`http(s)://`) sources cannot be checked up front and are validated when fetched.

What it boots:

  • Reads the artifact's manifest, objects, views, flows, …
  • Auto-registers the platform services declared in requires: [...] (e.g. ai, automation, analytics, auth, ui). Declaring a service capability (automation, analytics, ai, audit, …) is a requirement: if its provider package isn't installed, boot fails fast with a clear error instead of silently starting without a capability you asked for. (auth and ui are tier-gated with their own opt-in rules — auth's secret-gated skip is described below.)
  • Auto-detects the driver from the database URL scheme (memory:// → in-memory, libsql:///https:// → Turso, postgres[ql]:///pg:// → pg, mongodb[+srv]:// → MongoDB, otherwise sqlite)
  • Runs standalone boot mode with one active environment.

Authentication: os start always resolves an auth secret — --auth-secret > OS_AUTH_SECRET / AUTH_SECRET env > a secret auto-generated and persisted at <home>/auth-secret on first run — so /api/v1/auth/* (login/register) and the Console's login flow work out of the box, without any manual secret provisioning. Set the env var (or flag) explicitly when you deploy across multiple nodes or want to rotate the secret.

**`os start` vs `os serve`:** `os serve` boots from `objectstack.config.ts` (TypeScript source). `os start` boots from `objectstack.json` (compiled artifact) and falls back to the same default-host path if you happen to run it without a config but with an artifact present. The two commands ultimately go through the same kernel — they just differ in which input shape they accept. See [Source vs Artifact](#source-vs-artifact) below.

Build & Validate

Command Description
os compile [config] Compile configuration to a JSON artifact (dist/objectstack.json)
os build [config] Alias for os compile (scaffolded projects wire it as npm run build)
os validate [config] Validate schema, CEL predicates, and widget bindings — the same gates as os compile/os build, no artifact emitted
os info [config] Display metadata summary (objects, fields, apps, agents, etc.)

os compile

Bundles and validates your objectstack.config.ts against the ObjectStackDefinitionSchema, then outputs a deployable JSON artifact.

os compile                           # Default output: dist/objectstack.json
os compile -o build/stack.json       # Custom output path
os compile --json                    # JSON output for CI pipelines

Options:

  • -o, --output <path> — Output path (default: dist/objectstack.json)
  • --json — Output compile result as JSON (for CI)

Output example:

◆ Compile
────────────────────────────────────────
  → Loading configuration...
  Config: objectstack.config.ts
  Load time: 57ms
  → Normalizing stack definition...
  → Lowering inline handlers...
  → Validating protocol compliance...
  → Validating expressions (ADR-0032)...
  → Checking dashboard widget bindings (ADR-0021)...
  → Checking dashboard action references (ADR-0049)...
  → Checking SDUI styling (ADR-0065)...
  → Checking security posture (ADR-0090 D7)...
  → Collecting package docs (ADR-0046)...
  → Writing artifact...

  ✓ Build complete (74ms)

  Data: 2 Objects  6 Fields
  UI: 1 Views  1 Actions
  Runtime: 3 plugins

  Artifact: dist/objectstack.json (7.6 KB)

The resulting dist/objectstack.json is a portable, self-describing deployment unit — you can hand it to os start (locally or on a server), publish it to a CDN, or fetch it over HTTP from another runtime. See os start and Source vs Artifact for details.

os validate

The fast, artifact-free verification gate. It runs the same structural and semantic checks as os compile/os build but writes no dist/, so it is the command to run after every metadata edit. Use it before reporting a change done.

os validate                  # Validate current directory
os validate --strict         # Warnings as errors
os validate --json           # JSON output for CI
os validate path/to/config   # Validate specific file

Gates run (each exits non-zero with a located, corrective message):

  1. Protocol schema — the stack conforms to ObjectStackDefinitionSchema (@objectstack/spec).
  2. CEL / predicate validation (ADR-0032) — every visible / disabled / requiredWhen / validation rule / flow condition / sharing rule is parsed for CEL syntax and checked that each record.<field> reference exists on the target object. This catches a bare field ref (done instead of record.done) that would otherwise evaluate to null and silently hide an action on every record (#2183/#2185).
  3. Widget-binding integrity (ADR-0021) — every dashboard widget's dataset / dimensions / values resolves to a declared dataset/field, so a dangling binding fails here instead of rendering an empty chart.

Options:

  • --strict — Treat warnings as errors (exit code 1)
  • --json — Output results as JSON

Warnings checked (advisory, non-blocking unless --strict):

  • Missing manifest.id (required for deployment)
  • Missing manifest.namespace (required for multi-app hosting)
  • No objects defined
  • No apps or plugins defined
`os validate` and `os build` share one validator, so a config that passes `os validate` will not fail the build on schema/predicate/binding grounds. In a scaffolded project these are wired as `npm run validate` and `npm run build`; your `AGENTS.md` tells coding agents to run `npm run validate` after editing metadata. See [Validating metadata](/docs/deployment/validating-metadata).

os info

Displays a summary of your metadata without compilation or validation:

os info                # Show metadata summary
os info --json         # JSON output for tooling

Output example:

◆ Info
────────────────────────────────────────

  My App v0.1.0
  my-app
  Minimal ObjectStack environment — a clean slate for building.
    Namespace: my_app
    Type: app

  Data: 2 Objects  6 Fields
  UI: 1 Views  1 Actions
  Runtime: 3 plugins

  Objects:
    my_app_note (2 fields, user) — Note
    my_app_ticket (4 fields, user) — Ticket

  Loaded in 59ms

Schema migrations

The metadata→database sync is additive-only: on boot it creates missing tables, adds new columns and creates missing indexes, but never alters or drops existing ones. So a non-additive change to an object already backed by a database — relaxing required (drop NOT NULL), changing a field's type/length, removing a field, or re-scoping a unique constraint — silently diverges from the live schema, and the physical column wins at write time. os migrate reconciles the database to the metadata (the source of truth).

Command Description
os migrate plan Dry-run: show how the database has drifted from metadata, categorised safe / needs-confirm / destructive (no changes applied)
os migrate apply Reconcile the database to metadata. Applies loosening changes; destructive ones require --allow-destructive
os migrate plan                              # Preview drift (no changes)
os migrate apply                             # Apply safe (loosening) changes, with a confirm prompt
os migrate apply --yes                       # Skip the prompt (CI / scripts)
os migrate apply --allow-destructive --yes   # Also drop orphaned columns, tighten NOT NULL, narrow types
os migrate apply --force                     # Migrate even though another process is using the database
os migrate plan --json                       # Machine-readable output

Nothing is written before you confirm

Both commands boot your app to read its metadata. That boot no longer touches the target database: the additive schema sync (create missing tables, add missing columns) and the artifact's inline seed data are deferred, not performed. So plan really is a dry run, and everything apply is about to do — additive work included — is on screen before the [y/N] prompt:

  New (additive — created when you apply)
    + crm_quote [create_table, 9 column(s)]
    + crm_contact [add_columns: nickname, region]

  In place (existing rows converged when you apply)
    ~ crm_contact [normalize_datetime_storage: signed_at — 1,240 row update(s)]

  Safe (loosening — applied without --allow-destructive)
    ✓ crm_contact.email [relax_not_null]

Answering n leaves the database exactly as it was.

The two upper sections differ in a way worth reading carefully. New is purely additive — it creates tables and columns and never touches a row. In place rewrites existing data: the storage-form convergence a Field.datetime column needs when the database predates the canonical UTC storage (ADR-0053 addendum D-B1..D-B4). It carries a row count because that is the number deciding whether to run it now; on MySQL it reads widen_datetime_columns and is an ALTER … MODIFY table rebuild that holds a metadata lock for its duration.

Both are safe to apply — the convergence preserves every stored instant and is idempotent — but only the second takes time proportional to your data.

Occupancy check (SQLite)

A running os dev / os serve holding the same SQLite file open is the usual way a migration goes wrong: the migration itself is transactional and swaps tables inside the file, but the live server keeps prepared statements and a schema cookie that the migration invalidates, and its writes can collide as SQLITE_BUSY. Before booting, os migrate checks two things:

  1. Which processes hold the file open (/proc on Linux, lsof on macOS). The signal that works in every journal mode, and the only one that names the process to go and stop. It is also the only one that sees an idle server on a rollback-journal database, where a lock lasts no longer than the transaction that took it.
  2. A SQL lock probe (PRAGMA locking_mode = EXCLUSIVE under busy_timeout = 0). ObjectStack keeps file-backed SQLite in WAL mode, where this catches any attached connection — idle or not — including the cases step 1 cannot reach: a platform without process inspection, or a database held by another user's process.

Either one firing counts as busy. Both are non-destructive: no row is read or written.

✗ .objectstack/data/standalone.db is in use — it is open in pid 12367 (node).
⚠ Stop the process using it (a running "os dev"/"os serve" is the usual one)
  and re-run, or pass --force to migrate anyway.
Command If the database is in use
os migrate plan Warns and continues — a plan writes nothing either way
os migrate apply Refuses (exit 1, error: database_busy under --json). Stop the other process, or pass --force
os migrate files-to-references --apply Refuses likewise — it rewrites rows, so a concurrent writer is at least as dangerous

The check applies to SQLite only: Postgres and MySQL take their own server-side locks. Only same-user processes are visible without elevated privileges, and a -wal/-shm left behind by a crashed process is deliberately never treated as occupancy on its own.

Category Examples Applied by
safe relax NOT NULL → nullable, widen a varchar, create a declared index, replace a legacy global unique with its tenant-scoped composite os migrate apply (and dev auto-reconcile)
needs_confirm non-narrowing type change, rebuild a non-unique index whose columns changed os migrate apply
destructive drop an orphaned column or index, tighten NOT NULL, narrow a type, rebuild an index as UNIQUE os migrate apply --allow-destructive

Index drift

plan covers indexes as well as columns:

Op What it means
create_index Metadata declares an index the database does not have
replace_unique_index A field's unique used to be enforced platform-wide, but metadata now scopes it per tenant — the legacy single-column index is swapped for the (tenantField, field) composite. A pure relaxation: it creates before it drops, and cannot fail
recreate_index An index exists under the declared name but with different columns/uniqueness. The additive sync skips it by name, so it must be dropped and rebuilt
drop_index An index carrying ObjectStack's generated naming (uniq_… / idx_…) that metadata no longer declares

Orphan detection is deliberately limited to indexes ObjectStack itself generated. A hand-rolled covering index you added in psql is never reported as drift, and --allow-destructive will not delete it.

**Dev self-heal.** `os dev` runs the SQL driver with `autoMigrate: 'safe'`, so safe changes (you just made a field optional; a `unique` field became tenant-scoped) are applied to your existing dev database automatically on restart — no `os migrate` needed, no data loss. Auto-reconcile is **dev-only and never destructive**; it is force-disabled under `NODE_ENV=production`, where every change is shown by `os migrate plan` before you apply it deliberately. `os migrate` only sees objects in your **compiled artifact** — run `os build` first. It never drops a table that is absent from your metadata, and on SQLite it reconciles via a table rebuild (copy → swap) that preserves your data.

Data migrations

The commands above reconcile schema. A data migration rewrites rows, and whether it is done is a fact about your database, not about the platform version you installed — so each deployment runs it, and its result is recorded where the data lives.

Command Description
os migrate files-to-references Convert legacy file-field values to sys_file references, verify the ownership ledger, and record the deployment's migration flag
os migrate value-shapes Scan stored reference and structured-JSON field values against the platform's value contract, and record the deployment's migration flag when clean
os migrate files-to-references            # Dry run: full report, writes nothing
os migrate files-to-references --apply    # Convert, verify, record the flag (prompts)
os migrate files-to-references --apply --yes --json   # CI / scripts
os migrate files-to-references --object product       # Restrict to one object (repeatable)

A file / image / avatar / video / audio field value is an opaque sys_file id that the platform owns. Values written before that (an inline {url, name, …} blob, or a URL naming this platform's own …/storage/files/:id resolver) are converted in place; external URLs are reported, never re-hosted — re-hosting third-party content is a licensing and privacy decision, and the right fix is usually to model the field as a url field instead.

The run then reconciles what records actually hold against what sys_file records as each file's owner. Zero blocking discrepancies is what records the flag — and that flag, not the version number, is what enables behaviour that depends on the data actually being migrated. Never running it is safe: files are simply retained forever, and media values keep warning instead of failing.

Exit status is 0 only when the self-check passes, so CI can gate on it.

What the flag turns on

Once verified Effect
Media value shapes A malformed file / image / avatar / video / audio value is rejected (400 invalid_type) instead of warned about. Set OS_ALLOW_LAX_MEDIA_VALUES=1 to re-open leniency while diagnosing.
Released-file collection A field file whose one owning record lets go (the field is cleared or the record deleted) is tombstoned into the declared 30-day grace window; re-referencing the id within the window revives it, and after it the platform sweep reclaims the row and its bytes. Unverified deployments keep every released file forever.

Other value classes are unaffected: a lookup or location value keeps its own warn-first rollout until os migrate value-shapes (below) supplies their evidence, because this migration is evidence about file values and says nothing about theirs.

A dry run writes **nothing** — not the conversions, and not the flag either, even when the self-check would pass. `--apply` is the only writing mode. A later run that *fails* its self-check clears the flag's verified state, so a database that has drifted closes its own gate.

A running server reads the flag once; after migrating, restart it for enforcement (and release-time tombstoning) to take effect. The sweep's final delete check re-reads the flag fresh, so a later failing run stops collection without a restart.

os migrate value-shapes

The same gate for the non-media value classes — references (lookup, master_detail, user, tree) and structured JSON (location, address, composite, repeater, record, vector).

os migrate value-shapes                    # Scan: full report, writes nothing
os migrate value-shapes --apply            # Scan, then record the flag if clean (prompts)
os migrate value-shapes --apply --yes --json   # CI / scripts
os migrate value-shapes --object contact       # Restrict to one object (repeatable)

This one converts nothing. Its sibling rewrites legacy file values because the platform narrowed that storage form and therefore owes the conversion; a location stored as {latitude, longitude} instead of {lat, lng} is application data whose correct value only its author knows. So the run reports — object, field, type, how many records, sample record ids, and the parse issue — and you fix the values (or the code writing them) and re-run until it is green. Because there is nothing to convert, --apply's only write is the flag row itself.

A scan that is truncated (by --max-records) or that cannot read an object fails the gate even with zero violations found: "none in the part we read" is not the claim the flag makes.

Once verified Effect
Reference + structured-JSON value shapes A malformed value of those classes is rejected (400 invalid_type) instead of warned about. Set OS_ALLOW_LAX_VALUE_SHAPES=1 to re-open leniency while diagnosing.

This flag is deliberately separate from the file migration's. That one attests that file values were migrated and their ownership reconciled — it says nothing about whether a lookup id or a location payload is well formed, so it may not vouch for these classes. A deployment can legitimately have passed either without the other.

`OS_DATA_VALUE_SHAPE_STRICT_ENABLED=1` turns on **every** value class at once, regardless of which migrations this deployment has run. It is the "I already know my data" lever, not the route to strictness — the route is running the migration that produces the evidence.

Same writing rules as its sibling: a dry run writes nothing, --apply is the only writing mode, a later failing run clears the verified state, and a running server reads the flag once — restart it after a successful apply.

A database created by this version needs no migration

A deployment whose database the platform creates from empty records these flags at that moment, so it is enforcing from its first boot and never enters the warn regime at all. Nothing to run: the fact a migration would establish — no legacy value is stored here — is already settled by the store having no history.

The platform attests this only for a store it watched itself create: every table made by that first boot, none found already present. A database that existed before — an upgrade, a restore, a store shared with anything else — attests nothing and produces its evidence by running the command, because "found empty" and "created empty" are not the same claim.

Importing legacy values into such a deployment is rejected at the write path rather than silently accepted. That is the intended outcome; if you must admit them temporarily, OS_ALLOW_LAX_MEDIA_VALUES=1 (media) and OS_ALLOW_LAX_VALUE_SHAPES=1 (references and structured JSON) re-open leniency per class, and re-running the corresponding migration re-establishes its flag from the data itself.

Scaffolding

Command Alias Description
os generate <type> <name> os g Generate metadata files
os create <type> [name] Create a new package from template

os generate (alias: os g)

Generates properly typed metadata files with barrel index management.

os g object customer        # Generate a Customer object
os g view customer          # Generate a Customer list view
os g action approve         # Generate an action
os g flow customer          # Generate an automation flow
os g agent support          # Generate an AI agent
os g dashboard sales        # Generate a dashboard
os g app crm                # Generate an app definition

os g object task -d lib/    # Override target directory
os g object task --dry-run  # Preview without writing

Available types:

Type Default Directory Description
object src/objects/ Business data object with fields
view src/views/ List or form view definition
action src/actions/ Button or batch action
flow src/flows/ Automation flow
agent src/agents/ AI agent
dashboard src/dashboards/ Analytics dashboard
app src/apps/ Application navigation

Options:

  • -d, --dir <directory> — Override target directory
  • --dry-run — Preview without writing files

What it does:

  1. Creates a typed TypeScript file using Data.Object, UI.View, Automation.Flow, etc.
  2. Creates or updates the barrel index.ts in the target directory
  3. Shows a hint to run objectstack validate

os create

Creates new packages from built-in templates (for monorepo-level scaffolding):

os create plugin analytics    # Create packages/plugins/plugin-analytics
os create example my-app      # Create examples/my-app

Quality

Command Description
os lint [config] Check metadata for style and convention issues (beyond validate's hard gates)
os test [files] Run Quality Protocol test scenarios against a running server
os doctor Check development environment health

os lint

Style and convention checks on top of os validate — naming, labels, translation coverage — with a 0-100 quality score:

os lint                # Style / convention checks
os lint --score        # Append a 0-100 metadata quality score (letter-graded)
os lint --fix          # Show what would be fixed (dry-run)
os lint --json         # JSON output for CI

os test

Runs Quality Protocol test scenarios (JSON-based BDD) against a running ObjectStack server.

os test                               # Default: qa/*.test.json
os test qa/my-test.json               # Specific test file
os test --url http://localhost:4000    # Custom server URL
os test --token my-api-key            # With authentication

os doctor

Checks your development environment and reports issues:

os doctor           # Check health
os doctor -v        # Show fix suggestions for warnings

Checks performed:

  • Node.js version (≥18 required)
  • pnpm installation
  • TypeScript availability
  • Dependencies installed
  • @objectstack/spec build status
  • Git availability

Authentication

Command Description
os register Create an account and store local credentials
os login Sign in and store credentials in ~/.objectstack/credentials.json
os whoami Show the current authenticated user
os logout Revoke the server session and clear local credentials

os register

Creates a user account and stores the returned token locally.

os register
os register --email user@example.com --name "Jane Doe" --password secret
os register --url https://api.example.com

os login

In an interactive terminal, login uses a browser-based device flow by default: the CLI prints a one-time verification URL, opens the browser, and polls until you approve access in your browser.

os login
os login --url https://api.example.com
os login --no-browser

If a valid token already exists, os login exits successfully with "Already logged in as <email>". Use os logout to switch users, or pass --force to re-authenticate.

For CI and other non-interactive contexts, pass email/password directly:

os login --email user@example.com --password secret

os logout

Logout calls POST /api/v1/auth/sign-out before deleting local credentials, so the server-side session is revoked as well.

os logout

Cloud Environments

Command Description
os environments list List environments visible to the current session
os environments show <id> Show one environment
os environments create Provision a new environment
os environments switch <id> Set the active environment for later CLI calls
os environments bind <id> Bind a compiled local artifact to an existing environment

Create an environment from a local artifact

Compile first, then create an environment and bind the generated dist/objectstack.json in one call:

os compile
os environments create --org <org-id> --name CRM --artifact ./dist/objectstack.json

The server stores the absolute artifact path in environment metadata. On environment-kernel boot, ObjectStack loads the JSON bundle, registers schemas, and seeds records from the bundle's data arrays.

Bind an existing environment

os environments bind <environment-id> --artifact ./dist/objectstack.json
os environments bind <environment-id> --artifact ./dist/objectstack.json --build

--build runs objectstack compile before updating the project. --reseed is reserved for the server-side reseed endpoint; use it only when that endpoint is available in your deployment.

Configuration

The CLI looks for objectstack.config.ts (or .js, .mjs) in the current directory:

import { defineStack } from '@objectstack/spec';
import * as objects from './src/objects';
import * as actions from './src/actions';

export default defineStack({
  manifest: {
    id: 'com.example.my-app',
    namespace: 'my_app',
    version: '1.0.0',
    type: 'app',
    name: 'My App',
    description: 'My ObjectStack application',
    // Protocol major this app is authored against (ADR-0087 load-time check).
    engines: { protocol: '^17' },
  },

  objects: Object.values(objects),
  actions: Object.values(actions),
});

Config File Auto-Detection

The CLI searches for configuration files in this order:

  1. objectstack.config.ts
  2. objectstack.config.js
  3. objectstack.config.mjs

You can also specify a path explicitly:

os compile path/to/my-config.ts

Typical Workflow

# 1. Create project
os init my-crm && cd my-crm

# 2. Define your data model
os g object account
os g object contact
os g object opportunity

# 3. Add business logic
os g flow lead-qualification
os g agent sales-assistant

# 4. Validate everything
os validate

# 5. Start development with Console UI
os dev --ui

# 6. Build for production
os compile

# 7. Deploy: ship just the artifact
os start                                              # locally
OS_ARTIFACT_PATH=https://cdn.you.com/app.json os start  # remote artifact

Source vs Artifact

ObjectStack treats objectstack.config.ts and objectstack.json as two forms of the same schema — authoring source vs compiled artifact:

Aspect objectstack.config.ts objectstack.json
Role Authoring source Deployable artifact
Format TypeScript (defineStack({...})) Pure JSON
May contain code Yes (handler: async (ctx) => {...}) No — handlers are lowered to a sibling objectstack-runtime.<hash>.mjs
Loaded by os serve (via bundle-require) os start (via loadArtifactBundle — file or http(s)://)
Schema ObjectStackDefinitionSchema Same schema, plus runtimeModule reference
Produced by You (or os generate) os compile / os build

The artifact is fully self-describing: its requires: [...] field declares which platform services (ai, automation, analytics, …) the runtime should auto-register, so os start needs nothing other than the JSON itself to bring up a working server.

This is why an artifact is the canonical "portable deployment unit" — you can host it on S3 / GitHub raw / a CDN, and any ObjectStack runtime can fetch and execute it with no additional source code on the server.

Next Steps

CI/CD Integration

All commands that produce output support --json for machine-readable output:

# In CI pipeline
os validate --json --strict
os compile --json -o dist/objectstack.json
os info --json

Example GitHub Actions step:

- name: Validate ObjectStack Config
  run: npx objectstack validate --strict --json

- name: Build ObjectStack Artifact
  run: npx objectstack compile --json