@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.
- package/README.md +153 -3
- package/dist/main.mjs +2109 -183
- package/dist/main.mjs.map +1 -1
- package/json-schemas/config.json +2 -4
- package/package.json +7 -7
- package/skills/oxy-custom-apps/SKILL.md +69 -3
package/json-schemas/config.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
54
|
-
"zod": "^
|
|
53
|
+
"yaml": "^2.9.1",
|
|
54
|
+
"zod": "^4.6.5"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@types/node": "^26.
|
|
58
|
-
"tsdown": "^0.
|
|
57
|
+
"@types/node": "^26.6.2",
|
|
58
|
+
"tsdown": "^0.23.0",
|
|
59
59
|
"typescript": "^7.0.2",
|
|
60
|
-
"vitest": "^5.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.
|
|
113
|
-
`ctx.
|
|
114
|
-
|
|
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
|
```
|