@oxy-hq/cli 0.5.0 → 0.6.0

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.
package/README.md CHANGED
@@ -5,8 +5,10 @@ manages customer workspace repos.
5
5
 
6
6
  - **API client** — `api`, `routes`, `schema`, `openapi`, `login`, `whoami`, `assume`, `oltp`
7
7
  - **Customer workspaces** — `list`, `new`, `import`, `doctor`, `update`, `adopt`, `launch`
8
- - **Custom apps** — `publish`, `init-ci`, `proxy`
8
+ - **Custom apps** — `publish`, `init-ci`, `proxy`, `apps`
9
9
  - **Checks** — `checks run`
10
+ - **Sandboxes** — `env`, `fn call`, `invocations`, `logs`
11
+ - **Workspace previews** — `preview`
10
12
  - **Development** — `validate`, `mcp`, `guide`, `skills`
11
13
 
12
14
  ## Install
@@ -77,7 +79,10 @@ Credentials are stored **per deployment**, and the default is production. A
77
79
  oxyc api <path> [flags]
78
80
  ```
79
81
 
80
- `<path>` is relative to `/api`; a leading `/` or `api/` is accepted.
82
+ `<path>` is relative to `/api`; a leading `/` or `api/` is accepted. One
83
+ exception: `customer-apps/…` is sent as written, because `/customer-apps/<org>/<app>/…`
84
+ is where an app's bundle is served. The app registry API is `api/customer-apps/…`
85
+ — spell the `api/` out for it (a 404 on the bare form says so).
81
86
 
82
87
  | Flag | Meaning |
83
88
  | --- | --- |
@@ -111,9 +116,10 @@ the server.
111
116
  `oxyc login`; `/external/api/**` sends `X-API-Key` from `$OXY_API_KEY`.
112
117
 
113
118
  **`--paginate` is a heuristic.** Oxy sends `Link: rel="next"` on some endpoints
114
- and nothing on others; where there is no header, `oxyc` reads
115
- `pagination.has_next`, then `has_more`, then `page < total_pages`. Nothing
116
- recognised means one page, and it warns when the server said nothing at all.
119
+ and nothing on others; where there is no header, `oxyc` reads `next_offset`
120
+ (and asks again with `?offset=`), then `pagination.has_next`, then `has_more`,
121
+ then `page < total_pages` (asking again with `?page=`). Nothing recognised
122
+ means one page, and it warns when the server said nothing at all.
117
123
 
118
124
  ```bash
119
125
  oxyc api {org}/workspaces --md
@@ -181,6 +187,8 @@ Staff can see and request an org's transactional Postgres — the store behind
181
187
  ```bash
182
188
  oxyc oltp status [--org <slug|uuid|url>] [--json]
183
189
  oxyc oltp provision --org <slug|uuid|url> --writer app:<slug> [--writer pipeline:<source> …] [--yes] [--json]
190
+ oxyc oltp provision --org <slug|uuid|url> --branch staging [--yes] [--json]
191
+ oxyc oltp reset --org <slug|uuid|url> --branch staging [--yes] [--json]
184
192
  ```
185
193
 
186
194
  `status` lists every org, with a database or without; with `--org` it shows
@@ -194,6 +202,15 @@ derived the way the platform derives `ctx.oltp`'s schema (`app:store-ops` →
194
202
  `raw_<source>`. A 404 can be an org outside your staff grant's scope, and an
195
203
  active `assume` session closes `/admin`.
196
204
 
205
+ `--branch staging` also cuts the org's **staging branch**: one copy of the whole
206
+ database per org, which every app's staging environment writes to. It is made
207
+ only by this flag — never by a publish — and re-running it mints whatever a
208
+ writer added since the cut needs, without resetting anyone's data. `status`
209
+ shows its age and calls it stale past 30 days; nothing resets it on a timer.
210
+ `reset --branch staging` re-copies it from production, discarding every app's
211
+ staging data in the org, so it lists those apps and asks first (`--yes`
212
+ without a terminal).
213
+
197
214
  ## Customer workspaces
198
215
 
199
216
  A customer **is** a GitHub repo carrying the `oxy-customer` topic. Tagging the
@@ -225,7 +242,7 @@ the current directory while granting access to the customer's repo.
225
242
  ## Custom apps
226
243
 
227
244
  ```bash
228
- oxyc publish [--env <e>] [--dir <path>] [--promote] [--build-only | --prebuilt] [--json] [--allow-function-lint]
245
+ oxyc publish [--env <e>] [--dir <path>] [--promote] [--build-only | --prebuilt] [--json] [--allow-function-lint] [--app-env <dev-handle>]
229
246
  oxyc init-ci [--app <org>/<app>] [--environment <name>] [--force]
230
247
  oxyc proxy [--port <n>] [--allow-writes] [--allow-events] [--yes]
231
248
  ```
@@ -277,6 +294,104 @@ OIDC. `--promote` makes that publish go live and adds a `checks run` step after
277
294
  it, when the manifest declares a check. It prints the `oxyc api …/publishers`
278
295
  call that registers the workflow.
279
296
 
297
+ **`--app-env <dev-handle>`** moves one sandbox's build pointer instead of the
298
+ draft/live channel — see [Sandboxes](#sandboxes). It needs a staff credential
299
+ (never a publish token or OIDC) and is refused together with `--promote`: a
300
+ sandbox build is never promoted, so the loop is publish to the sandbox,
301
+ iterate, then publish the same tree to staging and promote that.
302
+
303
+ ## Apps
304
+
305
+ Read-only: every request these make is a GET, against `/api/customer-apps/**`
306
+ (the app-admin role). `<app>` is `<org-slug>/<app-slug>` or an app UUID.
307
+
308
+ ```bash
309
+ oxyc apps list [--org <slug>] [--published | --draft] [--builds] [--json]
310
+ oxyc apps show <app> [--json]
311
+ oxyc apps builds <app> [--json]
312
+ oxyc apps health [<app>] [--needs-attention] [--json]
313
+ oxyc apps usage <app> [--json]
314
+ oxyc apps drift [<app>] [--org <slug>] [--dir <path>] [--refresh] [--json]
315
+ ```
316
+
317
+ - **`list`** — every app you can see, across organizations, walking the paged
318
+ registry to its end. `STATE` is `live` when the app has a published build and
319
+ `draft` when it has none. `SOURCE` is the repository the app is *registered*
320
+ against. `--builds` adds the live build's id, the repository and commit it was
321
+ built from, who published it and when — one extra request per app, six at a
322
+ time.
323
+ - **`show`** — one app on one screen: the registry row, the live build, the
324
+ draft build when it is newer than the live one, deployment integrity,
325
+ availability, and the 7-day usage summary. `last promote` is when the promote
326
+ or rollback action was last used; `oxyc publish --promote` does not update it,
327
+ so it can be older than the live build.
328
+ - **`builds`** — the build history, newest first. `CHANNEL` marks the build each
329
+ channel points at: `live`, `draft`, or both for one build.
330
+ - **`health`** — with no argument, the fleet table (published apps only):
331
+ `down`, `degraded`, `not_measured`, `quiet` or `operational` per app, with the
332
+ totals on stderr. `--needs-attention` keeps the first three. With `<app>`: the
333
+ deployment-integrity checks, the availability windows, and the browser errors
334
+ of the last 24 hours.
335
+ - **`usage`** — the 7-day summary, the visitors, and the tracked events by name.
336
+ - **`drift`** — see below.
337
+
338
+ Times are UTC. `--json` prints one document on stdout; a table goes to stdout and
339
+ the count line to stderr.
340
+
341
+ **An empty result and a failed request are different answers.** No apps, no
342
+ builds, no errors recorded: the command says so and exits `0`. A request that
343
+ failed exits with the code its status maps to (`4`, `5`, `7`, …). Where a
344
+ command makes several independent requests (`show`, `health <app>`,
345
+ `list --builds`, `drift`), it prints what it did read, marks what it did not as
346
+ `NOT READ` (`"failed": […]` / `live_build_error` in `--json`), and then exits
347
+ non-zero. These commands report state: a failing health check, a `down` app or
348
+ a drifted app is an answer and exits `0` — read it from the output.
349
+
350
+ The health route answers **503 with its report** when a check fails; `oxyc`
351
+ prints that as a failing report, not as an outage.
352
+
353
+ ### `apps drift`
354
+
355
+ Compares what is live against source on **this machine**. For each app it takes
356
+ the live build's recorded repository and commit, finds a local checkout of that
357
+ repository (the ones `oxyc repos` and `oxyc path` report, or `--dir`), finds the
358
+ app's directory by the `oxy-app.json` whose `slug` and `orgSlug` match — at
359
+ `<app>/oxy-app.json` or `<app>/public/oxy-app.json`, never by folder name — and
360
+ counts the commits on the checkout's **current branch** that touch that
361
+ directory after the published commit.
362
+
363
+ | Result | Meaning |
364
+ | --- | --- |
365
+ | `in sync` | no commit on the current branch touches the app directory after the published commit |
366
+ | `N commits ahead` | that many do; they are listed |
367
+ | `unknown — <reason>` | the comparison could not be made |
368
+
369
+ `unknown` is never reported as `in sync`. Its reasons (`reason` in `--json`):
370
+
371
+ | `reason` | |
372
+ | --- | --- |
373
+ | `not_published` | nothing is live |
374
+ | `source_unrecorded` | the live build records no repository or no commit |
375
+ | `repo_not_checked_out` | the repository is not on this machine — clone it, or pass `--dir` |
376
+ | `commit_not_in_checkout` | the checkout does not have the published commit — `git fetch` there |
377
+ | `app_dir_not_found` | no tracked `oxy-app.json` declares this `slug` and `orgSlug` |
378
+ | `app_dir_ambiguous` | more than one does |
379
+ | `commit_not_on_branch` | the published commit is not an ancestor of the checked-out branch |
380
+ | `working_tree_dirty` | the app directory has uncommitted or untracked files |
381
+ | `git_failed` | `git log` or `git status` failed in the checkout, so the comparison could not be read |
382
+ | `request_failed` | the builds request failed (the command then exits non-zero) |
383
+
384
+ It runs read-only git commands and nothing else: it does not fetch, check out,
385
+ or refresh the index. The answer is therefore about the checkout as it is on
386
+ disk — a branch that is behind its remote reports fewer commits than the remote
387
+ has. With no `<app>`, every published app is compared, and `--dir` applies to
388
+ the apps whose source repository is that checkout's `origin`; with `<app>`,
389
+ `--dir` is used whatever its remote is.
390
+
391
+ An app published to two organizations from one directory (a staging org and a
392
+ production org) matches the manifest for only one of them; the other reports
393
+ `app_dir_not_found` and names the manifest it found with the other `orgSlug`.
394
+
280
395
  ## Checks
281
396
 
282
397
  ```bash
@@ -331,7 +446,12 @@ it admitted any function.
331
446
  the execution alike. So `checks run` after a plain `oxyc publish` verifies the
332
447
  *previously promoted* code, not the draft just uploaded. `oxyc init-ci` writes
333
448
  the step only with `--promote`, where the build just published is the one the
334
- check runs, and only when the manifest declares a check.
449
+ check runs, and only when the manifest declares a check. **With `--app-env`**
450
+ it runs against that environment's own build instead — a sandbox's own
451
+ publish, or staging's — and the report gains `environment` and each check's
452
+ `invocationId`; the publish-token and OIDC paths are unchanged and work only
453
+ against production, since every non-production operation needs a staff
454
+ credential (see [Sandboxes](#sandboxes)).
335
455
 
336
456
  With a publish token, `<app>` must be the **UUID** — resolving a slug means
337
457
  listing every app, which such a token may not do. The OIDC path needs no id at
@@ -346,6 +466,119 @@ expired 90-day API key reads this way, not as a missing one); `2` a bad
346
466
  the deployment answered the OIDC exchange without an `app_id`, meaning it
347
467
  predates trusted checks.
348
468
 
469
+ ## Sandboxes
470
+
471
+ ```bash
472
+ oxyc env create <app> <name> # name is dev-<handle>, 1-12 chars a-z0-9-
473
+ oxyc env list <app>
474
+ oxyc env show <app> <name>
475
+ oxyc env delete <app> <name> [--yes] [--wait [seconds]]
476
+
477
+ oxyc publish --app-env <name> [--env <e>] # see "Custom apps" above
478
+ oxyc fn call <app> <function> [--app-env <name>] [--data <json|@file|->]
479
+ oxyc checks run <app> --app-env <name> # see "Checks" above
480
+
481
+ oxyc invocations list <app> [--app-env] [--build] [--function] [--limit]
482
+ oxyc invocations held <app> <invocation-id>
483
+ oxyc logs <app> [--app-env] [--invocation] [--request] [--hours] [--limit]
484
+ ```
485
+
486
+ A **sandbox** is a named, short-lived `dev-<handle>` environment of one custom
487
+ app: its own build pointer, storage silo, secrets, Airhouse sibling and — in
488
+ an org with an OLTP staging branch — its own copy of the app's database, with
489
+ reads hitting production and most writes either isolated or *held* (recorded,
490
+ not performed — see `invocations held`). It starts with no build, is deleted
491
+ explicitly or after 7 idle days, and an app has at most 20. Full contract,
492
+ including what is **not** isolated (`ctx.warehouse`) and the other gaps:
493
+ `internal-docs/custom-app-sandboxes.md`.
494
+
495
+ `ctx.oltp` in a sandbox runs in a schema of its own (`app_<app>__dev_<handle>`)
496
+ inside the org's staging branch. `publish --app-env` queues it: a copy of
497
+ staging's tables (rows up to a size cap), then the build's OLTP migrations.
498
+ `env show` reports it as `oltp_schema` — `status` is `seeding`, `ready`,
499
+ `failed` (with `error`) or `stale` (the branch was reset; publish again), and
500
+ `structure_only` lists the tables copied empty. Until it is `ready` a
501
+ `ctx.oltp` call in the sandbox is refused with a message that says which.
502
+
503
+ The loop: `env create` → `publish --app-env` → `fn call` / `checks run
504
+ --app-env` → `invocations list` / `held` to read back what ran → iterate from
505
+ `publish` → `env delete --yes --wait` when done. `oxyc guide` has the same six
506
+ lines, meant to sit in an agent's context.
507
+
508
+ **`--app-env <environment>`** (`production`, `staging` or `dev-<handle>`,
509
+ validated client-side — exit `2` on a malformed name) is a *different axis*
510
+ from `--env`, which is the deployment: both can appear on the same command
511
+ (`oxyc fn call acme/store f --env dev --app-env dev-a1`). It is not a global
512
+ flag — only `publish`, `fn call`, `checks run`, `invocations list` and `logs`
513
+ take it. A publish-token credential is refused before any request for
514
+ `--app-env` other than `production` (exit `2`); `env`, `invocations` and
515
+ `logs` refuse one outright, for any environment, because sandbox management
516
+ is a staff console surface.
517
+
518
+ **`env delete`** without `--yes` asks on a terminal and exits `8` (refused)
519
+ off one, same as `oltp reset`. `--wait [seconds]` (default 120) polls until
520
+ the teardown finishes and prints `{"name","status":"deleted"}`; past the
521
+ deadline it exits `7` (retryable) rather than hanging.
522
+
523
+ **`fn call`** POSTs directly to `{target}/customer-apps/{org}/{app}/fn/{name}`
524
+ (no `/api`) — the response is Server-Sent Events, not JSON, so `oxyc` parses
525
+ the `log` / `data` / `done` / `error` frames itself. It exits `0` only when
526
+ the stream ends `done` with a 2xx status; a function that threw, or answered a
527
+ non-2xx, is a function-level failure (exit `1`, `ok: false` in the JSON), kept
528
+ distinct from a non-2xx on the POST itself (exit per the usual status mapping
529
+ — e.g. `EnvironmentHasNoBuild` before the stream starts). The
530
+ `x-oxy-invocation-id` response header becomes `invocationId` in the result.
531
+
532
+ **`invocations held`** is what the non-production write policy did *not* do —
533
+ production's would-be effect, recorded rather than performed. It is `[]` for a
534
+ production invocation, since nothing is held there.
535
+
536
+ ## Workspace previews
537
+
538
+ ```bash
539
+ oxyc preview create <branch> [--wait [seconds]] [--json]
540
+ oxyc preview list [--json]
541
+ oxyc preview show <branch> [--json]
542
+ oxyc preview delete <branch> [--yes] [--json]
543
+ oxyc preview checks <branch> [--json]
544
+
545
+ oxyc preview run <branch> <kind> <ref> [--variables <json|@file|->] [--read-live-only]
546
+ [--window-from <iso>] [--window-to <iso>] [--resource <name>...]
547
+ [--wait [seconds]] [--json]
548
+ oxyc preview runs list <branch> [--json]
549
+ oxyc preview runs show <run-id> [--wait [seconds]] [--json]
550
+ ```
551
+
552
+ A **preview** opens a workspace branch on the real product, against real data,
553
+ without the branch being live — staff only. `create` compiles the branch's
554
+ head into a staging revision (or reuses a ready one for that commit) and is
555
+ idempotent; `--wait [seconds]` (default 120) blocks until the compile is
556
+ `ready` or `failed` instead of returning the `compiling` item the request
557
+ itself answers with, and exits `7` (retryable) past the deadline. There is no
558
+ single-preview route: `show` fetches `list` and filters it client-side, so its
559
+ `5` (not found) is synthesized here, not a literal 404.
560
+
561
+ The loop: `preview create --wait` → `preview checks` → `preview run` →
562
+ `preview runs show --wait` → iterate → `preview delete --yes` when done.
563
+
564
+ **`<kind>`** is `procedure` or `airway_sample` — the only two `POST
565
+ /previews/runs` accepts (exit `2` client-side on anything else, naming the
566
+ read-back verb). `transform_build` and `compare` runs are queued by the
567
+ server's own Airway change check, never started here; `preview runs list`
568
+ surfaces them alongside the kinds you *can* start, and `preview runs show
569
+ <run-id> --wait` reads any of the four back — a procedure's held steps, a
570
+ transform build's or compare's outcome, or a sample's ask and result.
571
+ `--variables` / `--read-live-only` are `procedure`-only; `--window-from` /
572
+ `--window-to` / `--resource` (repeatable) are `airway_sample`-only.
573
+
574
+ Every verb needs `{workspace}` resolved — `--workspace <id>`, exactly the way
575
+ `oxyc api {workspace}/...` resolves it; there is no second workspace
576
+ resolver. A deployment with `OXY_PREVIEW_RUNS` off answers every runs route
577
+ `404 preview_runs_disabled`, surfaced the same way any other server refusal
578
+ is: the server's own `code` rides along on the thrown error, and the exit
579
+ class follows the usual HTTP-status mapping (`400`/`409` → `6`, `404` → `5`).
580
+ Full contract: `internal-docs/workspace-previews.md`.
581
+
349
582
  ## Development commands
350
583
 
351
584
  ```bash
@@ -424,9 +657,22 @@ production target (any host under `oxygen-hq.com`) is refused without `--yes`.
424
657
  Each Oxy Function call carries a W3C trace — the SDK's, or one minted here — and
425
658
  prints `↳ <status> fn <name> request_id=… trace_id=…`.
426
659
 
427
- **`mcp`** serves the API over stdio as four tools — `oxy_routes`, `oxy_schema`,
428
- `oxy_request`, `oxy_whoami` — rather than one per endpoint, so the tool schemas
429
- cost ~2 KB per turn and reach endpoints added after this package shipped.
660
+ **`mcp`** serves the API over stdio as four generic tools — `oxy_routes`,
661
+ `oxy_schema`, `oxy_request`, `oxy_whoami` — rather than one per endpoint, so
662
+ those schemas cost ~2 KB per turn and reach endpoints added after this
663
+ package shipped. Two named exceptions trade that minimalism for validation an
664
+ agent needs to drive a loop unsupervised: the **sandbox loop**
665
+ (`oxy_env_create`/`list`/`show`/`delete`, `oxy_publish_sandbox`,
666
+ `oxy_fn_call`, `oxy_checks_run`, `oxy_invocations_list`/`held`, `oxy_logs`)
667
+ and **workspace previews** (`oxy_preview_create`/`list`/`show`/`delete`/
668
+ `checks`/`run`/`runs_list`/`run_show`). Each tool calls its CLI verb's own
669
+ request-building function, so a tool's result is the same JSON document that
670
+ verb's `--json` prints, and a failure carries the server's own error code
671
+ plus the exit-code class (`[exit 5 NOT_FOUND]`) the way `oxyc`'s exit code
672
+ would. `oxy_env_delete` / `oxy_preview_delete` need `confirm: true` (there is
673
+ no terminal to ask on inside an MCP server), and `oxy_publish_sandbox`
674
+ requires a `dev-<handle>` `appEnv` and refuses production, staging or a
675
+ promote client-side — it can never reach the live channel.
430
676
 
431
677
  ```bash
432
678
  claude mcp add oxyc -- npx -y @oxy-hq/cli mcp --env production