@lizard-build/cli 4.0.44 → 4.0.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/AGENTS.md +3 -0
  2. package/dist/commands/add.js +2 -7
  3. package/dist/commands/add.js.map +1 -1
  4. package/dist/commands/agents.js +21 -2
  5. package/dist/commands/agents.js.map +1 -1
  6. package/dist/commands/domain.js +32 -2
  7. package/dist/commands/domain.js.map +1 -1
  8. package/dist/commands/init.js +15 -0
  9. package/dist/commands/init.js.map +1 -1
  10. package/dist/commands/metrics.js +11 -2
  11. package/dist/commands/metrics.js.map +1 -1
  12. package/dist/commands/s3.d.ts +15 -0
  13. package/dist/commands/s3.js +59 -0
  14. package/dist/commands/s3.js.map +1 -1
  15. package/dist/commands/sandbox.js +13 -5
  16. package/dist/commands/sandbox.js.map +1 -1
  17. package/dist/commands/skill.js +89 -16
  18. package/dist/commands/skill.js.map +1 -1
  19. package/dist/commands/up.js +23 -6
  20. package/dist/commands/up.js.map +1 -1
  21. package/dist/commands/upgrade.js +17 -3
  22. package/dist/commands/upgrade.js.map +1 -1
  23. package/dist/commands/volume.js +38 -4
  24. package/dist/commands/volume.js.map +1 -1
  25. package/dist/commands/workspace.js +4 -7
  26. package/dist/commands/workspace.js.map +1 -1
  27. package/dist/index.js +2 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/lib/api.js +21 -6
  30. package/dist/lib/api.js.map +1 -1
  31. package/dist/lib/config.d.ts +7 -0
  32. package/dist/lib/config.js +10 -1
  33. package/dist/lib/config.js.map +1 -1
  34. package/dist/lib/default-project.js +7 -2
  35. package/dist/lib/default-project.js.map +1 -1
  36. package/dist/lib/skills-data.generated.d.ts +2 -0
  37. package/dist/lib/skills-data.generated.js +6 -1
  38. package/dist/lib/skills-data.generated.js.map +1 -1
  39. package/dist/lib/updater.d.ts +1 -1
  40. package/dist/lib/updater.js +1 -1
  41. package/package.json +1 -1
  42. package/scripts/gen-skills.mjs +24 -5
  43. package/skill-data/core/SKILL.md +122 -377
  44. package/skill-data/core/references/deploys.md +201 -0
  45. package/skill-data/core/references/sandboxes.md +169 -0
  46. package/skill-data/core/references/sdk.md +227 -0
  47. package/src/commands/add.ts +2 -6
  48. package/src/commands/agents.ts +20 -4
  49. package/src/commands/domain.ts +36 -2
  50. package/src/commands/init.ts +18 -0
  51. package/src/commands/metrics.ts +11 -2
  52. package/src/commands/s3.ts +65 -0
  53. package/src/commands/sandbox.ts +15 -5
  54. package/src/commands/skill.ts +95 -21
  55. package/src/commands/up.ts +20 -6
  56. package/src/commands/upgrade.ts +14 -3
  57. package/src/commands/volume.ts +35 -4
  58. package/src/commands/workspace.ts +4 -9
  59. package/src/index.ts +4 -1
  60. package/src/lib/api.ts +21 -6
  61. package/src/lib/config.ts +11 -1
  62. package/src/lib/default-project.ts +5 -2
  63. package/src/lib/updater.ts +1 -1
  64. package/test/unit/default-project.test.ts +8 -2
  65. package/test/unit/s3-presign.test.ts +32 -0
  66. package/test/unit/sandbox-transport.test.ts +5 -1
  67. package/test/unit/sandbox.test.ts +1 -0
  68. package/test/unit/skills.test.ts +31 -12
@@ -9,47 +9,44 @@ allowed-tools: Bash(lizard:*), Bash(~/.lizard/bin/lizard:*), Bash(which:*), Bash
9
9
 
10
10
  Lizard runs sandboxes and hosts apps, services, agents and managed databases. All capabilities are exposed through the `lizard` CLI (npm package `@lizard-build/cli`). This skill teaches you to drive it. If `lizard` isn't on PATH, install it: `curl -fsSL https://lizard.build/install.sh | bash` on macOS and Linux (no Node.js or sudo needed; if `lizard` is still not found, call `~/.lizard/bin/lizard`), `irm https://lizard.build/api/cli/install.ps1 | iex` in Windows PowerShell, or `npm install -g @lizard-build/cli` anywhere. The install script ends by running `lizard login`, so its output may already hold the sign-in links: see [Sign-in](#sign-in).
11
11
 
12
- This guide covers both: [Sandboxes](#sandboxes) for running code and agents, and deploys for services that keep running.
12
+ This guide covers the whole platform. Three references go deeper; read one only when the task needs that depth:
13
13
 
14
- If `$ARGUMENTS` is non-empty, treat it as the user's request and act on it. If empty, ask what they want to do on Lizard.
15
-
16
- ## Read this first
17
-
18
- This skill documents platform behavior (build pipeline, env precedence, what knobs the API exposes). It does not describe the user's repo.
19
-
20
- Before writing commands for a specific project:
14
+ - `lizard skills get core --ref sandboxes --json`: templates, commands, files and ports in depth, lifetime, sizes, shell, coding agents in a sandbox, desktop (computer use), volumes, snapshots and pause/resume.
15
+ - `lizard skills get core --ref deploys --json`: how builds are chosen, git and upload deploys, service fields, worker mode, secret precedence and scoping, addon variables, multi-step recipes.
16
+ - `lizard skills get core --ref sdk --json`: building with Lizard from code through Lizard SDK (TypeScript, Python): sandboxes inside your own product, per-user workspaces and scoped keys, platform APIs.
21
17
 
22
- 1. Read the user's `package.json`, `Dockerfile`, `requirements.txt`, framework config — confirm what already exists before adding flags.
23
- 2. Don't assume scripts/conventions that aren't visible. On the lizardpack auto-detect path, `Procfile` (`web:` line, Python/Ruby) and `package.json scripts.start` (Node) ARE picked up as the start command; on the synthesized-Dockerfile path (`buildCommand`/`startCommand` set) neither is read. Ports are inferred only from `EXPOSE`, framework defaults, or an explicit `PORT` env.
24
- 3. When in doubt, ask the user or run `lizard <cmd> --help --json`.
18
+ If `$ARGUMENTS` is non-empty, treat it as the user's request and act on it. If empty, ask what they want to do on Lizard.
25
19
 
26
20
  ## Execution rules
27
21
 
28
- 1. Prefer the `lizard` CLI. For anything it does not expose, ask the user: don't call the API directly.
22
+ 1. Prefer the `lizard` CLI. Don't call the Lizard HTTP API yourself; for something the CLI does not expose, ask the user. Using a database, S3 or a deployed app through its own URL and credentials is fine.
29
23
  2. Always pass `--json` on non-interactive calls. The CLI also switches to JSON by itself when stdout isn't a TTY.
30
24
  3. For an unfamiliar command, run `lizard <cmd> --help --json` first: never guess flag shapes. See [Discovery](#discovery).
31
- 4. Resolve context before any change: `lizard status` shows the folder's link and its project. Confirm you are targeting the right project, service or sandbox.
32
- 5. For destructive actions (delete a service or sandbox, drop an addon, overwrite a project-wide secret, restart production), confirm with the user first. The CLI's own prompts only appear on a TTY.
25
+ 4. Resolve context before any change: `lizard status --json` shows whether this exact folder is linked. `linked: false` means commands go to the [default project](#default-project); pass `--project` when it matters. Confirm the project, service or sandbox you target.
26
+ 5. A network error (`fetch failed`, `UND_ERR_SOCKET`, `ECONNRESET`) is not an answer: run the command again. Before repeating a create, check with `list` / `ps` that the first attempt did not land.
27
+ 6. For destructive actions (delete a service, sandbox, volume or project, drop an addon, overwrite a project-wide secret, restart production), confirm with the user first. Without a terminal the CLI never asks: `sandbox rm`, `volume delete`, `keys delete` and `workspace rm` delete at once; `service`, `project`, `secrets` and `domain` deletes and billing changes refuse until you add `-y`. Add `-y` only after the user agrees.
33
28
 
34
29
  ## Deploy command output
35
30
 
36
- For streaming commands (`lizard up` without `--detach`), `--json` produces one JSON event per line: `{ event: "log", line }`, terminating with `{ event: "done" }` / `{ event: "error", message }`; `up` additionally emits a final `{ event: "deployed" | "failed" | "deploying", status, url }` (`url` may be `null`). `lizard logs --json` is **not** a stream: it returns the last 200 lines (override with `--tail N`, max 1000) and exits — do not wait on it expecting more. Need a specific incident? Use `--restart latest` or `--restart <id>`. Only stream logs without `--json` if the user actively wants a live tail.
31
+ `lizard up --json` (without `--detach`) prints one JSON event per line: `{ event: "log", line }` while it builds, then `{ event: "done" }`, or `{ event: "error", message }` and exit `1`. When the service runs, it ends with `{ event: "deployed", status: "running", url }`. No `deployed` event and exit `0` means the build finished but the service is not running: read `lizard logs --restart latest --json` and `lizard up status <appId> --json` (`status`, `deployStatus`). `lizard up --detach --json` prints `{ appId, status: "deploying" }` once. A brand-new URL can answer `404` (a plain nginx page) for up to a minute while its route is set up: wait and retry before you debug the app. `lizard logs --json` is **not** a stream: it returns the last 200 lines (override with `--tail N`, max 1000) and exits — do not wait on it expecting more. Need a specific incident? Use `--restart latest` or `--restart <id>`. Only stream logs without `--json` if the user actively wants a live tail.
37
32
 
38
33
  ## Mental model
39
34
 
40
35
  ```
41
- workspace → project → service (+ managed addons)
36
+ workspace → project → services, sandboxes, managed addons, volumes
42
37
  ```
43
38
 
44
39
  - Workspace — account/org level. User belongs to one or more.
45
40
  - Project — group of related services and sandboxes in one workspace. The cwd gets linked to a project (config at `~/.lizard/config.json`).
46
41
  - Service — a deployable unit. Source is either a git repo (`sourceType=github`) or an uploaded tarball (`sourceType=upload`).
47
- - Managed addons — `postgres`, `redis`, `s3`. Provisioned with `lizard add <type>`; `s3` ships with a private bucket named `default`. See [Managed addons](#managed-addons) for the env vars each type exposes.
48
- - Cross-resource refs — `${{<name>.<KEY>}}` resolves at deploy time against the target's merged env. A ref to a missing target or key resolves to an empty string and does NOT fail the deploy (only circular refs throw). After wiring refs, deploy and check `lizard logs --build --service <svc>`: the build log warns about every reference that came back empty. `lizard secrets list --service <svc> --show` shows what is stored. Refs match the target's current name, so renaming the target breaks them: update them after a rename.
42
+ - Sandbox — a short-lived Linux machine (Firecracker microVM) for running code and agents. See [Sandboxes](#sandboxes).
43
+ - Volume — persistent disk: for sandboxes (mounted at `/workspace`), or one per service (`lizard volume create <name> --service <svc>`, mounted at `/data` by default).
44
+ - Managed addons — `postgres`, `redis`, `s3`. Provisioned with `lizard add <type>`; `s3` ships with a private bucket named `default`. See [Databases and storage](#databases-and-storage) for the env vars each type exposes.
45
+ - Cross-resource refs — `${{<name>.<KEY>}}` resolves at deploy time against the target's merged env. A ref to a missing target or key resolves to an empty string and does NOT fail the deploy (only circular refs throw). After wiring refs, deploy and check `lizard logs --build --service <svc>`: the build log warns about every reference that came back empty. `lizard secrets list --ref --service <svc> --json` lists the references that resolve. Refs match the target's current name, so renaming the target breaks them: update them after a rename.
49
46
 
50
47
  ## Discovery
51
48
 
52
- The CLI has ~30 subcommands. Discover at runtime:
49
+ The CLI has about 37 commands. Discover at runtime:
53
50
 
54
51
  ```
55
52
  lizard --help --json # root + all commands + global flags + exit codes
@@ -57,428 +54,166 @@ lizard <cmd> --help --json # specific command schema
57
54
  lizard <cmd> <sub> --help --json # nested (e.g. `lizard service set --help --json`)
58
55
  ```
59
56
 
60
- Returns `{ cli, version, command: { arguments, options, subcommands }, globalOptions, exitCodes }`.
57
+ Returns `{ cli, version, agents, command: { arguments, options, subcommands }, globalOptions, exitCodes }`.
61
58
 
62
59
  ## Sign-in
63
60
 
64
61
  Every command that needs an account checks the sign-in first. Without one, in JSON mode it fails with exit code `2` and `error.code: "NOT_AUTHENTICATED"`, and `error.body.login` holds what you need to get the user signed in. `lizard login --json` prints the same object at the top level. It never waits and never opens a browser:
65
62
 
66
63
  - `status: "pending"`: `authUrls` holds one link per sign-in method (`github`, `google`), and `authUrl` the one to use when only one applies. Ask the user how they sign in to Lizard and give them that link as a clickable URL. Someone with an account must use the same method as before: another method opens a second, empty account unless both share a verified email. Once they say the browser shows "CLI authorized", run the same command again (`nextCommand` when present); it picks the sign-in up. A link lasts 5 minutes; the next run prints a new one. Follow `instruction`.
67
- - `status: "complete"` (from `lizard login` only): `username`, `email`, `source` (`browser`, `saved`, or `env` for `LIZARD_TOKEN` / `LIZARD_API_KEY`), `account` with the plan's `state` (`trial_available`, `subscription_required`, `trialing`, `active`, `past_due`, `credits`, `enterprise`, `not_owner`, `checkout_unavailable`, `unknown`), `billingUrl`, `payUrl` when a payment failed, and an `instruction` that says what to offer and what not to touch, plus `next` commands.
64
+ - `status: "complete"` (from `lizard login` only): `username`, `email`, `source` (`browser`, `saved`, or `env` for `LIZARD_TOKEN` / `LIZARD_API_KEY`), `account` with the plan's `state` (`trial_available`, `subscription_required`, `trialing`, `active`, `past_due`, `credits`, `enterprise`, `checkout_unavailable`, `unknown`), `billingUrl`, `payUrl` when a payment failed, and an `instruction` that says what to offer and what not to touch, plus `next` commands.
68
65
 
69
66
  Never start a plan (`lizard billing start`) unless the user agrees. `lizard login` is safe to re-run: a signed-in machine skips straight to the plan. In CI nothing can be opened, so set `LIZARD_TOKEN` or `LIZARD_API_KEY`, or save a token on the machine with `printf '%s' "$TOKEN" | lizard login --token-stdin`. `lizard login --token <token>` works too, but the token lands in the process list and the shell history.
70
67
 
71
68
  ## Exit codes
72
69
 
73
- - `0` success — continue
74
- - `1` generic error — inspect message, surface to user
75
- - `2` auth (401/403) — not signed in: give the user the link from `error.body.login` and run the command again once they have signed in (see [Sign-in](#sign-in)). No links in the body (CI, or the platform could not be reached)? Run `lizard login --json`, or ask for `LIZARD_TOKEN`.
76
- - `3` not found (404) — wrong name / resource gone; verify with `lizard project list` / `lizard ps`
77
- - `4` timeout — retry or report
78
- - `5` cancelled by user — stop
70
+ - `0` success — continue.
71
+ - `1` error — read `error.message` and show it to the user. A project, service or volume name that doesn't exist exits `1` (`… not found`); so does a `--wait` that runs out (`status: "timeout"`).
72
+ - `2` not allowed.
73
+ - `error.code: "NOT_AUTHENTICATED"` (HTTP 401): not signed in. Give the user the link from `error.body.login` and run the command again once they have signed in (see [Sign-in](#sign-in)). No links in the body (CI, or the platform could not be reached)? Run `lizard login --json`, or ask for `LIZARD_TOKEN`.
74
+ - HTTP 403: signed in, but this key or account may not do it (a key limited to other projects, not a member of the workspace, owner-only data, an account on hold). Signing in again does not help: show `message`.
75
+ - `LIZARD_TOKEN` / `LIZARD_API_KEY` wins over the saved login; a 401 while one is set means that token is bad.
76
+ - `3` not found on the server (HTTP 404), or an unknown command in `--help --json`. Check names with `lizard project list` / `lizard ps`.
77
+ - `4` network or gateway timeout — retry once, then report.
78
+ - `5` cancelled by user — stop.
79
+
80
+ `lizard sandbox exec`, `lizard ssh … -- <cmd>` and `lizard run` exit with the code of the command they ran (`127`: command not found), so the numbers above do not apply to them.
79
81
 
80
82
  ## Default project
81
83
 
82
- A command that works on a project takes it from `--project`, then from the folder's link, then falls back to the account's **default project**: the oldest live project in the personal workspace, which every account gets at sign-up (`<username>'s Project`). So `lizard sandbox create`, `lizard up`, `lizard logs` and the rest work in a folder that was never linked. They say so on stderr: `Project: <name> (your default)`.
84
+ A command that works on a project takes it from `--project`, then from the folder's link, then falls back to the account's **default project**: the oldest live project in the personal workspace (for a key limited to some projects or workspaces, the oldest one it can reach). So `lizard sandbox create`, `lizard up`, `lizard logs` and the rest work in a folder that was never linked. In a terminal they say so on stderr (`Project: <name> (your default)`); `--json` output does not, except `sandbox create`, whose JSON carries `projectId`, `projectName` and `projectDefaulted: true`. When the project matters, pass `--project`.
83
85
 
84
- - `lizard sandbox create` with no project lets the platform pick the default; its JSON carries `projectId`, `projectName` and `projectDefaulted: true`.
85
- - `lizard up` links the unlinked folder to the default project first, then creates a service named after the folder, so the next `up` from that folder updates the same service. `lizard init --force` relinks it elsewhere.
86
- - No default project (every project deleted): the old `No project linked. Run lizard init or pass --project <id>.` error.
86
+ - `lizard up` links the unlinked folder to the default project first, then creates a service named after `package.json` `name` (else the folder), so the next `up` from that folder updates the same service. The link belongs to that exact folder: `up` from a subfolder creates another service. `lizard init --force --name <project>` relinks the folder, and creates that project when none has the name.
87
+ - `lizard up --project <name>` in an unlinked folder also creates the project when no name matches: check the spelling first.
88
+ - No default project (every project deleted, or the personal workspace on a billing hold): `No project linked…`, and `up` asks for `lizard init --name <project>`. Run `lizard billing --json` to rule out a hold.
87
89
 
88
90
  ## Sandboxes
89
91
 
90
92
  A sandbox is a Linux machine for running code and agents: a Firecracker microVM with its own kernel. It lives 5 minutes by default and is then deleted with its files.
91
93
 
92
- ### Sandbox or deploy?
93
-
94
94
  - **Sandbox:** run code, tests, a build or an agent on demand, for minutes or hours, then delete it or snapshot it. The place for untrusted or AI-generated code.
95
- - **Deploy** (`lizard up`): a service that keeps running and serves traffic at a stable URL, with managed databases next to it. See [Deploying](#deploying).
96
-
97
- ### Quickstart
95
+ - **Deploy** (`lizard up`): a service that keeps running and serves traffic at a stable URL, with managed databases next to it. See [Deploys](#deploys).
98
96
 
99
97
  ```
100
98
  lizard sandbox create --json # base template, medium size, 5-minute lifetime; the JSON has the id
101
99
  lizard sandbox exec <id> -- python3 -c 'print(2 ** 10)' # exits with the command's exit code
100
+ lizard sandbox exec <id> -- sh -c 'cd /workspace && ls' # a shell line goes inside sh -c
102
101
  lizard sandbox files put <id> ./app.py /workspace/app.py
103
102
  lizard sandbox exec <id> -- python3 /workspace/app.py
104
103
  lizard sandbox expose <id> 8000 --json # an HTTPS URL for a port in the sandbox
105
104
  lizard sandbox timeout <id> 3600000 # keep it for an hour from now
106
- lizard sandbox rm <id> -y # delete it now
105
+ lizard sandbox rm <id> # delete it now: no prompt without a terminal
107
106
  ```
108
107
 
109
108
  `lizard sandbox list --json` shows the project's sandboxes; `--all` shows every one you have.
110
109
 
111
- ### Templates
112
-
113
- | `-t` | What is inside |
114
- |---|---|
115
- | `base` (default) | Python 3.11 on Alpine Linux, nothing else |
116
- | `interpreter` | A Python code-interpreter environment |
117
- | `codex` | Coding agents: see [Coding agents](#coding-agents) |
118
- | `desktop` | A graphical desktop with Chromium: see [Desktop](#desktop-computer-use) |
119
-
120
- The platform validates the name; `lizard sandbox create --help --json` lists the templates this CLI knows.
121
-
122
- ### Running commands
123
-
124
- `lizard sandbox exec <id> -- <cmd> [args...]` runs the command through `sh -c` in the sandbox, streams stdout and stderr, and exits with the command's exit code. In JSON mode it prints the raw output without the `$ <cmd>` header. Each call is a new process, so `cd` and exported variables don't carry over: chain steps in one call, e.g. `lizard sandbox exec <id> -- sh -c 'cd /workspace && npm test'`, and keep results in files.
110
+ - **Templates:** `base` (default, Python 3.11 on Alpine Linux), `interpreter` (Python with numpy, pandas, matplotlib and scipy, plus Node 22), `codex`, `claude`, `opencode`, `pi`, `prime` (one coding agent each), `desktop` (graphical desktop with Chromium).
111
+ - **Sizes:** `-s small|medium|large`, $0.0162 / $0.0324 (default) / $0.0648 per hour, billed per second while it runs. A paused sandbox costs nothing.
112
+ - **Lifetime:** 5 minutes by default, then the sandbox is deleted with its files. `--timeout 0` at create means no limit; `lizard sandbox timeout <id> <ms>` sets a new lifetime from now. Commands do not reset it.
113
+ - **Commands:** `exec <id> -- <cmd> [args…]` passes each argument as one word, so a whole shell line in one quoted string fails with `not found`: wrap it, `-- sh -c 'cd /workspace && npm test'`. Each call is a new process that starts in `/root`, gives up after 150 seconds (exit `4`, `EXEC_TIMEOUT`; the command may still be running) and keeps at most 512 KB of output. Run longer jobs in the background and poll a log: `-- sh -c 'cd /workspace && nohup npm run build > build.log 2>&1 &'`, then `-- tail -n 50 /workspace/build.log`. `ssh` needs a real terminal: from an agent, use `exec`.
114
+ - **Ports:** `lizard sandbox expose <id> <port> --json` gives `url` with `?lizard_token=…`, which works in a browser: it swaps the token for a cookie and redirects. `curl -L` without a cookie jar gets `401`; from a script send the token as a header instead: `curl -H "X-Lizard-Access-Token: <accessToken>" https://<hostname>/`. The token opens every port of that sandbox: treat the URL like a password. Start the server in the background (as above) so `exec` returns.
115
+ - **Keeping work:** files outlive a sandbox on a volume (`--volume <name>`) or in a snapshot; `pause` keeps memory and processes and stops billing.
116
+ - **Agents in a sandbox:** `lizard sandbox create -t codex --timeout 3600000`, then `lizard sandbox prompt <id> "task" --json`. Give it a real lifetime: at 5 minutes the sandbox and the agent are deleted mid-task.
125
117
 
126
- ### Files
118
+ Everything else (files, coding agents, desktop, volumes, snapshots): `lizard skills get core --ref sandboxes --json`. From your own code: `--ref sdk`.
127
119
 
128
- ```
129
- lizard sandbox files ls <id> [path] # default /
130
- lizard sandbox files cat <id> <path>
131
- lizard sandbox files put <id> <local> <remote>
132
- lizard sandbox files get <id> <remote> <local>
133
- lizard sandbox files rm <id> <path>
134
- ```
135
-
136
- `/workspace` is the working directory: `ssh` starts there and volumes mount there. `files put` and `files get` keep binary files intact.
137
-
138
- ### Ports
139
-
140
- `lizard sandbox expose <id> <port>` publishes a port inside the sandbox on an HTTPS URL; `lizard sandbox unexpose <id> <port>` removes it. The URL carries the sandbox's access token, so treat it like a password. The platform keeps one port for itself and refuses to expose it.
141
-
142
- ### Lifetime
143
-
144
- `lizard sandbox create` sends a five-minute lifetime (`300000` milliseconds) by default. Pass `--timeout 0` to create a sandbox without expiration, or pass an integer up to `2147483647` milliseconds. This is a lifetime, not an idle timer: commands do not reset it. `lizard sandbox timeout <id> <ms>` sets a new lifetime of at least `1000` milliseconds. Pausing a sandbox freezes its remaining lifetime; resuming continues from it (see [private snapshots and pause/resume](#snapshots-and-pauseresume)). The CLI has no sandbox fork command. Use volumes for persistent files.
120
+ ## Deploys
145
121
 
146
- ### Size and pricing
122
+ When the user wants to deploy or set up something new, work out the right action from the folder before running anything:
147
123
 
148
- `lizard sandbox create -s/--size small|medium|large` picks the machine; the default is `medium`. There are exactly three sizes:
124
+ 1. `lizard status --json` in the folder, then `lizard ps --json` for what the project already runs.
125
+ 2. The folder is linked to a service (`service` in the status), or the project already runs this code → update that service: `lizard up --service <svc>` for an upload, `lizard redeploy --service <svc>` or a push for git. Don't add a second one.
126
+ 3. Linked to a project, no service for this code → `lizard add -r owner/repo -n <svc>` (git) or `lizard up` (upload). Do not create a new project unless the user explicitly says so.
127
+ 4. A subfolder of a linked folder (a link covers one exact folder) → a monorepo sub-app. In the subfolder run `lizard link --project <parent's project>`, then `lizard up` from there; for git, `lizard add -r owner/repo -n <svc> --no-deploy`, `lizard service set <svc> --set rootDirectory=<subpath>`, `lizard redeploy --service <svc>`.
128
+ 5. Not linked → `lizard project list --json` for a project matching the folder or repo name. Match → `lizard link --project <name>`. No match → `lizard up` deploys into the [default project](#default-project); run `lizard init --name <name>` first only when the user wants a separate project.
149
129
 
150
- | Size | vCPU | RAM | Disk | Price |
151
- |---|---|---|---|---|
152
- | `small` | 2 | 4 GiB | 5 GB | $0.0162/h |
153
- | `medium` (default) | 4 | 8 GiB | 20 GB | $0.0324/h |
154
- | `large` | 8 | 16 GiB | 50 GB | $0.0648/h |
130
+ **Names.** A service's name is its key. `lizard add -r` or `add -s` with a name that exists overwrites that service and redeploys it; `lizard up` from an unlinked folder deploys into the service named like `package.json` `name` (else the folder), so two folders whose `package.json` names match land on one service. Name each service yourself: `lizard add -r owner/repo -n <svc>`, or `lizard up --service <svc>`, which deploys to that service and creates it when no service has the name. Use lowercase letters, digits and dashes.
155
131
 
156
- Prices are per hour, billed per second while the sandbox runs, flat by size: measured CPU/RAM is not charged, disk is included, and egress is included up to 2 TB per account over a rolling 30 days (past that, the account's sandboxes lose outbound network until the total drops). A paused sandbox costs nothing. Attached volumes bill separately. `--size` cannot be combined with `--snapshot`: a snapshot restores at the size it was captured at (see [private snapshots](#snapshots-and-pauseresume)).
132
+ **Git or upload.** Check `git remote get-url origin` first. A GitHub remote → `lizard add -r owner/repo -n <svc> --json`. It tracks `main`; for another branch add `--no-deploy`, then `lizard service set <svc> --set branch=<b>` and `lizard redeploy --service <svc>`. Pushes redeploy only when the Lizard GitHub App covers the repo (`lizard git status --json`). `Repository not found` (exit `3`) means Lizard cannot read the repo: `lizard git connect --json` returns an `installUrl` for the user to open. No remote, not GitHub, or not a git repo → `lizard up --json`. `lizard up` always switches a service to `sourceType=upload`: don't use it on a git-backed service.
157
133
 
158
- Sandboxes run as Firecracker microVMs, each with its own kernel. A sandbox that attaches an existing pod-backed volume, or restores an older pod snapshot, runs as a Kubernetes pod instead.
134
+ **Builds** run on the platform's build nodes; no local Docker needed. lizardpack detects Hugo, Go, Node, Python, Rust, Ruby, PHP, Java and static sites (first match wins). A root `Dockerfile` is used as is (on git builds, unless it only copies prebuilt output such as `COPY dist ...` with no install or build `RUN`). Read a failed build with `lizard logs --build --service <svc> --json`.
159
135
 
160
- ### Interactive shell
136
+ **Ports.** The app must listen on `$PORT`. `lizard up` fixes a new service's port from `--port`, else the folder's Dockerfile `EXPOSE`, else 3000; pass `--port` when the app listens elsewhere (Python servers often use 8000 or 8080). A wrong port shows as `the new version had not reported ready` and a crashed first deploy: make the app read `$PORT`, or run `lizard port <n> --service <svc>` and then `lizard redeploy --service <svc>`.
161
137
 
162
- `lizard sandbox ssh <id>` (alias `shell`) opens an interactive shell in any running sandbox,
163
- in `/workspace` as root. It uses your Lizard login, so there are no SSH keys to set up; a
164
- scoped API key only reaches sandboxes in its scope. It needs a real terminal — from a script
165
- or an agent, use `lizard sandbox exec <id> -- <cmd>` instead, which returns output and an
166
- exit code.
138
+ **Rebuilds.** On a running service, `service set` on a build field (`repoUrl`, `branch`, `sourceType`, `buildCommand`, `dockerfilePath`, `rootDirectory`) and setting a `VITE_*` or `NEXT_PUBLIC_*` variable rebuild on their own: don't chain `lizard redeploy`. A service that never came up, crashed or stopped does not rebuild by itself: run `lizard redeploy --service <svc> --wait --json`. Runtime-only fields (`startCommand`, `preDeployCommand`, `containerPort`) always need `lizard redeploy`.
167
139
 
168
- ### Coding agents
140
+ **A service that never came up has no URL.** After you fix why its first deploy failed (a secret, a port, the code), run `lizard redeploy --service <svc> --wait --json`: the domain is issued by the first deploy that succeeds, not by a restart. Then open the URL to confirm.
169
141
 
170
- Run a coding agent inside a sandbox (use `-t codex`) on the user's own credentials:
142
+ **Workers** without an HTTP listener: `containerPort=0` (`lizard up --port 0`, or `lizard port 0 --service <svc>` and then `lizard redeploy`). A worker has no readiness check, and each deploy stops the old copy before starting the new one.
171
143
 
172
- - `lizard agents` — your account's credentials and which agents can run. Connect once (here or in the
173
- dashboard: Account settings › Agents); every sandbox and every agent that accepts it uses it.
174
- - `lizard agents login chatgpt` — ChatGPT subscription via device code.
175
- `lizard agents add claude` (token from `claude setup-token`), `add anthropic|openai|openrouter|llmgateway|deepseek` for API keys.
176
- - `lizard agents use <agent> --credential … --model … --effort …` — per-agent defaults.
177
- - `lizard sandbox prompt <id> --agent codex|claude|pi|opencode|prime "task"` — streams tool calls and
178
- the answer; `-c <conversation>` continues one; `--no-wait` returns at once.
179
- - `lizard sandbox conversations <id>`, `lizard sandbox interrupt <id> [-c <conversation>]`.
144
+ **Files that must survive** restarts and redeploys (SQLite, uploads) go on a volume: `lizard volume create data --service <svc> [--mount-path /data] [--size <gb>]`. One volume per service; the service restarts with it mounted. Without one, the container's disk is wiped on every deploy. Use Postgres instead only when the user wants a database.
180
145
 
181
- Codex and Claude Code use one credential at a time; pi, OpenCode and Prime use every connected one
182
- they accept. Sandboxes never receive a subscription's refresh token, only a short-lived access token.
146
+ Details (build order, service fields, worker mode, recipes): `lizard skills get core --ref deploys --json`.
183
147
 
184
- Use `sandbox exec` to read a log file written by your process.
185
-
186
- ### Desktop (computer use)
187
-
188
- The `desktop` template is a sandbox with a graphical desktop (Openbox + Chromium) streamed to a browser over WebRTC (H.264), falling back to noVNC where WebRTC cannot connect. The viewer's side panel has fullscreen with keyboard lock, send-keys, type-clipboard and a switch to VNC.
148
+ ## Databases and storage
189
149
 
190
150
  ```
191
- lizard sandbox create -t desktop # only this template has a desktop
192
- lizard sandbox desktop <id> --open # start it (idempotent) and open it in the browser
193
- lizard sandbox desktop <id> --resolution 1920x1080 # 640-3840 x 480-2160
194
- lizard sandbox desktop <id> --view-only # watch-only link
195
- lizard sandbox desktop <id> --status # URLs/state without starting
196
- lizard sandbox desktop <id> --stop # stop and unpublish
197
- lizard sandbox vnc <id> # native VNC app: tunnels localhost:5900 to the desktop
198
- lizard sandbox vnc <id> --open # macOS: opens Screen Sharing; --view-only, --port N
151
+ lizard add postgres redis s3 # all three in one command
152
+ lizard add postgres # or one at a time
199
153
  ```
200
154
 
201
- The printed URLs embed secrets (the desktop's token and its password): the main URL grants control of the sandbox, so treat it like a password and don't paste it into logs, issues or chat; share `--view-only` for watching. On any other template the command fails with `DESKTOP_NOT_SUPPORTED`.
202
-
203
- Agents drive the desktop from inside the sandbox once it is started (`DISPLAY=:1` is preset in the image):
204
-
205
- ```
206
- lizard sandbox exec <id> -- xdotool mousemove 400 300 click 1
207
- lizard sandbox exec <id> -- xdotool type 'hello'
208
- lizard sandbox exec <id> -- lizard-desktop screenshot | grep -E '^[A-Za-z0-9+/=]+$' | base64 -d > screen.png
209
- lizard sandbox exec <id> -- lizard-desktop screenshot --path # writes the PNG to a file in the sandbox, prints its path
210
- ```
155
+ - The first addon of each type is named after it (`postgres`, `redis`, `s3`); later ones get names like `postgres-autumn-bear`. References use the real name: `${{postgres.DATABASE_PRIVATE_URL}}`.
156
+ - Connect a service explicitly: `lizard secrets set DATABASE_URL='${{postgres.DATABASE_PRIVATE_URL}}' --service <svc>` (Redis: `REDIS_URL='${{redis.REDIS_PRIVATE_URL}}'`). Every running Postgres and Redis also reaches every service as `DATABASE_URL` / `REDIS_URL` (public URL), but only from that service's next deploy, and with two of a type either may win.
157
+ - Private URLs resolve only in the addon's region: `lizard add` defaults to `us-east-1`, so pass the service's `--region` if it runs elsewhere.
158
+ - A reference to a missing addon or key becomes an empty string and does not fail the deploy; renaming an addon breaks its references.
159
+ - `lizard ps --json` and `lizard add <db> --json` print database passwords and S3 keys in plain text: don't repeat them to the user.
160
+ - S3 comes with a private bucket named `default`, up to 16 GiB per S3 addon. `lizard s3 upload <file>`, `lizard s3 list` and `lizard s3 url <key> --expires <seconds>` (a temporary download link that needs no credentials) work without an AWS SDK; AWS SDKs need `forcePathStyle: true`. Making a bucket public is done in the dashboard.
161
+ - Redis has no persistence settings: keep data you cannot lose in Postgres.
211
162
 
212
- `lizard-desktop screenshot` prints the PNG as base64 wrapped at 76 columns; the `grep` keeps only those lines (it drops the `$ <cmd>` header `sandbox exec` prints). Use `--path` to save the PNG, then download that path with `lizard sandbox files get <id> <remote-path> screen.png`. `files put` and `files get` preserve binary files.
163
+ Every variable each addon exposes: `lizard skills get core --ref deploys --json`.
213
164
 
214
- ### Volumes
215
-
216
- Use `lizard volume create <name> --size <gb> --project <project>` for persistent sandbox storage. Names are unique within a project; use lowercase letters, digits and dashes (up to 64 characters). Pass the name to `lizard sandbox create --volume <name>`. New volumes default to 5 GB and allow 1–50 GB. The CLI reads the server's current size limits before creating a volume; a server config override may change the maximum. Existing larger volumes keep their size. Storage charges cover used bytes, not the whole allocation.
217
-
218
- Persistent volumes for sandboxes. A volume's name is its key within the project; every command takes a name or ID.
165
+ ## Secrets and config
219
166
 
220
167
  ```
221
- lizard volume create <name> [--size <gb>] [--region <code>] # default 5 GB; region must match the sandbox's
222
- lizard volume list --json # name, sizeGb, status, attachedTo
223
- lizard volume resize <name> --size <gb> # grow in place
224
- lizard volume rm <name> [-y] # refused while attached; delete the sandbox first
225
- lizard sandbox create --volume <name> # attach at create time
168
+ lizard secrets list [--service <svc>] [--global] --json # names only
169
+ lizard secrets set KEY=v [K2=v2 …] --service <svc> # service scope
170
+ lizard secrets set KEY=v --global # every service in the project
171
+ lizard secrets import --service <svc> < .env # values keep their quotes: strip them first
172
+ lizard secrets list --ref --service <svc> --json # the ${{…}} references that resolve
226
173
  ```
227
174
 
228
- `volume resize` is online: an attached sandbox keeps running, and its `/workspace` shows the new size right away. A volume grown while detached gets the new size when a sandbox next mounts it. Firecracker volumes (the default) only grow; asking for less fails with `volume_shrink_unsupported`. A Kubernetes volume can also shrink if at least 10% of the new size stays free (`volume_too_full_to_shrink` otherwise). The size limit (`volume_too_large`) and the workspace's volume quota apply as at create. `The volume is not ready` (HTTP 409) right after create means retry in a few seconds.
229
-
230
- ### Snapshots and pause/resume
231
-
232
- - `lizard sandbox snapshot <sandbox-id> --name my-app` saves files and running memory privately. The snapshot is yours: by default it restores into any of your projects and workspaces; `--scope workspace` keeps it to the sandbox's workspace and `--scope project` to its project, and `sandbox snapshot-scope <id> <user|workspace|project>` changes that later. A Firecracker snapshot is ready as soon as the capture ends. A snapshot of a pod sandbox keeps **five** independent copies warm; `--warm 1..10` changes that count. It waits until ready; `--no-wait` returns the queued capture.
233
- - `lizard sandbox restore <snapshot-id> [--project <project>]` starts a new sandbox from a warm copy, in the snapshot's project unless `--project` names another. `--volume <volume>` mounts a volume at `/workspace` in the restored sandbox (Firecracker snapshots of a sandbox that had no volume attached; the volume must be in the snapshot's region). `sandbox create --snapshot <id> --project <project> [--volume <volume>]` also works. Restores stay in the snapshot's region. A Firecracker snapshot restores at its source's size and bills flat at that size; a pod snapshot runs on the machine it was captured on and bills by measured usage. `--size` with `--snapshot` is an error.
234
- - `lizard sandbox snapshots --project <project>` lists a project's snapshots; `--mine` lists every snapshot you took. `sandbox snapshot-warm <id> <count>` changes capacity. `sandbox snapshot-rm <id>` deletes the snapshot and free copies, preserving already running sandboxes.
235
- - `lizard sandbox pause <id>` snapshots and stops a Firecracker VM (a pod sandbox is checkpointed with CRIU), stops billing, and freezes its remaining lifetime. `sandbox resume <id>` restores the same sandbox ID, files, processes, memory, published ports, and remaining lifetime. Paused sandboxes keep no warm copies; resume includes a cold restore. Both support `--no-wait`.
236
- - Disconnect clients and terminals and stop workspace writes before capturing or pausing. On a pod sandbox, active TCP connections and unsupported CRIU process state cause a clear failure. To reuse one snapshot across sandboxes that each keep their own data, snapshot a sandbox with no volume attached and pass `--volume` on each restore; a snapshot taken with a volume attached cannot take a different one.
237
-
238
- ### From code: Lizard SDK
239
-
240
- For an app or script that creates sandboxes itself, use Lizard SDK instead of calling the CLI:
241
-
242
- | SDK | Install | Requires |
243
- |---|---|---|
244
- | TypeScript | `npm install @lizard-build/sdk` | Node.js 18+ |
245
- | Python | `pip install lizard-sdk` | Python 3.10+ |
175
+ - List before you set, so you don't duplicate or shadow a key.
176
+ - Keep secrets in service scope. `--global` is for values that are not secret (`LOG_LEVEL`, `NODE_ENV`, feature flags).
177
+ - Precedence, last wins: addon env < project secrets < project env < app env < app secrets < platform vars (`PORT`, `LIZARD_*`, which cannot be overridden).
178
+ - Setting a secret restarts the service with it, no rebuild; `${{…}}` references are read again then and on every deploy, not on `lizard restart`. Never print a service's full environment.
179
+ - For one service to call another, use `https://${{api.LIZARD_PUBLIC_DOMAIN}}` or the private `http://${{api.LIZARD_PRIVATE_DOMAIN}}:<port>`, not the service name.
246
180
 
247
- The SDK reads `LIZARD_API_KEY`. Create one with `lizard keys create <name> --project <project> --json`: a key limited to one project is safest when the code runs somewhere else.
181
+ Scoping rules and build-time variables in detail: `lizard skills get core --ref deploys --json`.
248
182
 
249
- ```ts
250
- import { Lizard } from '@lizard-build/sdk'
183
+ ## Domains
251
184
 
252
- const lizard = new Lizard({ project: 'my-project' })
253
- const sandbox = await lizard.create('base', { timeoutMs: 300_000 })
254
- try {
255
- await sandbox.fs.write('/tmp/hello.txt', 'Hello from Lizard!')
256
- const result = await sandbox.process.exec('cat /tmp/hello.txt')
257
- if (result.exitCode !== 0) throw new Error(result.stderr)
258
- console.log(result.stdout)
259
- } finally {
260
- await sandbox.kill()
261
- }
262
185
  ```
263
-
264
- ```python
265
- from lizard import Lizard
266
-
267
- lizard = Lizard(project="my-project")
268
- with lizard.create("base", timeout_ms=300_000) as sandbox:
269
- sandbox.fs.write("/tmp/hello.txt", "Hello from Lizard!")
270
- result = sandbox.process.exec_("cat /tmp/hello.txt")
271
- if result.exit_code != 0:
272
- raise RuntimeError(result.stderr)
273
- print(result.stdout)
186
+ lizard domain --service <svc> --json # show the service's onlizard.com domain, or generate one
187
+ lizard domain list --service <svc> --json # its domains, and custom ones waiting for DNS
188
+ lizard domain example.com --service <svc> --json # attach; the hostname is positional, there is no `add`
189
+ lizard domain verify example.com --service <svc> # once the DNS records have propagated
190
+ lizard domain example.com --service <svc> --force # move it here from another of your services
274
191
  ```
275
192
 
276
- Guides: [TypeScript](https://github.com/lizard-build/lizard-sdk/blob/main/packages/js/README.md), [Python](https://github.com/lizard-build/lizard-sdk/blob/main/packages/python/README.md), [sandbox reference](https://github.com/lizard-build/lizard-sdk/blob/main/docs/sandboxes.md).
277
-
278
- ## Setup decision flow
279
-
280
- When the user wants to deploy or set up something new, work out the right action from cwd context before running anything:
281
-
282
- 1. `lizard status --json` in cwd.
283
- 2. Linked to a project? → add a service in that project: `lizard add -r owner/repo` (git source) or `lizard add -s <name>` (empty). Do not create a new project unless the user explicitly says so.
284
- 3. Not linked but parent dir is linked? → likely a monorepo sub-app. Add a service in the parent's project and set `rootDirectory` to the cwd subpath via `service set`.
285
- 4. Neither linked? → check `lizard project list --json` for one matching the directory or repo name. Match → `lizard link --project <name> [--workspace <ws>]` (pass `--workspace` to disambiguate same-named projects across workspaces). No match → `lizard up` deploys into the [default project](#default-project); run `lizard init --name <name>` first only when the user wants a separate project.
286
-
287
- Naming heuristic: app-style names (`my-api`, `worker`, `flappy-bird`) are service names. Use the repo or directory name for the project.
193
+ Give the user `dnsRecords` from the attach JSON: a TXT record `_lizard-verify.<apex>` and a CNAME for the host. Then run `verify`; it checks only the TXT, and nothing verifies on its own. No TXT in the output means the apex is already proven: only the CNAME is needed. An apex host (`example.com`) needs a DNS provider that can put a CNAME at `@`.
288
194
 
289
- ## Platform builder
195
+ ## Logs and operations
290
196
 
291
- Builds run on the platform's build nodes (no local Docker needed). When a build fails, read logs with `lizard logs --build`.
292
-
293
- ### Build decision order
294
-
295
- 1. Repo Dockerfile (verbatim) — if `dockerfilePath` is set on the service, the platform builds that Dockerfile from the repo unchanged and ignores `buildCommand`/`startCommand` for the image. A set `startCommand` still replaces the image's CMD at run time.
296
- 2. Synthesized Dockerfile — if `buildCommand` and/or `startCommand` are set on the service (or passed via `lizard up`), the platform generates a Dockerfile from those commands. No lizardpack invocation.
297
- 3. lizardpack auto-detect — clone, run `lizardpack`. If a repo `Dockerfile` exists AND has a real build step (a `RUN <pkg-manager>` line, not just `COPY dist/`), it's used verbatim; otherwise lizardpack generates a multi-stage one. Supported: Go, Node, Python, Rust, Ruby, PHP, Java, static — first match in that order.
298
-
299
- ### What triggers a rebuild
300
-
301
- - `git push` to the tracked branch → auto-rebuild via GitHub webhook.
302
- - `lizard redeploy` / `lizard up` → explicit rebuild.
303
- - Setting a `VITE_*` or `NEXT_PUBLIC_*` variable → the platform redeploys the running services that receive it, so the build bakes it in. Do NOT chain a `lizard redeploy`. See [Applying an env change](#applying-an-env-change) for which builds receive them.
304
- - `service set` for build-affecting fields (`repoUrl`, `branch`, `sourceType`, `buildCommand`, `dockerfilePath`, `rootDirectory`) → auto-rebuilds running services. Do NOT chain a `lizard redeploy` after it — that queues a second, redundant build.
305
- - `service set` for runtime-only fields (`startCommand`, `preDeployCommand`, `containerPort`, `watchPatterns`) → no auto-rebuild. Follow with `lizard redeploy` to apply.
306
- - All other env vars / secrets → applied without a rebuild; the service restarts to pick them up.
307
-
308
- ## Deploying
309
-
310
- First question for a new service: upload vs git repo. **Always check for a git remote before reaching for `up`** — the CLI never auto-detects one, so you must do it yourself:
311
-
312
- ```
313
- git remote get-url origin
314
197
  ```
315
-
316
- - **Exit 0 with a GitHub URL → use the git-source path below**, not `up`. Parse `owner/repo` from the URL and run `lizard add -r owner/repo`. This is strongly preferred: pushes auto-redeploy via webhook, and there's no re-upload on every change.
317
- - **No remote / non-GitHub / not a git repo → use tarball upload (`lizard up`).** This is the fallback for quick local iteration or no-remote situations, not the default.
318
-
319
- `lizard up` always works regardless of git state, which makes it the path of least resistance — resist it when a GitHub remote exists. Only fall back to upload when the remote check actually fails. (For a private repo whose remote exists but isn't yet accessible to Lizard, run `lizard git connect` to install the GitHub App, then use the git-source path — don't silently downgrade to a tarball.)
320
-
321
- ### Git-source deploy (preferred when there's a remote)
322
-
323
- ```
324
- # One-shot for a new service from GitHub:
325
- lizard add -r owner/repo --json
326
-
327
- # Existing service: switch source to git or update branch:
328
- lizard service set <svc> \
329
- --set sourceType=github \
330
- --set repoUrl=https://github.com/owner/repo \
331
- --set branch=main \
332
- --json
333
- lizard redeploy --service <svc>
334
- ```
335
-
336
- When `repoUrl` is set, pushes to the matching branch auto-redeploy via the GitHub webhook. If the service has a `rootDirectory` (monorepo subpath) or watch patterns, only matching changes trigger redeploys.
337
-
338
- Useful `service set` fields (discover full list with `lizard service set --help --json`):
339
-
340
- - `sourceType` = `github | upload`
341
- - `repoUrl`, `branch`, `rootDirectory`
342
- - `dockerfilePath` — use a specific repo Dockerfile, bypasses lizardpack auto-detect
343
- - `buildCommand`
344
- - `startCommand`, `preDeployCommand` — `preDeployCommand` runs before `startCommand` at the start of every container (each replica, each restart), and only when `startCommand` is also set. Make it safe to run many times and at the same time.
345
- - `watchPatterns` — string array, comma-separated or JSON
346
- - `containerPort` — TCP port the app listens on (defaults to 3000). Set to `0` for [worker mode](#worker-mode).
347
- - `name` — rename a service (lowercase a-z, digits, hyphens; 1–40 chars). Goes through `config:apply`; the legacy `PATCH /api/apps/:id` returns 410. Refs to the old name (`${{old.KEY}}`) stop resolving: update them.
348
-
349
- Field names are flat and match the wire schema 1:1 (and `service show` output). No `build.*` / `deploy.*` / `source.*` grouping exists in the API, DB, or node-agent.
350
-
351
- `service set` uses optimistic concurrency via `configRevision`. On 409, re-read with `lizard service show`, reconcile, retry; `--force` overrides.
352
-
353
- ### Tarball upload (no git remote, or quick local iteration)
354
-
355
- ```
356
- lizard up --json
357
- ```
358
-
359
- - Uploads cwd as a tarball (respects `.gitignore`), forces `sourceType=upload`.
360
- - Streams build logs over SSE; emits a final `{ event: "deployed", url }` on success (`{ event: "failed" }` on failure; `url` may be `null`).
361
- - Flags: `--project`, `--service`, `--region`, `--build-command`, `--start-command`, `--pre-deploy-command`, `--port`, `--detach`, `--ci`.
362
- - If cwd isn't linked and no `--project` is given, it links the folder to the [default project](#default-project) and deploys there as a new service. `--project <name>` uses that project, or creates it if none matches. It never creates a project from the folder name on its own.
363
- - `lizard up` always switches the service to `sourceType=upload`. Do not use it to update a git-backed service — use `lizard redeploy` or push to the remote.
364
-
365
- ## Worker mode
366
-
367
- For services that don't expose an HTTP listener (background workers, reconcilers, queue consumers, cron-style polling loops), set `containerPort=0`. The platform then:
368
-
369
- - Skips `PORT` env injection (the worker doesn't bind anywhere).
370
- - Skips the port reachability check (no `app port X unreachable` log spam, no false-positive "unhealthy" status).
371
- - Skips `EXPOSE` in the synthesized Dockerfile.
372
- - Skips the LB route registration on the node — nothing is served. (A generated `.onlizard.com` domain may still appear on the service; it won't respond.)
373
-
374
- Set it one of three ways:
375
-
376
- ```
377
- lizard up --port 0 # new upload-source worker
378
- lizard port 0 [--service <svc>] # flip existing service to worker mode
379
- lizard service set <svc> --set containerPort=0 # same, via the config:apply path
380
- ```
381
-
382
- `lizard port` with no argument prints the current port (or `worker mode` when 0). Worker mode is a hard switch — re-deploys are needed for the port change to take effect.
383
-
384
- Don't use worker mode for a regular HTTP service that just happens to be slow to start — worker mode hides "the listener never came up" bugs because there's nothing to check.
385
-
386
- ## Secrets
387
-
388
- Two scopes exist. No workspace-level globals.
389
-
390
- - Project ("global"): `lizard secrets set KEY=v [K2=v2 …] --global` → project scope (wire: `secrets.shared`)
391
- - Service (default): `lizard secrets set KEY=v [K2=v2 …] [--service <svc>]` → service scope (wire: `secrets.services[<svc>]`)
392
-
393
- `set` is variadic. Companion subcommands: `lizard secrets list|delete K1 K2|import` (import reads dotenv from stdin). When the linked service in cwd is set, plain `lizard secrets set KEY=v` writes to that service. Pass `--global` to escape to project scope.
394
-
395
- ### Precedence (last writer wins)
396
-
397
- ```
398
- addon-issued env < project secrets < project env < app env < app secrets < platform vars
399
- ```
400
-
401
- App secrets override project secrets. Platform vars (`LIZARD_SERVICE_NAME`, `LIZARD_PROJECT_ID`, `PORT`, `LIZARD_PUBLIC_DOMAIN`, `LIZARD_PRIVATE_DOMAIN`) are last and cannot be shadowed. Addon-issued env is every running Postgres and Redis addon's `DATABASE_URL` / `REDIS_URL`, injected into every service in the project.
402
-
403
- ### Secret scoping
404
-
405
- Default to service-scope. `--global` puts the value into `process.env` of every service in the project — including ones that don't need it.
406
-
407
- Rules:
408
-
409
- - Check first: `lizard secrets list` (+ `--global`) before set/update — avoid creating a duplicate or shadowing an existing key (service scope wins over global; see [Precedence](#precedence-last-writer-wins)).
410
- - Default — service-scope: `lizard secrets set KEY=v --service <svc>` per consumer. For addon DSNs, prefer a private reference when the consumer can reach it: `lizard secrets set DATABASE_URL='${{postgres.DATABASE_PRIVATE_URL}}' --service <svc>` (no separate `env` command — refs are interpolated at deploy time wherever they appear). A ref reads the addon's current value on each deploy of the consumer; there is no automatic credential rotation.
411
- - `--global` only for non-secrets and provably-public values: `LOG_LEVEL`, `NODE_ENV`, feature flags, frontend `SENTRY_DSN`. If unsure whether a value is a secret, treat it as one. A compromised service reads its own env; broader scope = more credentials exposed for no reason.
412
-
413
- ### Applying an env change
414
-
415
- - Runtime vars/secrets apply without rebuilding the image. The application process restarts to read its new environment; do not promise uninterrupted requests.
416
- - `VITE_*` / `NEXT_PUBLIC_*` (build-time baked) → setting one redeploys the running services that receive it; a plain restart won't pick them up. Only a Dockerfile the platform generates bakes them in: lizardpack on a GitHub service, or a service with `buildCommand`/`startCommand`. A `dockerfilePath` build, or a `lizard up` upload without those overrides, gets no Lizard variables at build time, and no build gets build args except `COMMIT_SHA`. For public values there, commit them in `.env.production`, set a build command, or write them into the Dockerfile.
417
- - Verify a non-secret value or the application's behavior. Do not print the full environment: it can expose credentials.
418
-
419
- ## Managed addons
420
-
421
- Provision with `lizard add <type>`. Each addon exposes a fixed env-var set; reference by name from a consumer service via `${{<addon-name>.KEY}}`. The first addon of a given type gets the bare type as its name (so use `postgres` in the reference for the first Managed Postgres instance); subsequent ones get `{type}-{adjective}-{noun}` like `postgres-autumn-bear`. There's no type-alias fallback — a ref must use the addon's actual name. Refs match the addon's current name, so renaming it breaks existing refs: update them after a rename. Every running Postgres and Redis addon's `DATABASE_URL` / `REDIS_URL` also reaches every service in the project at the lowest precedence; a value set on the consumer overrides it.
422
-
423
- - `postgres` — two connection URLs: prefer `DATABASE_PRIVATE_URL` when available and reachable within the same project and region (no ingress/egress charges); use `DATABASE_URL` for public access, such as local clients or external CI (network charges may apply). Also exposes `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`, `POSTGRES_USER`, `POSTGRES_DB`, `POSTGRES_PASSWORD`.
424
- - `redis` — `REDIS_URL`; services in the same project should use `REDIS_PRIVATE_URL`. No eviction policy and no append-only file are configured: if it runs out of memory, the instance restarts and recent writes can be lost. Keep data you cannot lose in Postgres.
425
- - `s3` — `S3_ENDPOINT`, `S3_DEFAULT_BUCKET`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_REGION`. Auto-creates a private bucket named `default`, and each account can store up to 16 GiB across its addons. Make a bucket public from the dashboard's S3 browser; objects in a public bucket are served without auth two ways: the gateway URL the dashboard shows, `https://s3-<region>.onlizard.com/<addonId>/<bucket>/<key>`, or the platform proxy `<dashboard-host>/api/s3/<addonId>/public/<bucket>/<key>` (the host `lizard open` launches; long-lived immutable cache headers). For AWS SDK use, set `forcePathStyle: true`. ACL flips aren't on the CLI yet, so point users at the dashboard. To push a local file into a bucket directly (no AWS SDK needed), use `lizard s3 upload <file> [--addon <name>] [--bucket <name>] [--key <key>] [--content-type <type>]` — auto-resolves the project's only S3 addon and bucket `default` if omitted; prints `{key, etag, size, url}` (`url` is `null` unless the bucket is public). `lizard s3 list|ls [--bucket <name>] [--prefix <p>]` lists objects in a bucket.
426
-
427
- ## Composition patterns
428
-
429
- Multi-step requests follow natural chains. Return one unified response, don't farm out steps:
430
-
431
- - First deploy from git — pick action via [Setup decision flow](#setup-decision-flow) → `lizard add -r owner/repo` → stream build → surface URL.
432
- - First deploy from local code — Setup decision flow → `lizard up` → surface URL.
433
- - Add a managed database to an existing service — `lizard add postgres` → bind the consumer's `DATABASE_URL` to the private reference as described above (the service restarts with it; no redeploy needed).
434
- - Add object storage to a service — `lizard add s3` → reference `${{s3.S3_ENDPOINT}}`, `${{s3.S3_DEFAULT_BUCKET}}`, `${{s3.S3_ACCESS_KEY_ID}}`, `${{s3.S3_SECRET_ACCESS_KEY}}`, `${{s3.S3_REGION}}` from the consumer service. The `default` bucket is private; to serve files without auth, have the user make it public in the dashboard's S3 browser, then objects are served at `<dashboard-host>/api/s3/<addonId>/public/default/<key>`. See [Managed addons](#managed-addons).
435
- - Wire a fresh git source on an existing service — `service set --set sourceType=github --set repoUrl=… --set branch=…` → `redeploy`.
436
- - Fix a failed build — `logs --build` → diagnose → fix project (user's repo) OR adjust `buildCommand` / `startCommand` via `service set` → `redeploy` → `logs` to verify.
437
- - Add a custom domain — `domain <host> --service <svc>` (the hostname is a positional, there is no `add` subcommand) → surface the TXT/CNAME records to the user → `domain verify <host>` once DNS propagates. Bare `domain` shows (or auto-generates) the service's current domain. If the host is already attached to another of the user's services, the attach 409s with a reclaim hint — re-run with `--force` to move it here.
438
-
439
- ## Common ops
440
-
441
- ```
442
- lizard logs --json [--service <name>] # last 200 runtime log lines, then exit (--tail N to override)
443
- lizard logs --build --json # last build's logs
444
- lizard logs --restart latest --json # log tail of the most recent crash/restart
445
- lizard ps --json # services with status + URL (per-replica detail: `events`)
446
- lizard status # cwd project link (no auth needed)
447
- lizard restart --service <name> # rolling restart
448
- lizard redeploy [--service <name>] # rebuild + redeploy from current source
449
- lizard scale --service <name> --replicas N
450
- lizard domain example.com --service <name> # attach custom domain (positional, not `domain add`)
451
- lizard domain --json # show/auto-generate the service's current domain
452
- lizard domain verify example.com # activate after DNS records propagate
453
- lizard domain example.com --service <name> --force # move domain here from another of your services
198
+ lizard logs --service <svc> --json # last 200 runtime lines, then exit (--tail N, max 1000); without --service it mixes every service
199
+ lizard logs --build --service <svc> --json # last build's logs
200
+ lizard logs --restart latest --service <svc> --json # log tail of the most recent crash/restart
201
+ lizard ps --json # {apps, addons}: status and bare `domain` (no https://)
202
+ lizard status --json # this folder's link (no auth needed)
203
+ lizard restart --service <svc> # same image and env; rolling only for an HTTP service without a volume
204
+ lizard redeploy --service <svc> --wait --json # rebuild from the latest commit or last upload; waits until ready
205
+ lizard scale --service <svc> --replicas N --memory <mb> --cpu <cores> # replicas 1-10, memory 128-8192 MB, cpu 1-4
454
206
  lizard metrics --json # CPU/memory/network/disk (add --cost for cost)
455
- lizard billing --json # plan, trial, usage this month, Billing link
456
207
  lizard metrics --all --json # all services in the project, even from a service-linked directory
457
- lizard metrics --all --watch # live overview of all services in the project
458
- lizard events --json # deploy history + replica status
459
- lizard ssh --service <name> -- <cmd> # DISABLED for services: the API answers 403 "Shell access to services is disabled" (sandboxes: `lizard sandbox ssh`)
460
- lizard run --service <name> -- <cmd> # run a command LOCALLY with the service's stored secrets (an addon gives its real connection values)
461
- lizard project list --json # all projects in workspace
208
+ lizard events --service <svc> --json # deploy history + replica status
209
+ lizard run --service <svc> -- <cmd> # run a command LOCALLY with the service's stored secrets (an addon gives its real connection values)
210
+ lizard project list --json # your projects in every workspace (-w <ws> to filter)
462
211
  lizard regions --json
463
- lizard open # open dashboard
464
212
  lizard whoami --json # auth check
465
- lizard s3 upload <file> [--bucket <b>] # upload a local file to an S3 addon bucket
466
- lizard s3 list --json [--bucket <b>] # list objects in an S3 addon bucket
213
+ lizard billing --json # plan, trial, usage this month, Billing link
467
214
  ```
468
215
 
469
- `lizard metrics --all` ignores the directory's linked service for this command.
470
- It uses the linked project unless `--project <name>` selects another project.
471
- Without `--all`, metrics defaults to the linked service, or the project overview
472
- when no service is linked. Do not combine `--all` with `--service`.
473
- The project overview includes `Egress` (outbound bytes per second) and `Volumes`
474
- (used / total bytes). These columns use the latest sample from the past hour;
475
- `Sampled` refers to CPU and memory. Missing measurements show `—`.
476
- Watch mode refreshes CPU and memory every 3 seconds and I/O history every 30 seconds.
477
- JSON output includes the history in each service's `series` and `timestamps` fields.
478
-
479
- `lizard run --service <app>` injects stored values only: `${{…}}` refs arrive as literal text, and platform vars (`PORT`, `LIZARD_*`) are not set. `--service <addon>` injects the addon's real connection values, so use it to run a local command against a database. To check a service's wiring, use `lizard logs --build --service <svc>` and `lizard secrets list --service <svc> --show`; shell access into services is off.
480
-
481
- For exact flags, `lizard <cmd> --help --json`. Other commands not shown above: `lizard git` (GitHub integration), `lizard config` (project configuration), `lizard workspace` (workspace info) — discover each with `lizard <cmd> --help --json`.
216
+ There is no shell into a running service (`lizard ssh` answers 403); for a sandbox use `lizard sandbox exec`. Metrics options and `lizard run` in detail: `lizard skills get core --ref deploys --json`.
482
217
 
483
218
  ## Response format
484
219
 
@@ -493,40 +228,44 @@ Skip command-by-command transcripts unless they explain a failure.
493
228
  ## Don't do
494
229
 
495
230
  1. Don't add Docker `HEALTHCHECK` — the platform ignores it (it doesn't run Docker's healthcheck loop).
496
- 2. On the lizardpack auto-detect path, `Procfile` (`web:`) and `package.json scripts.start` are picked up automatically — don't force a redundant `startCommand`. But the moment `buildCommand`/`startCommand` is set (synthesized-Dockerfile path), neither is read — there set `startCommand` explicitly via `lizard up --start-command` / `service set --set startCommand=...`, or include `CMD` in the user's Dockerfile.
231
+ 2. On the lizardpack auto-detect path, `package.json` `scripts.start` (Node) and a `Procfile` `web:` line (Python, Ruby) are picked up automatically — don't force a redundant `startCommand`. Once `buildCommand` or `startCommand` is set, the platform generates its own Dockerfile and reads neither, nor the user's `Dockerfile`: set `startCommand` explicitly, or set `dockerfilePath` so the user's Dockerfile and its `CMD` are used as is.
497
232
  3. Don't use `lizard up` to switch a service to a git source. It always forces `sourceType=upload`. Use `service set` + `redeploy` instead.
498
- 4. A Dockerfile that copies pre-built artifacts (`COPY dist/`, `build/`, `out/`, `.next/`, `public/`) without a `RUN` build step gets silently regenerated by lizardpack. Add a build step or set `dockerfilePath` to force verbatim use.
233
+ 4. On git builds, a `Dockerfile` whose `COPY` starts with `dist`, `build`, `out`, `.next` or `public` and that has no install or build `RUN` (`npm`, `pip`, `go build`…) is replaced by a lizardpack one (the build log says so). Add a build step, or set `dockerfilePath` to force it as is.
499
234
  5. Don't generate Dockerfiles unsolicited — lizardpack auto-detects most stacks. Try a deploy first; write one only if it fails. Ask before either.
500
- 6. Don't put runtime secrets (DB credentials, API keys, RPC creds, S3 keys) in `--global` "just in case another service needs it later". Scope to the services that consume them — see [Secret scoping](#secret-scoping).
235
+ 6. Don't put runtime secrets (DB credentials, API keys, RPC creds, S3 keys) in `--global` "just in case another service needs it later". Scope to the services that consume them — see [Secrets and config](#secrets-and-config).
501
236
 
502
237
  ## Billing
503
238
 
504
- Billing belongs to the account; these commands take no project or service selector.
239
+ Billing belongs to the account owner. `lizard billing --workspace <id> --json` shows the plan of another workspace's owner.
505
240
 
506
241
  ```sh
507
242
  lizard billing --json # plan, trial, this month's usage, next charge, billingUrl
508
243
  lizard billing start --json # Stripe Checkout link: the Pro trial, or Pro at once
509
244
  lizard billing promo <code> --json # a longer trial with more trial usage
510
245
  lizard billing open # the Billing page
511
- lizard metrics --cost --json # spend per project; proCredits for Pro accounts
246
+ lizard metrics --cost --json # one project's cost per hour and this period's usage (owner only; a key limited to projects can't read costs); proCredits on Pro
512
247
  ```
513
248
 
514
249
  Pro costs $19/month, taxes included, and includes $19 of usage each month for
515
250
  everything on the account; unused usage does not carry over. Usage above that is
516
251
  pay as you go at the published rates, charged to the card as it builds up. A new
517
252
  account starts with no plan. The 7-day trial includes $5 of usage; Checkout asks
518
- for a card and charges nothing until the trial ends, then Pro is $19/month. When
253
+ for a card and charges nothing until the trial ends, then Pro is $19/month. Some
254
+ accounts run a longer trial without a card: read `trial.endsAt` and
255
+ `trial.creditCents` from `lizard billing --json` instead of assuming. When
519
256
  the trial usage runs out, running resources pause and nothing new can be created
520
257
  until Pro starts. Data is kept. Promo codes work only before the first payment.
521
258
  Trial accounts run 1 replica per app and 5 sandboxes at once; after the first
522
259
  paid invoice, 5 replicas per app and 100 sandboxes (paused sandboxes don't
523
260
  count). Pro cards are changed or removed on the Billing page;
524
- `lizard billing payment-methods remove` works only for Enterprise accounts.
261
+ `lizard billing payment-methods remove` works only for Enterprise accounts (Pro: 409 `CREDITS_NOT_AVAILABLE`).
525
262
  Enterprise accounts pay as you go, invoiced monthly. There are no top-ups, no
526
263
  auto top-up and no crypto or x402 payments.
527
264
 
528
265
  When the account needs a plan or a payment, every create command (`up`, `add`,
529
- `sandbox create`, `volume create`, `domain`, redeploys) fails with HTTP 402.
266
+ `sandbox create`, `volume create`, `domain`, `redeploy`, `restart`) fails with
267
+ HTTP 402. In a workspace someone else owns, the 402 is about the owner's plan:
268
+ `lizard billing start` would act on your own account, so ask the owner.
530
269
  The JSON error has `code: "PAYMENT_REQUIRED"` (`"INSUFFICIENT_CREDITS"` from
531
270
  older servers), a `message` to show the user as is (its last line is the link),
532
271
  and `body.status` with the links:
@@ -540,6 +279,12 @@ and `body.status` with the links:
540
279
  | `paused` | Running resources are paused, data kept. `message` says why: trial usage used up (Start Pro now, as above) or a failed payment (pay the open invoice in Billing) |
541
280
  | anything else | Show `message` and `body.billingUrl` |
542
281
 
282
+ `sandbox resume` on an account on hold fails with 403 instead (exit `2`). Too many
283
+ sandboxes at once, or too many starts per minute, is HTTP 429 (`SANDBOX_LIMIT_REACHED`,
284
+ `SANDBOX_RATE_LIMITED` with `retryAfterS`), not a billing problem: wait or delete
285
+ idle sandboxes. `billing start` can also answer 402 `UNPAID_INVOICE` (pay
286
+ `body.invoiceUrl` first) or 409 `ALREADY_SUBSCRIBED`.
287
+
543
288
  Checkout and invoice pages open in the user's browser; you cannot finish them.
544
289
  Give the user the link, never ask for card details in chat, and retry the
545
290
  failed command after they say they are done. `start-now` (charges $19 now),