oikina
v0.1.8
Published
Experimental CLI for validating and deploying Oikina Apps.
Maintainers
Readme
Oikina CLI
Scaffold, validate, and deploy a type-safe constrained full-stack React/Vite app to the Oikina Hosted Runtime.
The generated template/AGENTS.md documents executable action/SQLite patterns,
reactive cleanup, replace-all secret sync, and pre-deploy troubleshooting.
Publishing the CLI does not by itself prove that a selected runtime target has
enabled full-stack execution.
oikina login <api-key> validates the supplied deploy credential with Hosted
Runtime before writing it. Canonical beta keys and the existing legacy owner
credential are both supported; malformed, invalid, inactive, or temporarily
unverifiable values leave the mode-0600 credential file untouched.
The full-stack workflow supports Linux and native macOS with Node.js 22.16 or newer. Windows full-stack workflows require WSL2; native Windows remains fail-closed until equivalent reparse-point, ACL, and process-tree guarantees are available. Static CLI commands remain supported natively on Windows.
Workflow:
npx oikina new incident-tracker
cd incident-tracker
# edit the app
npx oikina dev
npx oikina validate
npx oikina login <deploy-token>
npx oikina deploydeploy defaults to Oikina Hosted. Use --target oikina for the explicit equivalent or --target cloudflare for Workers and D1. Cloudflare requires server.databaseMode: "portable", project-local Wrangler 4.127.1, and either an existing Wrangler login or scoped CLOUDFLARE_API_TOKEN. The first D1 creation requires --yes. A durable deployment allocates a stable <normalized-app-name>-db-<random> database name and records it in the ignored .oikina/cloudflare-database.json file; later deployments reuse that name. Temporary preview deployment is opt-in with --temporary and allocates a fresh database name. This target supports declared request/response actions, Vite assets, SPA fallback, and D1. Portable SQL parameters and row values are limited to null, strings, and finite numbers; bigint and blob values remain available only through the Hosted synchronous database API. Cloudflare deployment stops before remote changes when .env.production contains non-VITE_ values because server-secret sync is not implemented for this target.
Generated apps use four versioned package modules:
import { createReactClient } from "oikina/react";
import { defineContracts, s } from "oikina/schema";
import { defineActions, defineQuery } from "oikina/server";
import { oikina } from "oikina/vite";These modules own the browser protocol, runtime schemas, constrained action definitions, and authenticated local Vite proxy. The generated project keeps only its configuration and application code.
login atomically replaces
$XDG_CONFIG_HOME/oikina/credentials.json (or
~/.config/oikina/credentials.json) with {version, apiUrl, token} at mode
0600; the containing directory uses mode 0700. The validated API origin is
bound to the saved key, and deploy refuses to combine it with a different
OIKINA_API_URL. Legacy {token} files remain readable for compatibility. For
automation, set OIKINA_TOKEN and OIKINA_API_URL together instead; the
environment credential takes precedence over the saved record.
dev and deploy automatically prefer pnpm when it is available and working,
otherwise they use npm. Before running the requested script, they install
dependencies when manager integrity metadata is missing or invalid, or when
the selected manager's package, lock, or configuration inputs have changed.
If no lockfile exists, the first install creates one with pnpm install or
npm install --no-audit --no-fund. Later installs use
pnpm install --frozen-lockfile or npm ci --no-audit --no-fund.
They record an Oikina-managed fingerprint after a successful install
so unchanged subsequent deploys skip that work. Generated app scripts route
pnpm dev and npm run dev through the CLI, which starts the action runtime
and then runs the internal dev:ui Vite script. Older app projects without
dev:ui continue to use their dev script. App manifests also run
pnpm typecheck or npm run typecheck before the build, so replacing the
project build script cannot bypass server contract checking. Commands run
without a shell on POSIX. Native Windows invokes only the fixed trusted
npm.cmd/pnpm.cmd command and CLI-owned arguments through cmd.exe, as
required for Windows command shims; app input never contributes command text.
For app manifests, dev binds a private ephemeral 127.0.0.1 action gateway,
injects an unexposed proxy token into the Vite process, and runs every action in
a fresh one-shot Node child against .oikina/dev/data/app.sqlite. It re-runs the
app typecheck and secure server bundler before each action, so invalid current
server code fails generically instead of serving a stale bundle. Normal restarts
preserve local data; oikina dev --reset-data removes only the dedicated local
data directory and reapplies trusted migrations.
Host-mode local execution is intentionally a developer convenience, not a sandbox. App code retains the developer OS account's filesystem and network authority. Production alone supplies the fixed networkless Worker, read-only root, dropped capabilities, resource limits, and authenticated Manager boundary.
Only regular
files beneath the declared dist directory are uploaded; symlinks, unsafe
paths, missing index.html, more than 1,000 files, and artifacts over 8 MiB
are rejected. Remote uploads declare the already validated manifest slug in
X-Oikina-App-Slug so scoped authorization completes before the server reads
the upload. Set OIKINA_API_URL to target a non-production service, such as
http://127.0.0.1:8080.
The generated app imports its schema, server, React client, and Vite helpers
from the exact oikina version that created it. The project keeps only its UI,
typed client configuration, shared action contracts, handlers, manifest, and
migrations. One shared contract supplies server validation, browser call types,
and response decoding. The client checks contract and manifest action names at
startup. It also includes
a trusted migration and a small durable SQLite notes example. Its fixed .mjs entrypoint
re-exports only
typed local modules; deployment bundles those private sources separately from
the public Vite output. Deploy tokens and other secrets must never be placed in
client source or Vite environment variables.
Database calls accept one bounded comment-free prepared statement: query is
SELECT only and execute is INSERT, UPDATE, or DELETE only. Transaction
callbacks are synchronous and return only a primitive or nullish value.
Migration history is append-only and each entry is one constrained CREATE
TABLE statement. Generated reactive query consumers must abort on cleanup;
reconnect obtains a fresh snapshot rather than replaying past events.
publish-local remains available for registry development and does not call a
remote service.
For app bundles, the CLI reads the manifest-declared migration JSON through a
bounded no-follow snapshot, validates the shared migration contract, and uploads
only ordered { version, name, digest } expectations alongside the private
bundle. The SQL remains in the private app module/artifact and never enters the
browser output. CLI analysis is import/bundle hygiene, not confinement. It
proves only a single named actions export and catches obvious statically
resolved forbidden imports. It does not evaluate the module value, match its
keys to the manifest, detect all side effects or computed imports, or block
process.getBuiltinModule, fetch, or runtime capabilities. The active
one-shot worker imports only inside its fresh sandbox, validates exact deeply
frozen actions and migrations exports against the manifest and uploaded
digests, and bounds JSON and database operations. Under complete explicit
staging configuration, Hosted routes declared actions through the authenticated
Manager and mounts stable app data keyed by organization + app.
For app manifests, .env.production is parsed strictly before upload. VITE_*
values are public Vite inputs; all other allowed names are synchronized as
server-only values in a distinct deploy field. The CLI always sends that field,
including {} to clear removed values, and refuses a git-tracked production
file. Local .env.development uses the same split: public values reach Vite and
server values reach only each fresh action child.
