CLI reference
pip install linkworldlinkworld --helpCommands
Section titled “Commands”linkworld init <slug>
Section titled “linkworld init <slug>”Scaffold a new app in ./<slug>. Generates the manifest, server-side
handler stub, Dockerfile, deploy workflow, and a web/ subdir with
a working frontend bundle (HTML/CSS/JS) wired to the postMessage
bridge.
| Flag | Default | Description |
|---|---|---|
--lang | python | python or typescript |
--name | derived from slug | Display name |
--description | empty | One-line description |
--github-owner | your-org | Used in the GHCR image URL |
--directory | ./<slug> | Override target dir |
linkworld init tax-bot --lang python --github-owner youlinkworld init tax-bot-ts --lang typescript --github-owner youThe scaffolded web/index.html already shows the bridge protocol
(init listener, tool call, resize). Edit web/styles.css and
web/app.js to build your real UI; see
Building app UIs.
linkworld login
Section titled “linkworld login”Open a browser to GitHub OAuth. The CLI receives a JWT and stores it
at ~/.linkworld/config.toml.
# OAuth flowlinkworld login
# Fallback: paste an admin-issued JWTlinkworld login --token eyJhbGc...linkworld whoami
Section titled “linkworld whoami”Print the current dev account (email, github_login, publish_allowed,
tenant context). Useful in CI to verify the right token is in
LINKWORLD_TOKEN before a release deploys to production.
$ linkworld whoami[email protected] (publish_allowed=true)Exit codes: 0 if authenticated, 1 if not.
linkworld logout
Section titled “linkworld logout”Forget the locally-stored token by rewriting ~/.linkworld/config.toml
without the [auth] section. Server-side the token stays valid until
its TTL expires — for revocation, contact a platform admin.
linkworld logoutlinkworld apps list / linkworld apps show <slug>
Section titled “linkworld apps list / linkworld apps show <slug>”Inspect what you’ve published.
$ linkworld apps listslug name status current versionmy-bot My Bot active 1.2.0news-feed News Feed draft —
$ linkworld apps show my-botmy-bot — My Bot status: active current version: 1.2.0
versions: 1.2.0 published success 2026-04-12 ghcr.io/you/my-bot:1.2.0 1.1.0 published success 2026-03-30 ghcr.io/you/my-bot:1.1.0apps show is the fastest way to confirm a deploy actually landed.
The status column reflects the platform’s view (draft = registered
but no current_version set; active = at least one version is
current; archived = explicitly retired by the dev).
linkworld upgrade <slug>
Section titled “linkworld upgrade <slug>”Force tenant installs of an app to its current published version.
Bypasses the per-install upgrade_policy — used in dev to push a
freshly-published fix to test tenants without waiting for auto-upgrade.
| Flag | Default | Description |
|---|---|---|
--target-version | current_version | Pin to a specific published version. |
--tenant <UUID> | all installs | Restrict to a single tenant. |
--force-grant-new-scopes | off | Bypass the scope-expansion-needs-consent guard. Tenants normally re-consent in the UI when a new scope appears; this flag is for dev-tenant pushes only. |
# Push the current version to every tenant that has an old onelinkworld upgrade my-bot
# Or just to a specific test tenantlinkworld upgrade my-bot --tenant 11111111-...linkworld doctor
Section titled “linkworld doctor”Diagnose the local environment. Runs the env-setup checks
linkworld deploy would do anyway — but with actionable fix-hints
when something’s off, so you don’t waste a deploy round-trip.
Five checks:
| Check | What it verifies |
|---|---|
login | ~/.linkworld/config.toml has a token AND the platform accepts it (/api/dev/me returns 200). Reports publish_allowed=false as a warn. |
docker | docker is on PATH and the daemon responds. Required unless you always deploy with --skip-build. |
ghcr | ~/.docker/config.json shows a credential for ghcr.io (or a credHelper). Best-effort — false negatives possible with helper-based auth. |
sdk | linkworld-sdk is importable in this Python env (used by linkworld eval). |
project | linkworld.app.yaml exists in the current directory. |
Exit code: 1 if any check failed, 0 if all-pass or warnings
only. Run from the project root for the full picture.
linkworld lint
Section titled “linkworld lint”Run the same preflight linkworld deploy runs, without deploying.
Catches manifest errors, scope-existence problems, and cross-file
inconsistency (e.g. ctx.tools.call("email_search", ...) when the
manifest doesn’t declare mail.read) before you spend a build cycle
on a version the platform would reject.
| Flag | Default | Description |
|---|---|---|
--manifest | linkworld.app.yaml | Override the manifest path |
$ linkworld lintRunning preflight checks…✓ preflight passed
# Or with errors:$ linkworld lint error linkworld.app.yaml — required_scopes references unknown scope 'mail.send_attachments' fix: Run `linkworld scopes` (or call `mcp.list_skills`) to see the catalog. Common typos: 'mail.send_attachments' → 'mail.send'.Exit codes: 0 on pass, 1 on any error-severity issue, 2 if the
preflight request itself fails (network/auth).
linkworld deploy
Section titled “linkworld deploy”Run preflight, build the Docker image, push to GHCR, register the
version with the platform, and (by default) promote it to
current_version.
| Flag | Default | Description |
|---|---|---|
--version | derived from manifest | Override the version published |
--skip-build | off | Don’t build/push; just register an existing image |
--no-preflight | off | Skip the preflight check. Use only when the platform is unreachable. |
--no-promote | off | Register the version but don’t make it current |
--manifest | linkworld.app.yaml | Override the manifest path |
--bundle | auto-detect | Path to the frontend bundle dir to upload. Auto-detects ./dist, then ./web. Pass --bundle '' to skip. |
Deploy lifecycle:
- Preflight — POST manifest + source files to
/api/dev/preflight. Manifest validation, lint (cross-file scope ↔ tool consistency), and scope-existence check against the running platform’s scope registry. If any error: print issues + fix-hints, exit1. - Image build + push —
docker build+docker pushagainst the manifest’sruntime.image. Skipped with--skip-build. - Version registration —
POST /api/dev/apps/<slug>/versions. - Bundle upload — if
./distor./webexists (or--bundleis set), tarball + upload as the iframe content for/apps/<slug>.
# Common case: preflight + build + push + register + upload bundlelinkworld deploy
# CI flow: image already in GHCR, preflight + registerlinkworld deploy --skip-build --version 1.2.3
# Skip bundle upload (headless app — no /apps/<slug> UI)linkworld deploy --bundle ''
# Platform unreachable — bypass preflight (rarely needed)linkworld deploy --no-preflightBundle upload is immutable per version. Re-deploying the same version with a different bundle is rejected — bump to a new version to publish updated UI. Tenants stay frozen on whatever version they installed until they explicitly upgrade.
linkworld dev
Section titled “linkworld dev”Run a local dev loop for your frontend bundle. Spins up two HTTP
servers — a static-file server with hot-reload for the bundle, and
a “fake parent” that replicates the tenant shell’s
postMessage bridge — so you can iterate on
HTML/CSS/JS without a linkworld deploy round-trip.
| Flag | Default | Description |
|---|---|---|
--dir | auto-detect | Bundle directory. Defaults to ./dist, then ./web. |
--app-id | from manifest | App slug used in the init payload. |
--mocks | linkworld.dev.json | JSON file mapping tool names → mock responses. |
--bundle-port | 5173 | Port for the bundle’s static server. |
--parent-port | 5174 | Port for the fake-parent shell. |
--no-open | off | Don’t auto-open a browser. |
$ linkworld dev
linkworld dev app_id: your-app bundle: /path/to/web mocks: /path/to/linkworld.dev.json (using defaults) open at: http://localhost:5174/ bundle on: http://localhost:5173/
Edit files in the bundle dir; the iframe reloads automatically.Ctrl+C to stop.The fake parent ships a light/dark toggle in its chrome so you
can verify the theme.changed flow without needing a real tenant
shell. It also logs every bridge message to the chrome bar — handy
when something stops working.
Mock config — linkworld.dev.json
Section titled “Mock config — linkworld.dev.json”Drop this file next to your manifest to control what platform tool calls return:
{ "email_send": { "ok": false, "error": { "decision": "scope_denied", "message": "App didn't request mail.send.", "neededScopes": ["mail.send"] } }, "echo": { "ok": true, "result": { "echoed": "args reflected back" } }}If a tool is called that isn’t in the file, the parent returns
{ decision: 'unmocked', message: ... } so the absence is loud.
The file is re-read every few seconds — edits pick up without
restarting the server.
What the dev loop is not
Section titled “What the dev loop is not”- Not a real-platform integration test. Real tools, scopes, and
audit logs only happen against
api.linkworld.ai(or your local backend). For that, deploy to staging or run the full Linkworld monorepo onlocalhost:3000. - Not security-equivalent. CSP, sandbox, and origin gates the
production CDN enforces are skipped here so you can iterate
without fighting policy. The production behaviour kicks in as
soon as you
linkworld deploy.
linkworld secrets
Section titled “linkworld secrets”Manage dev-default secrets visible to every tenant install of an app. Tenant-specific overrides live in the tenant’s app-settings UI.
linkworld secrets set OPENAI_KEY=sk-real-... --app tax-bot --description "LLM key"linkworld secrets list --app tax-botlinkworld secrets get OPENAI_KEY --app tax-bot # metadata only, never the valuelinkworld secrets delete OPENAI_KEY --app tax-botKey format: ^[A-Z][A-Z0-9_]{0,63}$ (uppercase, digits, underscore;
first char letter). Values are encrypted at rest with AES-256-GCM and
never displayed after creation.
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Bad input (invalid slug, missing required flag) |
| 2 | Platform error (4xx/5xx, network) |
| 3 | Auth error (no token, expired, suspended) |