@oxy-hq/cli 0.4.0 → 0.5.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.
@@ -2,10 +2,6 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "title": "Config",
4
4
  "type": "object",
5
- "required": [
6
- "databases",
7
- "models"
8
- ],
9
5
  "properties": {
10
6
  "base_branch": {
11
7
  "description": "Branch that new worktrees fork from when saving on a protected branch. Defaults to the currently checked-out branch (usually the default branch) when not set. Use this when Oxy serves from a \"deployment\" branch but new work should fork from an \"integration\" branch (e.g. serve from `deploy`, fork new work from `main`).\n\nExample config.yml: base_branch: main",
@@ -25,6 +21,7 @@
25
21
  ]
26
22
  },
27
23
  "databases": {
24
+ "default": [],
28
25
  "type": "array",
29
26
  "items": {
30
27
  "$ref": "#/definitions/Database"
@@ -70,6 +67,7 @@
70
67
  ]
71
68
  },
72
69
  "models": {
70
+ "default": [],
73
71
  "type": "array",
74
72
  "items": {
75
73
  "$ref": "#/definitions/Model"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy-hq/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "oxyc — the Oxy CLI: a gh-api-style client for the Oxy HTTP API, plus the customer-account tooling that scopes a session to one customer.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,20 +44,20 @@
44
44
  ],
45
45
  "license": "MIT",
46
46
  "dependencies": {
47
- "@clack/prompts": "^1.7.0",
47
+ "@clack/prompts": "^1.8.1",
48
48
  "@modelcontextprotocol/sdk": "^1.30.0",
49
49
  "ajv": "^8.20.0",
50
50
  "ajv-formats": "^3.0.1",
51
51
  "commander": "^15.0.0",
52
52
  "picocolors": "^1.1.1",
53
- "yaml": "^2.8.1",
54
- "zod": "^3.25.28 || ^4"
53
+ "yaml": "^2.9.1",
54
+ "zod": "^4.6.5"
55
55
  },
56
56
  "devDependencies": {
57
- "@types/node": "^26.4.1",
58
- "tsdown": "^0.22.14",
57
+ "@types/node": "^26.6.2",
58
+ "tsdown": "^0.23.0",
59
59
  "typescript": "^7.0.2",
60
- "vitest": "^5.0.0"
60
+ "vitest": "^5.0.1"
61
61
  },
62
62
  "engines": {
63
63
  "node": ">=20"
@@ -109,9 +109,15 @@ qualified.)
109
109
  ```
110
110
 
111
111
  `ctx` bridges to the data plane: `ctx.query` / `ctx.queryStream`,
112
- `ctx.semantic.query`, `ctx.warehouse.{insert,exec,upsert}`,
113
- `ctx.airway.run`, `ctx.fetch` (SSRF-allowlisted), `ctx.env`,
114
- `ctx.user`, `ctx.log`. `oxyc publish` bundles `functions/*.ts` with
112
+ `ctx.semantic.query`, `ctx.warehouse.query`,
113
+ `ctx.warehouse.{insert,exec,upsert}` (to Airhouse, or to a customer
114
+ warehouse the function names in `customerWarehouseWrites`), `ctx.airway.run`,
115
+ `ctx.fetch` (SSRF-allowlisted), `ctx.env`, `ctx.user`, `ctx.log`.
116
+ What the app STORES picks its surface by shape — facts through
117
+ `ctx.airhouse`, records through `ctx.oltp`, files through
118
+ `ctx.storage` — and customer warehouses are read-only. See
119
+ **Where data goes** below before writing anything.
120
+ `oxyc publish` bundles `functions/*.ts` with
115
121
  esbuild — no separate backend to stand up. Worked example +
116
122
  author-facing `ctx` types: `oxy-hq/customer-apps:
117
123
  examples/hello-oxy/functions/`.
@@ -124,6 +130,65 @@ qualified.)
124
130
  from the manifest for `pnpm dev` / `pnpm build`. Mismatch → asset
125
131
  404s → blank dashboard.
126
132
 
133
+ ## Where data goes (the shape of the data picks the store)
134
+
135
+ | The data | Store | In a function | In `oxy-app.json` |
136
+ | --- | --- | --- | --- |
137
+ | **Facts** — something happened and won't change: an order, a completed checklist, a delivery, a reading | The workspace's Airhouse, in your schema `app_<writer>` | `ctx.airhouse.append(table, rows)` · `ctx.airhouse.query(sql)` · `ctx.airhouse.exec(sql)` | function: `"airhouse": { "enabled": true }` · app: `"airhouseMigrations": { "dir": "airhouse-migrations" }` |
138
+ | **Records** your app edits — bookings, assignments, templates, counts | The org's OLTP Postgres, in your schema `app_<writer>` | `ctx.oltp.query/exec(sql, params)` · `ctx.oltp.tx(async (tx) => …)` | function: `"oltp": { "enabled": true }` · app: `"migrations": { "dir": "migrations" }` |
139
+ | **Files** — photos, PDFs, exports | Your app's storage silo | `ctx.storage` | function: `"storage": { "read": true, "write": true }` |
140
+ | **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
+
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
+ Three questions, in order: **Is it bytes?** → storage (keep the key in a record
151
+ or fact). **Does it change after it's written?** No → a fact; yes → a record;
152
+ both when you need history (the record says what is true now, the fact says
153
+ what changed). **Is it already in the customer's warehouse?** → read it there.
154
+
155
+ `<writer>` is your slug with `-` → `_` (`store-ops` → `app_store_ops`); a slug
156
+ with `_` can't back a schema. Both stores are written **as the app**, whoever
157
+ invoked the function, so a scheduled run writes like a click.
158
+
159
+ **Facts, in practice.** Airhouse is DuckLake: no `PRIMARY KEY`, `UNIQUE`,
160
+ indexes or foreign keys — publish refuses a migration carrying one. So:
161
+
162
+ - Give every fact the id its source assigned and a `recorded_at`. A retried
163
+ function appends twice; readers keep one row per id:
164
+ `QUALIFY row_number() OVER (PARTITION BY completion_id ORDER BY recorded_at DESC) = 1`.
165
+ - A correction is a new fact (`voided`, `amended`), not an `UPDATE`.
166
+ - Name tables `app_<writer>.<table>` in migrations and in `exec`/`query`
167
+ (`ctx.airhouse.schema` has the name). `append` takes the bare table name.
168
+ - One statement per call, nothing bound: request data goes through `append`,
169
+ never into `exec` SQL. `exec` runs no DDL — tables come from migrations.
170
+
171
+ **Records, in practice.** Bound parameters (`$1`, `$2`) always. Several
172
+ statements that must land together go in one `ctx.oltp.tx`. The store is
173
+ provisioned per org by an operator; until then calls fail naming that step.
174
+
175
+ **Not a store:** `ctx.secrets` holds credentials you rotate. A cursor, a
176
+ counter or a JSON blob is a record. `oxyc validate` warns about it, about
177
+ keys in Airhouse migrations, and lists every customer-warehouse write exception.
178
+
179
+ ```ts
180
+ // A checklist is finished: the record changes, and a fact is appended.
181
+ async function completeChecklist(ctx, runId: string, storeGuid: string) {
182
+ await ctx.oltp.tx(async (tx) => {
183
+ await tx.exec("UPDATE checklist_runs SET status = 'done', completed_at = now() WHERE id = $1", [runId]);
184
+ await tx.exec("DELETE FROM open_reminders WHERE run_id = $1", [runId]);
185
+ });
186
+ await ctx.airhouse.append("checklist_completions", [
187
+ { completion_id: runId, store_guid: storeGuid, completed_by: ctx.user.id, recorded_at: new Date().toISOString() },
188
+ ]);
189
+ }
190
+ ```
191
+
127
192
  ## Four pitfalls
128
193
 
129
194
  - **CORS errors are usually wrong-URL errors in disguise.** The SDK's
@@ -278,6 +343,7 @@ be resolved at publish time):
278
343
  ```bash
279
344
  oxyc login --env production # once; browser flow, caches a token,
280
345
  # and prints whether you're an app-admin
346
+ oxyc validate # data-placement and manifest checks first
281
347
  oxyc publish --env production # build → tar → upload (draft channel)
282
348
  oxyc publish --env production --promote # …straight to live
283
349
  ```