@oxy-hq/cli 0.8.0 → 0.9.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 +89 -7
- package/dist/main.mjs +4914 -3760
- package/dist/main.mjs.map +1 -1
- package/dist/{mcp-xxWL3_dz.mjs → mcp-Bdp0rX0l.mjs} +2 -2
- package/dist/{mcp-xxWL3_dz.mjs.map → mcp-Bdp0rX0l.mjs.map} +1 -1
- package/package.json +5 -5
- package/skills/oxy-agents/SKILL.md +318 -0
- package/skills/oxy-agents/references/quick-reference.md +222 -0
- package/skills/oxy-agents/templates/agentic-template.yml +141 -0
- package/skills/oxy-airway-pipelines/SKILL.md +223 -316
- package/skills/oxy-automations/SKILL.md +61 -15
- package/skills/oxy-cli/SKILL.md +32 -5
- package/skills/oxy-custom-apps/SKILL.md +193 -291
- package/skills/oxy-custom-apps/references/minimal-starter.md +63 -0
- package/skills/oxy-custom-apps/references/performance-and-usage.md +105 -0
- package/skills/oxy-custom-apps/references/reading-data.md +106 -0
- package/skills/oxy-custom-apps/references/sandbox-agent-loop.md +267 -0
- package/skills/oxy-custom-apps/references/verifying-in-the-browser.md +65 -0
- package/skills/oxy-data-apps/SKILL.md +362 -0
- package/skills/oxy-data-apps/references/examples.md +263 -0
- package/skills/oxy-data-apps/references/quick-reference.md +171 -0
- package/skills/oxy-data-apps/templates/dashboard-with-controls.app.yml +128 -0
- package/skills/oxy-data-apps/templates/simple-dashboard.app.yml +102 -0
- package/skills/oxy-data-architecture/SKILL.md +137 -152
- package/skills/oxy-evals/SKILL.md +362 -0
- package/skills/oxy-evals/references/diagnosis.md +243 -0
- package/skills/oxy-evals/references/results-json.md +145 -0
- package/skills/oxy-evals/references/writing-expected.md +112 -0
- package/skills/oxy-semantic-modeling/SKILL.md +239 -59
- package/skills/oxy-semantic-modeling/references/warehouse-casts.md +57 -0
- package/skills/oxy-semantic-modeling/templates/topic-template.yml +38 -0
- package/skills/oxy-semantic-modeling/templates/view-template.yml +119 -0
- package/template/.github/workflows/publish.yaml +15 -13
- package/template/.nvmrc +1 -1
- package/template/.oxyc-managed +5 -5
- package/template/README.md +21 -10
- package/template/package.json +3 -2
- package/template/pnpm-workspace.yaml +28 -17
|
@@ -1,36 +1,47 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: oxy-custom-apps
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
Use when building, debugging, reviewing, shipping, or verifying an Oxy
|
|
5
|
+
custom-app bundle (a code-first React/Vite app served by Oxy at
|
|
6
|
+
/customer-apps/<org>/<slug>/), including writing Oxy Functions, driving them
|
|
7
|
+
from the UI, and checking a published app in the browser with the Playwright
|
|
8
|
+
MCP server (no CI checks a published bundle). Covers the oxy-app.json
|
|
9
|
+
contract, @oxy-hq/sdk and @oxy-hq/vite-plugin, where app data goes
|
|
10
|
+
(ctx.airhouse, ctx.oltp, ctx.storage), pitfalls, performance guardrails, load
|
|
11
|
+
speed and usage tracking (service worker, Activity tab, useTrackEvent), the
|
|
12
|
+
self-serve oxyc publish ship flow, dev-<handle> sandboxes and workspace
|
|
13
|
+
previews, how an AI agent gets its own short-lived credential (oxyc tokens
|
|
14
|
+
create --sandbox-agent or --agent, never the engineer's oxyc login), and
|
|
15
|
+
reading a deployment's live data with oxyc api and oxyc proxy (which replaced
|
|
16
|
+
oxy api and the Rust oxy login, publish and proxy).
|
|
4
17
|
---
|
|
5
18
|
|
|
6
|
-
#
|
|
19
|
+
# Custom-app development
|
|
7
20
|
|
|
8
|
-
|
|
21
|
+
Custom-app bundles are arbitrary JS apps served by Oxy at
|
|
9
22
|
`https://<env>/customer-apps/<org>/<slug>/`. Oxy is a data gateway:
|
|
10
23
|
serves static assets, gates cookie auth, and proxies queries. The
|
|
11
24
|
bundle is opaque above the data plane — no per-app server config.
|
|
12
25
|
|
|
26
|
+
This file is the contract. The detail lives in `references/` — see
|
|
27
|
+
[Read next](#read-next) for which file answers what.
|
|
28
|
+
|
|
13
29
|
## The Oxy App Kit (use it)
|
|
14
30
|
|
|
15
31
|
Two small packages remove the footguns — default to them rather than wiring a
|
|
16
32
|
bundle by hand:
|
|
17
33
|
|
|
18
34
|
- **`@oxy-hq/sdk`** — the data layer (`useQuery`, `useSemanticQuery`,
|
|
19
|
-
`useAgentRun`, `<OxyChat>`); see rule 2 and
|
|
20
|
-
docs/sdk-flow.md`.
|
|
35
|
+
`useAgentRun`, `<OxyChat>`); see rule 2 and the `@oxy-hq/sdk` README.
|
|
21
36
|
- **`@oxy-hq/vite-plugin`** (`oxyApp()` in `vite.config.ts`) — bakes the asset
|
|
22
37
|
**base path** from the manifest, copies `oxy-app.json` into `out/`, proxies
|
|
23
38
|
`/api` in dev, and injects the `window.__OXY_APP__` shim. Dropping it
|
|
24
39
|
re-introduces the #1 bug — asset 404s → blank dashboard (rule 3).
|
|
25
40
|
|
|
26
|
-
Scaffold both with
|
|
27
|
-
|
|
28
|
-
platform is kit-agnostic (any bundle with a correct base path
|
|
29
|
-
hand-rolling means owning the base-path + dev-proxy wiring yourself.
|
|
30
|
-
|
|
31
|
-
(**`oxy-hq/customer-apps:` prefixes a path in that repo**, not in whatever repo
|
|
32
|
-
you are sitting in. This skill loads globally, so every such pointer is
|
|
33
|
-
qualified.)
|
|
41
|
+
Scaffold both with `pnpm create @oxy-hq/oxy-app my-app` (templates `vite` — the
|
|
42
|
+
default — `functions`, `dashboard`, `single-store`; `-t functions` adds Oxy
|
|
43
|
+
Functions). The platform is kit-agnostic (any bundle with a correct base path
|
|
44
|
+
works), but hand-rolling means owning the base-path + dev-proxy wiring yourself.
|
|
34
45
|
|
|
35
46
|
## Three rules
|
|
36
47
|
|
|
@@ -60,9 +71,10 @@ qualified.)
|
|
|
60
71
|
don't exist in v2 and the probe rejects them.
|
|
61
72
|
|
|
62
73
|
File location: **`<root>/oxy-app.json`**, sibling to
|
|
63
|
-
`vite.config.ts`. The vite-plugin reads it from there
|
|
64
|
-
|
|
65
|
-
base-path
|
|
74
|
+
`vite.config.ts`. The vite-plugin reads it from there, and so does
|
|
75
|
+
`oxyc publish` (only from the directory it runs in); legacy bundles
|
|
76
|
+
that kept it under `public/` get neither the plugin's base-path
|
|
77
|
+
derivation nor the publish's `slug` / `orgSlug`.
|
|
66
78
|
|
|
67
79
|
2. **All data flows through SDK hooks → oxy HTTP endpoints.** The
|
|
68
80
|
surface is intentionally small:
|
|
@@ -86,8 +98,8 @@ qualified.)
|
|
|
86
98
|
<OxyChat agentId="analytics" />
|
|
87
99
|
```
|
|
88
100
|
|
|
89
|
-
See
|
|
90
|
-
|
|
101
|
+
See the `@oxy-hq/sdk` README for the full reference, including
|
|
102
|
+
`OxyApiError` parsing and beta-surface caveats.
|
|
91
103
|
|
|
92
104
|
**Server-side work → Oxy Functions, not the browser.** `useQuery` &
|
|
93
105
|
friends run in the client; for work the browser shouldn't do
|
|
@@ -118,9 +130,10 @@ qualified.)
|
|
|
118
130
|
`ctx.storage` — and customer warehouses are read-only. See
|
|
119
131
|
**Where data goes** below before writing anything.
|
|
120
132
|
`oxyc publish` bundles `functions/*.ts` with
|
|
121
|
-
esbuild — no separate backend to stand up.
|
|
122
|
-
|
|
123
|
-
|
|
133
|
+
esbuild — no separate backend to stand up. Author-facing types:
|
|
134
|
+
`OxyFunctionContext` / `OxyFunctionRequest` from `@oxy-hq/sdk`; a worked
|
|
135
|
+
example ships in the `functions` template (`pnpm create @oxy-hq/oxy-app
|
|
136
|
+
my-app -t functions`).
|
|
124
137
|
|
|
125
138
|
3. **`OXY_APP_BASE_PATH` must match the path the bundle is linked
|
|
126
139
|
under.** `@oxy-hq/vite-plugin` resolves it from (in order):
|
|
@@ -139,20 +152,12 @@ qualified.)
|
|
|
139
152
|
| **Files** — photos, PDFs, exports | Your app's storage silo | `ctx.storage` | function: `"storage": { "read": true, "write": true }` |
|
|
140
153
|
| **The customer's warehouse** — ClickHouse, Snowflake, BigQuery, their Postgres | Theirs | `ctx.query` · `ctx.warehouse.query(db, sql)` | read-only: a write is refused unless the function declares `"customerWarehouseWrites": { "<db>": "<why>" }` |
|
|
141
154
|
|
|
142
|
-
**Availability.** `ctx.airhouse`, `ctx.oltp.tx`, `airhouseMigrations`, the
|
|
143
|
-
read-only rule for customer warehouses and `oxyc validate`'s data-placement
|
|
144
|
-
and manifest checks arrive with the Oxy release carrying oxygen-internal#3187,
|
|
145
|
-
together with the `@oxy-hq/sdk` and `@oxy-hq/cli` published from it. Against
|
|
146
|
-
an older Oxy, `ctx.airhouse` is `undefined` and the new manifest fields are
|
|
147
|
-
ignored; vendored `oxy.d.ts` copies (`examples/hello-oxy/functions/oxy.d.ts`)
|
|
148
|
-
gain the types when they are re-synced from that SDK.
|
|
149
|
-
|
|
150
155
|
Three questions, in order: **Is it bytes?** → storage (keep the key in a record
|
|
151
156
|
or fact). **Does it change after it's written?** No → a fact; yes → a record;
|
|
152
157
|
both when you need history (the record says what is true now, the fact says
|
|
153
158
|
what changed). **Is it already in the customer's warehouse?** → read it there.
|
|
154
159
|
|
|
155
|
-
`<writer>` is your slug with `-` → `_` (`
|
|
160
|
+
`<writer>` is your slug with `-` → `_` (`my-app` → `app_my_app`); a slug
|
|
156
161
|
with `_` can't back a schema. Both stores are written **as the app**, whoever
|
|
157
162
|
invoked the function, so a scheduled run writes like a click.
|
|
158
163
|
|
|
@@ -189,6 +194,90 @@ async function completeChecklist(ctx, runId: string, storeGuid: string) {
|
|
|
189
194
|
}
|
|
190
195
|
```
|
|
191
196
|
|
|
197
|
+
## Telling people something
|
|
198
|
+
|
|
199
|
+
Name **people**; the platform reaches them. Don't build a recipient table, a
|
|
200
|
+
"sent" log or a Slack webhook helper:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
await ctx.notify({
|
|
204
|
+
to: { role: "Store Manager", location: storeId }, // or { userId } · { team }
|
|
205
|
+
kind: "list_overdue", // declared in oxy-app.json
|
|
206
|
+
title: "Opening list is 30 minutes late",
|
|
207
|
+
link: `/lists/${listId}`, // a path inside the app
|
|
208
|
+
email: true, // optional; slack: true too
|
|
209
|
+
idempotencyKey: `overdue:${occurrenceId}`, // retry-safe
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`oxy-app.json`: `"notify": { "kinds": { "list_overdue": {} } }` on the function
|
|
214
|
+
(`{ "critical": true }` for a kind people must not mute). Everyone lands in
|
|
215
|
+
their Oxygen inbox — frontline crew too, who have no email — and is pushed,
|
|
216
|
+
emailed or Slacked where they can be. `slack: true` DMs each person's own Slack
|
|
217
|
+
account (found by their email once the org has connected Slack), so an app
|
|
218
|
+
never holds a Slack token or maps users to Slack ids. Needs the org's OLTP
|
|
219
|
+
store. Use
|
|
220
|
+
`ctx.email.send` only for addresses **outside** the org (a vendor, an
|
|
221
|
+
accountant), and give it an `idempotencyKey` in a scheduled function.
|
|
222
|
+
|
|
223
|
+
### Categories, levels, threads and actions
|
|
224
|
+
|
|
225
|
+
Describe your kinds once, at the top level of `oxy-app.json`. It is the tree
|
|
226
|
+
people mute in (a whole app, a category, or one kind) and the one a phone groups
|
|
227
|
+
by, so declare it with the first `ctx.notify`, not later:
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
"notifications": {
|
|
231
|
+
"categories": {
|
|
232
|
+
"operations": { "label": "Store operations" },
|
|
233
|
+
"operations/food-safety": { "label": "Food safety" }
|
|
234
|
+
},
|
|
235
|
+
"kinds": {
|
|
236
|
+
"list_overdue": { "category": "operations", "label": "Overdue lists", "level": "time_sensitive" },
|
|
237
|
+
"temp_alarm": { "category": "operations/food-safety", "label": "Temperature alarm", "level": "critical",
|
|
238
|
+
"actions": [{ "id": "ack", "label": "Acknowledge", "function": "ack-alarm" }] }
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
- **Category**: a path of up to 3 segments (`a-z0-9`, `_`, `-`); a declared
|
|
244
|
+
path's parents need no entry. A kind with none is in `general`.
|
|
245
|
+
- **Level**: `passive`, `active` (default), `time_sensitive`, `critical`. Only
|
|
246
|
+
`critical` ignores mutes; a function's `"critical": true` still makes it so.
|
|
247
|
+
- **Actions**: at most 3, label ≤ 24 characters. With `function` (one this app
|
|
248
|
+
declares), a tap calls it as the person with `{ notificationId, action, link }`
|
|
249
|
+
and marks it read; without, it opens the link.
|
|
250
|
+
- **Thread**: ``ctx.notify({ …, thread: `store:${storeId}` })`` (≤ 128
|
|
251
|
+
characters) folds a run of notifications about one thing into the newest.
|
|
252
|
+
- The function's `notify.kinds` still decides what it may send; a kind it sends
|
|
253
|
+
that the catalogue leaves out gets the defaults. `oxyc validate`, the Vite
|
|
254
|
+
plugin (`pnpm dev` / `pnpm build`) and `oxyc publish` refuse a malformed or
|
|
255
|
+
unknown category, a bad level, more than 3 actions, an action naming an
|
|
256
|
+
undeclared function, a misspelt field and any `oxy.` kind — every problem at
|
|
257
|
+
once; a catalogue kind no function sends is a warning.
|
|
258
|
+
|
|
259
|
+
The app opens in its own tab, so show the inbox **in the app** — people who
|
|
260
|
+
live in it (crew on a kiosk) never see Oxygen's bell:
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
import { NotificationsBell } from "@oxy-hq/sdk";
|
|
264
|
+
|
|
265
|
+
<NotificationsBell onNavigate={(link) => navigate(link)} /> // in the header
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
It lists this app's notifications (`scope="all"` for the whole inbox), each
|
|
269
|
+
with its category under the title, `critical` and `time_sensitive` ones marked,
|
|
270
|
+
and threads folded; a tap
|
|
271
|
+
hands the `link` you passed to `ctx.notify` to `onNavigate` (without it, the
|
|
272
|
+
page loads that path under the app's base). It renders nothing where there is
|
|
273
|
+
no inbox (signed out, an older server, `pnpm dev`). `useNotifications()` gives
|
|
274
|
+
`{ items, unread, markRead, markAllRead, refresh, … }` to draw your own, and
|
|
275
|
+
takes `{ category, kind, thread }` to narrow it. Theme
|
|
276
|
+
it with `--oxy-notifications-*` CSS variables. Push sign-up and mutes stay in
|
|
277
|
+
Oxygen's Settings → Notifications — don't build them. In tests,
|
|
278
|
+
`createTestInbox({ manifest })` from `@oxy-hq/sdk/testing` is a fake inbox to pass as
|
|
279
|
+
`<OxyAppProvider fetcher={inbox.fetcher}>`.
|
|
280
|
+
|
|
192
281
|
## Four pitfalls
|
|
193
282
|
|
|
194
283
|
- **CORS errors are usually wrong-URL errors in disguise.** The SDK's
|
|
@@ -211,154 +300,64 @@ async function completeChecklist(ctx, runId: string, storeGuid: string) {
|
|
|
211
300
|
rely on the nil UUID `00000000-...` as a default project; don't
|
|
212
301
|
assume `OXY_STATE_DIR` is writable.
|
|
213
302
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
cheap up front, hard to retrofit. (Established by the pokehouse `post-je` function.)
|
|
218
|
-
|
|
219
|
-
**In an Oxy Function — count the round-trips.** Each isolate run is stateless and
|
|
220
|
-
pays a fresh network hop per `ctx.query` / `ctx.warehouse.*`, so serial *dependent*
|
|
221
|
-
calls dominate latency.
|
|
222
|
-
- Run independent data-plane calls with `Promise.all`, not sequential `await`
|
|
223
|
-
(e.g. resolve reference data **and** provision tables concurrently — only the
|
|
224
|
-
final step needs both).
|
|
225
|
-
- Fold a check into a query that already runs: a duplicate/existence guard is a
|
|
226
|
-
scalar subquery on the main `SELECT`, not a second round-trip.
|
|
227
|
-
- `Promise.all` idempotent DDL (`CREATE TABLE IF NOT EXISTS`); better, keep
|
|
228
|
-
provisioning off the hot path if a migration can own it.
|
|
229
|
-
- A "cheap" extra `ctx.query`/`ctx.log` is N× the cost across N invocations.
|
|
230
|
-
|
|
231
|
-
**Driving many function calls from the UI (bulk actions) — don't go serial.**
|
|
232
|
-
- Independent calls run with **bounded concurrency** (~3–4) via a worker-pool over a
|
|
233
|
-
queue, not one-at-a-time — turns an O(N) wall-clock batch into a few waves.
|
|
234
|
-
- If the calls share mutable server state that races under concurrency (a rotating
|
|
235
|
-
token, a lazily-created table), the best fix is to **remove the shared mutation**
|
|
236
|
-
(e.g. a single-writer scheduled refresher owns the token; requests only read).
|
|
237
|
-
When you can't, warm the state with one call first, then fan out.
|
|
238
|
-
- Don't refetch derived state (a status ledger) after *every* item — the progress UI
|
|
239
|
-
is the live feedback; **throttle** the refetch (~1/s) and flush once at the end.
|
|
240
|
-
- Guard `setState` after the component may have unmounted mid-batch (`mounted` ref).
|
|
241
|
-
|
|
242
|
-
**Trust the server, not the client body.** Environment / tenant / which-secret
|
|
243
|
-
selection is derived server-side (a function secret, a server-side mapping table,
|
|
244
|
-
the authed identity), never from the request body — validate or override anything
|
|
245
|
-
the client sends that picks a target.
|
|
246
|
-
|
|
247
|
-
**Reuse the SDK's data-plane caching.** `useQuery` already dedups concurrent identical
|
|
248
|
-
queries and short-caches results — don't hand-roll a fetch that bypasses it. Mark
|
|
249
|
-
read-only functions `cache: { ttlSeconds }` in the manifest; **never** cache a
|
|
250
|
-
side-effecting one (it silently drops writes). `idempotencyKey` = a fresh UUID
|
|
251
|
-
per ATTEMPT (click), never a stable per-subject key: the server stores each
|
|
252
|
-
key's response indefinitely and replays it WITHOUT running the function, so a
|
|
253
|
-
`store:period` key silently breaks any post → compensate (delete) → post-again
|
|
254
|
-
cycle — the second post "succeeds" with the stale response and writes nothing.
|
|
255
|
-
The key shields one request from transport duplication; cross-attempt dedup
|
|
256
|
-
belongs in the function itself (a server-side guard over real state, e.g. a
|
|
257
|
-
posted-ledger row the compensating action clears).
|
|
258
|
-
|
|
259
|
-
**Watch-outs when optimizing (each surfaced in review):**
|
|
260
|
-
- **Clear UI running-state in `finally`.** An async batch driver must reset its
|
|
261
|
-
`running`/loading flag (and do its final refetch) in a `finally`, not after the
|
|
262
|
-
`await` — a stray throw otherwise leaves the bar stuck at "…" with the button
|
|
263
|
-
disabled.
|
|
264
|
-
- **Parallelizing can move a side effect ahead of a gate.** Running an independent
|
|
265
|
-
side-effecting call concurrently with a validation query means it can now fire
|
|
266
|
-
even when validation would reject. Fine if the side effect is benign — just know
|
|
267
|
-
you traded the ordering for the latency.
|
|
268
|
-
- **Additive migration for a scoped guard → backfill legacy rows to their TRUE value,
|
|
269
|
-
not a constant.** When a dedup/uniqueness guard starts keying on a new column,
|
|
270
|
-
`ADD COLUMN … DEFAULT <constant>` silently defeats the guard for pre-existing rows.
|
|
271
|
-
Derive the real value from an existing column (e.g. a DocNumber prefix) so old rows
|
|
272
|
-
stay correctly scoped.
|
|
273
|
-
|
|
274
|
-
## Minimal starter
|
|
275
|
-
|
|
276
|
-
Easiest path: `pnpm dlx @oxy-hq/create-oxy-app@latest my-app --template vite`.
|
|
277
|
-
The scaffolded layout:
|
|
278
|
-
|
|
279
|
-
```
|
|
280
|
-
my-app/
|
|
281
|
-
├── oxy-app.json ← project root, not public/
|
|
282
|
-
├── src/
|
|
283
|
-
│ ├── App.tsx
|
|
284
|
-
│ └── main.tsx
|
|
285
|
-
├── package.json
|
|
286
|
-
├── vite.config.ts
|
|
287
|
-
└── tsconfig.json
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
`oxy-app.json`:
|
|
291
|
-
```json
|
|
292
|
-
{ "schemaVersion": 2, "slug": "my-app", "orgSlug": "test", "name": "My App" }
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
`src/main.tsx`:
|
|
296
|
-
```tsx
|
|
297
|
-
import { OxyAppProvider } from "@oxy-hq/sdk";
|
|
298
|
-
import { createRoot } from "react-dom/client";
|
|
299
|
-
import { App } from "./App";
|
|
300
|
-
|
|
301
|
-
createRoot(document.getElementById("root")!).render(
|
|
302
|
-
<OxyAppProvider><App /></OxyAppProvider>
|
|
303
|
-
);
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
`src/App.tsx`:
|
|
307
|
-
```tsx
|
|
308
|
-
import { useQuery } from "@oxy-hq/sdk";
|
|
309
|
-
|
|
310
|
-
export function App() {
|
|
311
|
-
const { rows, loading, error } = useQuery({
|
|
312
|
-
sql: "SELECT 1 AS hello",
|
|
313
|
-
});
|
|
314
|
-
if (loading) return <p>loading…</p>;
|
|
315
|
-
if (error) return <p style={{ color: "red" }}>{error.message}</p>;
|
|
316
|
-
return <pre>{JSON.stringify(rows, null, 2)}</pre>;
|
|
317
|
-
}
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
`vite.config.ts`:
|
|
321
|
-
```ts
|
|
322
|
-
import oxyApp from "@oxy-hq/vite-plugin";
|
|
323
|
-
import react from "@vitejs/plugin-react";
|
|
324
|
-
import { defineConfig } from "vite";
|
|
325
|
-
|
|
326
|
-
export default defineConfig({
|
|
327
|
-
plugins: [react(), oxyApp()],
|
|
328
|
-
});
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
The plugin handles `outDir = "out"`, base-path resolution from the
|
|
332
|
-
manifest, dev-server proxy to `/api`, and the `window.__OXY_APP__`
|
|
333
|
-
dev shim. Plain `pnpm build` produces a bundle with the correct
|
|
334
|
-
asset base baked in.
|
|
303
|
+
Two more that read as bugs — `useTrackEvent("oxy-…")` is refused (`400`,
|
|
304
|
+
the prefix is reserved), and a hovered-but-never-opened app records no view —
|
|
305
|
+
are in [references/performance-and-usage.md](references/performance-and-usage.md).
|
|
335
306
|
|
|
336
307
|
## Publishing
|
|
337
308
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
309
|
+
**These commands are a person's.** `oxyc login` is the engineer's own browser
|
|
310
|
+
login, and a publish to a channel or a promote is refused under a sandbox
|
|
311
|
+
agent token. As an agent, publish into your own sandbox
|
|
312
|
+
([references/sandbox-agent-loop.md](references/sandbox-agent-loop.md)) and
|
|
313
|
+
leave the ship step to your engineer.
|
|
314
|
+
|
|
315
|
+
Ship with `oxyc publish` (`npm i -g @oxy-hq/cli`) — **no CI step, no S3 sync,
|
|
316
|
+
no folder picker**. From the app directory:
|
|
342
317
|
|
|
343
318
|
```bash
|
|
344
319
|
oxyc login --env production # once; browser flow, caches a token,
|
|
345
320
|
# and prints whether you're an app-admin
|
|
346
321
|
oxyc validate # data-placement and manifest checks first
|
|
347
|
-
oxyc publish --env production # build → tar → upload (draft channel)
|
|
322
|
+
oxyc publish --env production # build → bundle functions → tar → upload (draft channel)
|
|
348
323
|
oxyc publish --env production --promote # …straight to live
|
|
349
324
|
```
|
|
350
325
|
|
|
351
|
-
- **Identity
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
326
|
+
- **Identity**, for each half, is `--app` / `--org`, then `OXY_APP` /
|
|
327
|
+
`OXY_ORG`, then the manifest's `slug` + `orgSlug`, then an
|
|
328
|
+
`apps/<org>/<app>/` working directory. `--org` takes a slug or a UUID; pass
|
|
329
|
+
the slug whenever `oxyc` runs the build, because the asset base path is
|
|
330
|
+
baked from what you pass. With no org from any of those, the build stops
|
|
331
|
+
with `missing org: the app's base path needs it` (exit `2`): add
|
|
332
|
+
`--org <org-slug>`, and `--app <slug>` when the directory has no manifest.
|
|
333
|
+
The server upserts the app row — there is no separate create step.
|
|
334
|
+
- **First publish of a new app** needs `--project <workspace-uuid>` (unless
|
|
335
|
+
the app was registered in the admin UI): the build-config lookup can't
|
|
336
|
+
resolve a project until the app exists. A pinned workspace also supplies
|
|
337
|
+
the org. Drop the flag after the first publish — it's auto-resolved from
|
|
338
|
+
then on.
|
|
339
|
+
- **`--env` targets:** `local` → a local Oxy web-app Vite dev server `:5173`
|
|
340
|
+
(it serves `/cli-auth` and proxies `/api` to oxy on `:3000`), `dev` →
|
|
341
|
+
`aip.dev.oxy.tech`, `staging` → `aip.staging.oxy.tech`, `production` (the
|
|
342
|
+
default) → `app.oxygen-hq.com`. A value with a `.`, `/` or `:` is read as a
|
|
343
|
+
URL.
|
|
344
|
+
- **Function lint.** Before the build, each Oxy Function's source is checked
|
|
345
|
+
for what the host would refuse at the first call: a `ctx.*` call whose
|
|
346
|
+
capability the manifest lacks, a global the isolate does not have
|
|
347
|
+
(`Buffer`, `TextEncoder`, `process`, …), a write outside `destinations`. A finding fails
|
|
348
|
+
the publish (exit `1`); `--allow-function-lint` publishes past a false
|
|
349
|
+
positive.
|
|
350
|
+
- `--dir out` skips the build and uploads a prebuilt directory (declared
|
|
351
|
+
functions are still bundled into it); `--build-only` builds and bundles
|
|
352
|
+
without a credential or an upload; `--prebuilt` (with `--dir`) uploads a
|
|
353
|
+
directory whose functions are already bundled. Server warnings print on
|
|
354
|
+
stderr; `--json` prints the result.
|
|
355
|
+
- `.env.local` / `.env` are loaded from the app directory and its parents
|
|
356
|
+
(never overriding what is already set), so a stray `OXY_PROJECT` there
|
|
357
|
+
retargets a publish.
|
|
358
|
+
- **Credential:** `OXY_TOKEN`, then `oxyc login`'s cache — or, in a GitHub
|
|
359
|
+
Actions job with `id-token: write`, the job's OIDC identity.
|
|
360
|
+
`oxyc init-ci` writes a workflow that publishes with no stored secret.
|
|
362
361
|
- Promote / roll back / unpublish from the oxy admin app-detail console;
|
|
363
362
|
every publish is audited (who shipped each build, when).
|
|
364
363
|
- **What is live now:** `oxyc apps show <org>/<app>` prints the live build, the
|
|
@@ -367,7 +366,8 @@ oxyc publish --env production --promote # …straight to live
|
|
|
367
366
|
lists the commits in your checkout that touch the app after the live build's
|
|
368
367
|
commit — `unknown` there means it could not compare, not that nothing changed.
|
|
369
368
|
|
|
370
|
-
|
|
369
|
+
The build is not the check: after a publish, drive the page
|
|
370
|
+
([references/verifying-in-the-browser.md](references/verifying-in-the-browser.md)).
|
|
371
371
|
|
|
372
372
|
## Sandboxes — try a change on real data before shipping it
|
|
373
373
|
|
|
@@ -375,13 +375,12 @@ A sandbox (`dev-<handle>`, 1-12 lowercase letters/digits/hyphens, no leading,
|
|
|
375
375
|
trailing or double hyphen) is a named, short-lived environment of one app: its
|
|
376
376
|
own build pointer, storage silo, secrets and Airhouse sibling schema. Reads
|
|
377
377
|
hit production; most writes are isolated or *held* (recorded, not performed —
|
|
378
|
-
`ctx.oltp`/`ctx.warehouse` stay shared
|
|
379
|
-
`
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
every iteration that would otherwise touch staging. Limit: 20 per app.
|
|
378
|
+
`ctx.oltp`/`ctx.warehouse` stay shared; a held write's `note` field in
|
|
379
|
+
`invocations held` names the manifest key that would make it land instead).
|
|
380
|
+
Schedules and webhooks do not fire in a sandbox: call the function. It starts
|
|
381
|
+
with no build, costs nothing idle, and is gone after 7 idle days or an
|
|
382
|
+
explicit delete — safe to use for every iteration that would otherwise touch
|
|
383
|
+
staging. Limit: 20 per app, counting ones still being deleted.
|
|
385
384
|
|
|
386
385
|
```bash
|
|
387
386
|
oxyc env create acme/store dev-x --env dev --json # starts with no build
|
|
@@ -391,47 +390,12 @@ oxyc fn call acme/store submit-order --app-env dev-x --env dev --data '{}' --jso
|
|
|
391
390
|
oxyc checks run acme/store --app-env dev-x --env dev --json # run its checks
|
|
392
391
|
oxyc invocations list acme/store --app-env dev-x --env dev --json # what ran; a row's id is `id`
|
|
393
392
|
oxyc invocations held acme/store <invocation-id> --env dev --json # what it held, not wrote
|
|
393
|
+
oxyc logs acme/store --app-env dev-x --env dev --json # its function logs
|
|
394
394
|
oxyc env delete acme/store dev-x --env dev --yes --wait --json # done; tears its homes down
|
|
395
395
|
```
|
|
396
396
|
|
|
397
397
|
(`--data` has no short form — don't write `-d`.)
|
|
398
398
|
|
|
399
|
-
**An agent's credential is a sandbox agent token** (`oxy_sbx_…`), not a staff
|
|
400
|
-
login. Get it yourself, once per task — your operator approves in the browser
|
|
401
|
-
that opens — and end it when the task is done:
|
|
402
|
-
|
|
403
|
-
```bash
|
|
404
|
-
eval "$(oxyc tokens create --sandbox-agent --app acme/store --env dev)" # prints: export OXY_TOKEN=oxy_sbx_…
|
|
405
|
-
oxyc whoami --env dev --json # kind "sandbox_agent", your app in `apps`, `expires_at` after the task ends
|
|
406
|
-
# … the loop above …
|
|
407
|
-
oxyc tokens revoke --current --env dev # last, after the delete
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
It does this loop on the apps it was minted for (`--app`, up to five), in
|
|
411
|
-
sandboxes it created, for 8 hours (`--hours`, up to 168) — and nothing else:
|
|
412
|
-
production, staging, other apps, `oxyc api`, `oxyc apps` and a secret's value
|
|
413
|
-
are all refused. With it, every verb that takes `--app-env` requires a
|
|
414
|
-
`dev-<handle>` (exit `2` otherwise), and at most three of its sandboxes may be
|
|
415
|
-
live at once.
|
|
416
|
-
|
|
417
|
-
**Over MCP.** If your runtime speaks MCP, `oxyc mcp --env dev` serves the same
|
|
418
|
-
loop as tools — `oxy_env_create` / `oxy_env_list` / `oxy_env_show` /
|
|
419
|
-
`oxy_env_delete`, `oxy_publish_sandbox`, `oxy_fn_call`, `oxy_checks_run`,
|
|
420
|
-
`oxy_invocations_list` / `oxy_invocations_held`, `oxy_logs`. It reads its
|
|
421
|
-
credential from `OXY_TOKEN` and nowhere else (a person on their own login
|
|
422
|
-
adds `--login`). With a sandbox agent token the list is those ten,
|
|
423
|
-
`oxy_whoami`, `oxy_env_secret_list` / `_set` / `_delete` and
|
|
424
|
-
`oxy_token_revoke` (`confirm: true`) — and no `oxy_request`. Prefer them to
|
|
425
|
-
shelling out: each calls its verb's own request-building code, so the result
|
|
426
|
-
is the document that verb's `--json` prints, and the choice changes nothing
|
|
427
|
-
on the server. There is no exit code over MCP: a failure is `isError: true`
|
|
428
|
-
with the class at the end of its text (`[exit 5 NOT_FOUND]`, plus `server
|
|
429
|
-
code: <code>` when the server sent one) — branch on that the way you would
|
|
430
|
-
on the exit code. `oxy_fn_call` and `oxy_checks_run` set `isError` when the
|
|
431
|
-
function or a check failed, with the full document still in the result.
|
|
432
|
-
`oxy_env_delete` needs `confirm: true`. `oxy_publish_sandbox` takes only a
|
|
433
|
-
`dev-<handle>` and cannot reach staging or production.
|
|
434
|
-
|
|
435
399
|
Iterate from `publish` as the code changes — each publish is a new build, and
|
|
436
400
|
the sandbox always serves the newest. A sandbox build is **never promoted**:
|
|
437
401
|
ship by publishing the same tree to staging, then promoting from the console.
|
|
@@ -439,86 +403,24 @@ Staff only (needs `develop_apps` + `manage_apps`) — or a sandbox agent token
|
|
|
439
403
|
such staff approved; a publish token is refused on every sandbox operation,
|
|
440
404
|
so CI cannot use one.
|
|
441
405
|
|
|
442
|
-
**
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
- Branch on exit codes and JSON fields only, never on stderr text: `0` ok,
|
|
448
|
-
`1` it ran and the thing itself failed, `2` you called it wrong, `4` not
|
|
449
|
-
authenticated — your token expired, was revoked, or your operator lost
|
|
450
|
-
access (stop — don't try another credential, a cached `oxyc login`
|
|
451
|
-
included), `5` not found
|
|
452
|
-
(environment/function/invocation — check the name, or publish first if
|
|
453
|
-
the environment has no build), `6` a conflict or bad name (pick another
|
|
454
|
-
name, wait out a teardown, or free up the 20-sandbox limit — or, on
|
|
455
|
-
`token_sandbox_limit`, delete one of your own three with `--wait` first: a
|
|
456
|
-
sandbox still being deleted counts toward the three), `7` retryable
|
|
457
|
-
(network/5xx, or a `--wait` deadline), `8` refused (stop and report), `9`
|
|
458
|
-
a check failed — read `invocations held` for that check's invocation
|
|
459
|
-
before assuming the function is broken.
|
|
460
|
-
- Hard rules: never pass `--promote` in this loop; never `publish` without
|
|
461
|
-
`--app-env` while iterating in a sandbox; never use `production` as
|
|
462
|
-
`--app-env` to "check" something — reads already hit production. Always
|
|
463
|
-
delete the sandbox you created, **even when an earlier step failed**. On
|
|
464
|
-
exit `4` or `8`, stop and report to a human rather than retrying with a
|
|
465
|
-
different credential or flag. A held write is information to report, not
|
|
466
|
-
an error to engineer around. Set a sandbox's secret with `oxyc env secret
|
|
467
|
-
set` (`oxy_env_secret_set`), never a hand-built request, and never try to
|
|
468
|
-
read a value back. Revoke your token last: `oxyc tokens revoke --current`.
|
|
469
|
-
- Report back: the sandbox name, the build id(s) published, each check's
|
|
470
|
-
`passed`, every held write (`op`/`plane`/`table`/`note`), and confirmation
|
|
471
|
-
the sandbox was deleted.
|
|
406
|
+
**An agent runs this loop on its own sandbox agent token**, never a person's
|
|
407
|
+
login, with `--token-env OXY_TOKEN` on every command — the mint, exit codes,
|
|
408
|
+
rules and what to hand back are in
|
|
409
|
+
[references/sandbox-agent-loop.md](references/sandbox-agent-loop.md). Read it
|
|
410
|
+
before an agent runs any `oxyc` command against a deployment.
|
|
472
411
|
|
|
473
412
|
**A sandbox isolates the app, not the workspace.** The semantic model,
|
|
474
413
|
automations and Airway pipelines it reads are `main`'s. To try a workspace
|
|
475
|
-
branch, open a
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
read them with `preview runs list` / `runs show`. A `--wait` verb exits `1`
|
|
489
|
-
when what it waited for ended `failed` or `cancelled`, `7` past its deadline.
|
|
490
|
-
Over MCP the same verbs are `oxy_preview_create` / `_list` / `_show` /
|
|
491
|
-
`_delete` (`confirm: true`) / `_checks` / `_run` / `_runs_list` / `_run_show`.
|
|
492
|
-
To make a sandbox read the branch's semantic model, publish to it with
|
|
493
|
-
`--semantic-branch <branch>`.
|
|
494
|
-
|
|
495
|
-
## Reading data while vibe-coding
|
|
496
|
-
|
|
497
|
-
You don't need an `X-API-Key` to poke at the data API by hand — your
|
|
498
|
-
`oxyc login` token authenticates as a bearer. From an app directory:
|
|
499
|
-
|
|
500
|
-
```bash
|
|
501
|
-
oxyc login --env local # once; caches a token per host
|
|
502
|
-
|
|
503
|
-
# gh-api-style client (resolves the target + bearer for you, no headers):
|
|
504
|
-
oxyc api user --env local # who am I?
|
|
505
|
-
oxyc api projects/<projectId>/query --env local -f sql='select 1'
|
|
506
|
-
|
|
507
|
-
# raw curl with your token (e.g. to script or debug):
|
|
508
|
-
curl -H "Authorization: Bearer $(oxyc token --env local)" \
|
|
509
|
-
-H 'content-type: application/json' \
|
|
510
|
-
localhost:5173/api/projects/<projectId>/query -d '{"sql":"select 1"}'
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
`oxyc api <path>` takes the path relative to `/api/`; `-X` sets the method
|
|
514
|
-
(POST when a body is given), `-f`/`-F` set the body (string / JSON-typed),
|
|
515
|
-
`--input` sends a raw one, `-i` includes response headers. `--env` resolves the
|
|
516
|
-
same targets as `oxyc publish` (`local` → the Vite dev server `:5173`, which
|
|
517
|
-
proxies `/api` → oxy `:3000`).
|
|
518
|
-
|
|
519
|
-
Do not guess a path: `oxyc routes <filter>` lists what exists and
|
|
520
|
-
`oxyc schema <path>` gives the body it expects.
|
|
521
|
-
|
|
522
|
-
Access is your **normal org membership** — fine for your own test
|
|
523
|
-
workspace, not a cross-customer backdoor. The `projectId` is the one the
|
|
524
|
-
vite-plugin already resolved for the app (it's in `window.__OXY_APP__`).
|
|
414
|
+
branch, open a workspace preview (staff only, `oxyc preview …`), or publish to
|
|
415
|
+
the sandbox with `--semantic-branch <branch>` — see
|
|
416
|
+
[references/sandbox-agent-loop.md](references/sandbox-agent-loop.md#a-sandbox-isolates-the-app-not-the-workspace).
|
|
417
|
+
|
|
418
|
+
## Read next
|
|
419
|
+
|
|
420
|
+
| File | Read it when |
|
|
421
|
+
| --- | --- |
|
|
422
|
+
| [references/sandbox-agent-loop.md](references/sandbox-agent-loop.md) | You are an AI agent about to touch a deployment: minting the sandbox agent token, the loop, exit codes, rules, what to hand back, when you cannot get a token, the MCP tools, workspace previews. |
|
|
423
|
+
| [references/verifying-in-the-browser.md](references/verifying-in-the-browser.md) | You changed something a person sees (`src/**`, or a function a card calls) and need to prove it on the published page. |
|
|
424
|
+
| [references/reading-data.md](references/reading-data.md) | You need a deployment's live data while building: an agent's own `--agent` token, `oxyc api` / `routes` / `schema`, and `oxyc proxy` for `pnpm dev` against cloud data. |
|
|
425
|
+
| [references/performance-and-usage.md](references/performance-and-usage.md) | You write an Oxy Function, a bulk action, caching or idempotency keys — or need to know what Oxy does for load speed and usage tracking, and the opt-outs. |
|
|
426
|
+
| [references/minimal-starter.md](references/minimal-starter.md) | You are writing an app by hand instead of scaffolding it, or need the smallest correct layout. |
|