Skip to content

Latest commit

 

History

History

README.md

@objectstack/cli

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.

Installation

pnpm add -D @objectstack/cli

The CLI is available as objectstack or the shorter alias os.

Quick Start

# 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

Commands

Development

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

Build & Validate

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.)

Scaffolding

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.

Cloud — publish & install

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-data

os 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.

Credentials and server URL

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).

Plugin Management

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.

Quality

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

Reference

Command Description
os explain [schema] Display human-readable explanation of an ObjectStack schema

Configuration

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],
});

Environment files (.env)

dev, start, and serve all load .env files following the Vite / Next.js convention, in this order (later files override earlier):

  1. .env
  2. .env.${NODE_ENV} — development for os dev, production for os start (override with NODE_ENV=...)
  3. .env.local
  4. .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.

CLI Options

Global

  • --version — Show version number
  • --help — Show help (os --help, or os <command> --help for 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.

os init

  • -t, --template <template> — Template: app (default), plugin (a metadata package — declarative objects another stack loads, not the kernel code plugin os create plugin emits), empty
  • --no-install — Skip dependency installation

os compile

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

os validate

  • --strict — Treat warnings as errors
  • --json — Output result as JSON

os serve

  • -p, --port <port> — Server port. Resolution: --port › $OS_PORT › $PORT › 3000. With --dev a 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/console is installed (default: true)
  • --no-server — Skip starting HTTP server plugin

os generate

  • -d, --dir <directory> — Override target directory

os plugins and os help (not commands)

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.

os info

  • --json — Output as JSON

os doctor

  • -v, --verbose — Show fix suggestions for warnings
  • --scan-deprecations — Scan for deprecated patterns

oclif Plugin System

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.

How Plugin Extension Works

  1. Create an oclif plugin package with its own oclif config in package.json
  2. Export oclif Command classes from the plugin's src/commands/ directory
  3. Load the plugin through an os distribution you build: a package whose own package.json lists the plugin in both oclif.plugins and dependencies. oclif's core-plugin loader matches oclif.plugins names only against dependencies; a name listed under devDependencies alone never loads.

Creating a CLI Plugin

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"

Key Differences from Previous Plugin Model

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

Typical Workflow

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

Architecture

@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

Public subpath exports

@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.

@objectstack/cli/hook-body

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.

Default capability slate (always-on)

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).

License

Apache-2.0. See LICENSING.md.