@oxy-hq/cli 0.2.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 +459 -115
- package/dist/main.mjs +3343 -231
- package/dist/main.mjs.map +1 -1
- package/dist/{mcp-VdontkUr.mjs → mcp-CIsyezzU.mjs} +2 -2
- package/dist/mcp-CIsyezzU.mjs.map +1 -0
- package/json-schemas/config.json +2 -4
- package/package.json +8 -8
- package/skills/oxy-custom-apps/SKILL.md +78 -12
- package/template/.github/scripts/ci-publish.sh +3 -3
- package/template/.github/scripts/discover-apps.sh +9 -13
- package/template/.github/scripts/publish-env.sh +3 -3
- package/template/.github/workflows/publish.yaml +82 -90
- package/template/README.md +17 -15
- package/template/_gitignore +1 -1
- package/template/scripts/dev.sh +23 -19
- package/dist/mcp-VdontkUr.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,163 +1,507 @@
|
|
|
1
|
-
# `@oxy-hq/cli`
|
|
1
|
+
# `@oxy-hq/cli`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`oxyc` — a `gh api`-shaped client for the Oxy HTTP API, plus the tooling that
|
|
4
|
+
manages customer workspace repos.
|
|
4
5
|
|
|
5
|
-
- **
|
|
6
|
-
|
|
7
|
-
- **
|
|
8
|
-
|
|
6
|
+
- **API client** — `api`, `routes`, `schema`, `openapi`, `login`, `whoami`, `assume`, `oltp`
|
|
7
|
+
- **Customer workspaces** — `list`, `new`, `import`, `doctor`, `update`, `adopt`, `launch`
|
|
8
|
+
- **Custom apps** — `publish`, `init-ci`, `proxy`
|
|
9
|
+
- **Checks** — `checks run`
|
|
10
|
+
- **Development** — `validate`, `mcp`, `guide`, `skills`
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
in a customer's repo and `oxyc api {org}/workspaces` needs no ids typed.
|
|
12
|
+
## Install
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g @oxy-hq/cli # then: oxyc
|
|
16
|
+
npx @oxy-hq/cli routes # zero-install; works for everything but `skills install`
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
From a checkout:
|
|
17
20
|
|
|
18
21
|
```bash
|
|
19
22
|
cd sdk/cli && pnpm install && pnpm build
|
|
20
|
-
node dist/main.mjs --help
|
|
23
|
+
node dist/main.mjs --help
|
|
24
|
+
pnpm link --global # or, for `oxyc` on PATH
|
|
21
25
|
```
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
Requires `node >= 20`. `gh` (authenticated) is required for the customer
|
|
28
|
+
commands, `jq` for `--jq`, `git` for the repo commands. Each is reported by
|
|
29
|
+
name with its install line the first time a command needs it.
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
24
32
|
|
|
25
33
|
```bash
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
oxyc login --env dev # browser flow; stores a token
|
|
35
|
+
oxyc whoami --env dev # verify it against the deployment
|
|
36
|
+
|
|
37
|
+
oxyc routes threads # find the endpoint
|
|
38
|
+
oxyc schema {workspace}/threads -X POST # find the body it takes
|
|
39
|
+
oxyc api {workspace}/threads --md # call it
|
|
28
40
|
```
|
|
29
41
|
|
|
30
|
-
##
|
|
42
|
+
## Global flags
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
Accepted by every command except `validate`, `guide`, `exit-codes`, `skills`
|
|
45
|
+
and `cache`.
|
|
46
|
+
|
|
47
|
+
| Flag | Default | Meaning |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| `--env <name\|url>` | `production` | deployment to target |
|
|
50
|
+
| `--target <url>` | — | explicit base URL; overrides `--env` |
|
|
51
|
+
| `--token-env <VAR>` | `OXY_TOKEN` | env var holding the bearer token |
|
|
52
|
+
| `--api-key-env <VAR>` | `OXY_API_KEY` | env var holding the key for `/external/api` |
|
|
53
|
+
| `--org <slug>` | — | value for the `{org}` placeholder |
|
|
54
|
+
| `--workspace <id>` | — | value for the `{workspace}` placeholder |
|
|
55
|
+
| `--project <id>` | — | value for the `{project}` placeholder |
|
|
56
|
+
| `--customer <name>` | — | act as though run inside this customer's repo |
|
|
57
|
+
| `--quiet` | — | suppress progress messages on stderr |
|
|
58
|
+
|
|
59
|
+
### Environments
|
|
60
|
+
|
|
61
|
+
`--env` takes a name or any URL. Anything not in this table is used as a URL.
|
|
62
|
+
|
|
63
|
+
| `--env` | URL |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `local` | `http://localhost:5173` |
|
|
66
|
+
| `dev`, `development` | `https://aip.dev.oxy.tech` |
|
|
67
|
+
| `staging` | `https://aip.staging.oxy.tech` |
|
|
68
|
+
| `production`, `prod` (default) | `https://app.oxygen-hq.com` |
|
|
69
|
+
|
|
70
|
+
Credentials are stored **per deployment**, and the default is production. A
|
|
71
|
+
401 against dev usually means you are logged into prod — pass `--env` to
|
|
72
|
+
`login` and to the call.
|
|
73
|
+
|
|
74
|
+
## `oxyc api`
|
|
35
75
|
|
|
36
|
-
|
|
37
|
-
oxyc
|
|
38
|
-
oxyc api {workspace}/threads --jq '.threads[].title'
|
|
76
|
+
```
|
|
77
|
+
oxyc api <path> [flags]
|
|
39
78
|
```
|
|
40
79
|
|
|
41
|
-
|
|
80
|
+
`<path>` is relative to `/api`; a leading `/` or `api/` is accepted.
|
|
81
|
+
|
|
82
|
+
| Flag | Meaning |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `-X, --method <verb>` | HTTP method. Default GET, or POST when a body is present. |
|
|
85
|
+
| `-f, --raw-field <k=v>` | string parameter |
|
|
86
|
+
| `-F, --field <k=v>` | typed parameter — `true`, `3`, `["a"]`, `@file`, `@-` |
|
|
87
|
+
| `-H, --header <name:value>` | extra header (repeatable) |
|
|
88
|
+
| `--input <file\|->` | raw request body from a file, or `-` for stdin |
|
|
89
|
+
| `-q, --jq <expr>` | filter the response through `jq` |
|
|
90
|
+
| `--md` | render the result as a markdown table |
|
|
91
|
+
| `--paginate` | follow every page and return one document |
|
|
92
|
+
| `--paginate-key <field>` | the field holding the rows, when the guess is wrong |
|
|
93
|
+
| `--max-pages <n>` | stop after `n` pages (default 100) |
|
|
94
|
+
| `--slurp` | with `--paginate`, emit an array of pages instead of merging |
|
|
95
|
+
| `--cache <duration>` | reuse a recent successful GET (`30s`, `5m`, `2h`) |
|
|
96
|
+
| `-i, --include` | print the status line and response headers |
|
|
97
|
+
| `--silent` | make the request, print nothing |
|
|
98
|
+
| `--verbose` | log the request before making it |
|
|
99
|
+
| `--timeout <duration>` | request timeout (default `2m`) |
|
|
100
|
+
|
|
101
|
+
**Bodies.** `-F 'ids[]=a' -F 'ids[]=b'` accumulates into an array. On GET, HEAD
|
|
102
|
+
and DELETE, fields become query parameters instead of a body.
|
|
103
|
+
|
|
104
|
+
**Placeholders.** `{org}`, `{workspace}`, `{project}`, `{customer}` and `{me}`
|
|
105
|
+
are resolved from the customer repo you are standing in, from an org URL passed
|
|
106
|
+
to `--env`, or from `--org` / `--workspace` / `--project`. An unresolvable
|
|
107
|
+
placeholder is an error naming the flag that fills it — never a literal sent to
|
|
108
|
+
the server.
|
|
109
|
+
|
|
110
|
+
**Surfaces.** The path selects the credential: `/api/**` sends the bearer from
|
|
111
|
+
`oxyc login`; `/external/api/**` sends `X-API-Key` from `$OXY_API_KEY`.
|
|
112
|
+
|
|
113
|
+
**`--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.
|
|
42
117
|
|
|
43
118
|
```bash
|
|
44
|
-
oxyc
|
|
45
|
-
oxyc
|
|
46
|
-
oxyc
|
|
47
|
-
oxyc
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
`.app.yml` and `.agent.test.yml` against `json-schemas/*.json` — which are
|
|
52
|
-
generated from the Rust config types, so this is a second *reader* of one
|
|
53
|
-
definition rather than a second definition. It does not replace `oxy validate`:
|
|
54
|
-
that one loads the workspace and can resolve `databases:` and `llm.ref`. Where
|
|
55
|
-
they disagree, it wins. No network, no token — the one command that works
|
|
56
|
-
entirely offline.
|
|
57
|
-
|
|
58
|
-
**`oxyc proxy`** forwards a dev server's Oxy calls to a cloud target with your
|
|
59
|
-
login token attached. Side-effecting calls are **held** and tracking events
|
|
60
|
-
**dropped** by default (`--allow-writes` / `--allow-events`); a production
|
|
61
|
-
target is refused without `--yes`.
|
|
62
|
-
|
|
63
|
-
**`oxyc mcp`** serves the API as four MCP tools — `oxy_routes`, `oxy_schema`,
|
|
64
|
-
`oxy_request`, `oxy_whoami` — over stdio. Four rather than one per endpoint
|
|
65
|
-
because an agent runtime ships every tool's schema on every turn, and ~670 of
|
|
66
|
-
them is tens of KB per request; discovery stays a question the agent asks.
|
|
119
|
+
oxyc api {org}/workspaces --md
|
|
120
|
+
oxyc api {workspace}/threads -q '.threads[].title'
|
|
121
|
+
oxyc api {workspace}/sql/query -X POST -f 'sql=select 1' --md
|
|
122
|
+
oxyc api /admin/users --paginate --md
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Discovery
|
|
67
126
|
|
|
68
127
|
```bash
|
|
69
|
-
|
|
128
|
+
oxyc routes [filter] [--json] [--all] [--refresh] # endpoints this deployment mounts
|
|
129
|
+
oxyc schema <path> [-X <verb>] # request/response shape for one
|
|
130
|
+
oxyc openapi # the whole OpenAPI document
|
|
70
131
|
```
|
|
71
132
|
|
|
72
|
-
`
|
|
133
|
+
`routes` is served by `GET /api/_catalog`, so it describes the deployment you
|
|
134
|
+
are talking to rather than a baked table. It is cached per host; `--refresh`
|
|
135
|
+
re-asks. `--all` includes ide-only and worker-only mounts. `--json` adds each
|
|
136
|
+
route's surface and fleet role.
|
|
73
137
|
|
|
74
|
-
|
|
75
|
-
| --- | --- |
|
|
76
|
-
| `-X, --method` | HTTP method. Defaults to GET, or POST with a body. |
|
|
77
|
-
| `-f, --raw-field k=v` | a **string** parameter |
|
|
78
|
-
| `-F, --field k=v` | a **typed** parameter — `true` / `3` / `["a"]` / `@file` / `@-` |
|
|
79
|
-
| `-F 'ids[]=a' -F 'ids[]=b'` | repeats accumulate into an array |
|
|
80
|
-
| `--input @body.json` | raw body from a file, or `-` for stdin |
|
|
81
|
-
| `-q, --jq <expr>` | filter through `jq` |
|
|
82
|
-
| `--md` | render results as a markdown table — handles arrays of objects, `{columns, rows}`, and the header-row-first arrays `/sql/query` returns |
|
|
83
|
-
| `--paginate` / `--slurp` | walk every page; `--slurp` keeps them separate |
|
|
84
|
-
| `--cache 5m` | reuse a recent successful GET |
|
|
85
|
-
| `-i` / `--silent` / `--verbose` | headers / nothing / log the request |
|
|
86
|
-
|
|
87
|
-
On GET, HEAD and DELETE, fields become query parameters instead of a body.
|
|
138
|
+
## Authentication
|
|
88
139
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
140
|
+
```bash
|
|
141
|
+
oxyc login [--env <e>] [--login-env <e...>] [--assume <org> -r <why>]
|
|
142
|
+
oxyc whoami [--json]
|
|
143
|
+
oxyc token # print the bearer, for a raw curl
|
|
144
|
+
oxyc logout
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`--login-env` is repeatable and comma-separated (`--login-env dev,staging`);
|
|
148
|
+
the browser opens once per environment, in sequence.
|
|
94
149
|
|
|
95
|
-
|
|
96
|
-
|
|
150
|
+
Credentials live in the OS config directory under **`oxy`** — the same file the
|
|
151
|
+
Rust `oxy login` wrote before it was removed, so an existing login still works:
|
|
97
152
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
"retryable", `2` is "you called it wrong".
|
|
153
|
+
- macOS: `~/Library/Application Support/oxy/credentials.json`
|
|
154
|
+
- Linux: `$XDG_CONFIG_HOME/oxy/credentials.json`
|
|
101
155
|
|
|
102
|
-
|
|
156
|
+
`OXY_CREDENTIALS_PATH` overrides the path. Caches are separate, under `oxyc`
|
|
157
|
+
(`~/.cache/oxyc`). In CI set `OXY_TOKEN` instead of logging in.
|
|
158
|
+
|
|
159
|
+
## Acting as an organization
|
|
103
160
|
|
|
104
161
|
```bash
|
|
105
|
-
oxyc
|
|
106
|
-
oxyc
|
|
107
|
-
oxyc <
|
|
108
|
-
oxyc doctor <customer> # what the tool knows, changing nothing
|
|
109
|
-
oxyc update <customer> # drift from the workspace template
|
|
110
|
-
oxyc adopt <customer> # give an IMPORTED repo the managed files
|
|
111
|
-
oxyc activity <customer> # merged PRs, theirs and ours
|
|
162
|
+
oxyc assume start --org <slug|uuid|url> -r "<reason>"
|
|
163
|
+
oxyc assume status [--json]
|
|
164
|
+
oxyc assume end [--org <slug>] [--all]
|
|
112
165
|
```
|
|
113
166
|
|
|
114
|
-
|
|
167
|
+
Sessions last **60 minutes and are not renewable** — re-running `start` returns
|
|
168
|
+
the existing session rather than extending it. `--org` is optional when `--env`
|
|
169
|
+
is itself an org URL. The reason is recorded in the audit log.
|
|
170
|
+
|
|
171
|
+
The session belongs to your account, not this terminal, so your browser shares
|
|
172
|
+
it and `end` ends it in both. A staff 403 on a tenant surface is usually no
|
|
173
|
+
active session rather than a role problem; `assume status` answers "none"
|
|
174
|
+
without failing.
|
|
175
|
+
|
|
176
|
+
## OLTP databases
|
|
177
|
+
|
|
178
|
+
Staff can see and request an org's transactional Postgres — the store behind
|
|
179
|
+
`ctx.oltp` — without a shell on the server.
|
|
115
180
|
|
|
116
181
|
```bash
|
|
117
|
-
oxyc
|
|
118
|
-
oxyc
|
|
119
|
-
oxyc assume end # or --all
|
|
120
|
-
oxyc login --login-env dev,staging # log into several at once
|
|
182
|
+
oxyc oltp status [--org <slug|uuid|url>] [--json]
|
|
183
|
+
oxyc oltp provision --org <slug|uuid|url> --writer app:<slug> [--writer pipeline:<source> …] [--yes] [--json]
|
|
121
184
|
```
|
|
122
185
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
186
|
+
`status` lists every org, with a database or without; with `--org` it shows
|
|
187
|
+
that org's store and writers. No credential is ever printed.
|
|
188
|
+
|
|
189
|
+
`provision` prints what it will create and asks first, because it calls a paid
|
|
190
|
+
provider; without a terminal it refuses unless `--yes`. It is idempotent — an
|
|
191
|
+
existing database or writer is reconciled, not duplicated. `app:<slug>` is
|
|
192
|
+
derived the way the platform derives `ctx.oltp`'s schema (`app:store-ops` →
|
|
193
|
+
`app:store_ops`, schema `app_store_ops`); `pipeline:<source>` names
|
|
194
|
+
`raw_<source>`. A 404 can be an org outside your staff grant's scope, and an
|
|
195
|
+
active `assume` session closes `/admin`.
|
|
196
|
+
|
|
197
|
+
## Customer workspaces
|
|
126
198
|
|
|
127
199
|
A customer **is** a GitHub repo carrying the `oxy-customer` topic. Tagging the
|
|
128
|
-
repo is the whole of registration
|
|
129
|
-
|
|
200
|
+
repo is the whole of registration; there is no separate registry file. These
|
|
201
|
+
commands need `gh` authenticated.
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
oxyc list [--json] [--refresh] # the customers
|
|
205
|
+
oxyc path <customer> # where their repo is on this machine
|
|
206
|
+
oxyc new <customer> [--display <name>] # create, tag and scaffold a repo
|
|
207
|
+
oxyc import <org/repo> [--clone] # tag an existing repo
|
|
208
|
+
oxyc remove|rm <customer> [--purge --yes] # untag (never deletes the repo)
|
|
209
|
+
oxyc doctor [<customer>] [--all] # report state, change nothing
|
|
210
|
+
oxyc update <customer> [--apply] [--diff-all] # drift from the workspace template
|
|
211
|
+
oxyc adopt <customer> [--apply] # install managed files an import lacks
|
|
212
|
+
oxyc activity <customer> [--since <date>] [--repo <org/name>] [--write] [--json]
|
|
213
|
+
oxyc launch <customer> [claude-args...] [--here] [--dry-run]
|
|
214
|
+
oxyc repos [--refresh] # where OUR repos are checked out
|
|
215
|
+
```
|
|
130
216
|
|
|
131
|
-
`update` and `adopt` **report by default and write only with `--apply
|
|
132
|
-
|
|
133
|
-
`template/.oxyc-managed
|
|
134
|
-
|
|
217
|
+
`update` and `adopt` **report by default and write only with `--apply`**.
|
|
218
|
+
Neither commits, branches or pushes. What they may rewrite is decided by
|
|
219
|
+
`template/.oxyc-managed`: an unclassified file belongs to the customer and is
|
|
220
|
+
never touched.
|
|
135
221
|
|
|
136
|
-
|
|
222
|
+
`launch` starts a Claude Code session scoped to one customer; `--here` runs in
|
|
223
|
+
the current directory while granting access to the customer's repo.
|
|
224
|
+
|
|
225
|
+
## Custom apps
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
oxyc publish [--env <e>] [--dir <path>] [--promote] [--build-only | --prebuilt] [--json] [--allow-function-lint]
|
|
229
|
+
oxyc init-ci [--app <org>/<app>] [--environment <name>] [--force]
|
|
230
|
+
oxyc proxy [--port <n>] [--allow-writes] [--allow-events] [--yes]
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**`publish`**, from an app directory, runs `oxy-app.json`'s build (default
|
|
234
|
+
`pnpm install`, `pnpm build`, output `out/`), bundles each declared Oxy Function
|
|
235
|
+
with `pnpm exec esbuild` into `functions/<name>.js`, resolves the project from
|
|
236
|
+
the target's public `build-config`, and uploads the bundle — to the **draft**
|
|
237
|
+
channel unless `--promote`. `--env` defaults to **production**; name it.
|
|
238
|
+
|
|
239
|
+
- **Identity**: `--org` / `--app`, then `OXY_ORG` / `OXY_APP`, then the manifest's
|
|
240
|
+
`orgSlug` / `slug`, then an `apps/<org>/<app>/` working directory. `--org`
|
|
241
|
+
takes a slug or a UUID. `--project` pins the workspace (and implies its org).
|
|
242
|
+
- **`--dir`** publishes a pre-built directory instead of building; functions are
|
|
243
|
+
still bundled into it. **`--build-only`** stops after building and bundling —
|
|
244
|
+
no credential, no network unless the org must come from `--project`.
|
|
245
|
+
**`--prebuilt`** (with `--dir`) skips esbuild and refuses if a declared
|
|
246
|
+
function's `functions/<name>.js` is missing. The pair splits CI so the job that
|
|
247
|
+
runs package scripts never holds the credential.
|
|
248
|
+
- **Auth**: the `--token-env` variable (`OXY_TOKEN`), then the login cache — or,
|
|
249
|
+
in a GitHub Actions job with `id-token: write` and neither set, **trusted
|
|
250
|
+
publishing**: the job's OIDC token is exchanged for a credential scoped to this
|
|
251
|
+
one app. That needs the org **slug** and a publisher registered for the
|
|
252
|
+
workflow (see `init-ci`).
|
|
253
|
+
- **Provenance**: the checkout's `origin`, `HEAD` and branch (else `GITHUB_SHA` /
|
|
254
|
+
`GITHUB_REF_NAME`) are recorded; a missing half or a dirty app directory is a
|
|
255
|
+
warning, never a failure. The build id defaults to
|
|
256
|
+
`$GITHUB_SHA-$GITHUB_RUN_ID.$GITHUB_RUN_ATTEMPT`, else random — a reused one is
|
|
257
|
+
a `409`.
|
|
258
|
+
- `.env.local`, then `.env`, are read from the directory and its parents without
|
|
259
|
+
overriding the environment. The server's warnings print on stderr; `--json`
|
|
260
|
+
prints its result on stdout.
|
|
261
|
+
- **Function lint**: before the build, each declared function's source (and the
|
|
262
|
+
relative imports it reaches) is checked for what the host refuses at the first
|
|
263
|
+
call — a `ctx.*` call whose capability the manifest lacks, a global the isolate
|
|
264
|
+
does not have, a `ctx.warehouse` / `ctx.tx` write outside `destinations`; before
|
|
265
|
+
the upload, with the target's database list, an `upsert` or `ctx.tx` on an
|
|
266
|
+
engine that refuses it and a customer-warehouse write with no
|
|
267
|
+
`customerWarehouseWrites` reason. The rules are `validate`'s, below. A finding
|
|
268
|
+
fails the publish with exit `1` and names the file, line, call and fix; if the
|
|
269
|
+
target will not list the databases, the engine half prints one warning and is
|
|
270
|
+
skipped. **`--allow-function-lint`** is the way past a false positive: every
|
|
271
|
+
finding becomes a warning that names its rule — please open an issue with it.
|
|
272
|
+
|
|
273
|
+
**`init-ci`** writes `.github/workflows/oxy-publish.yml` at the repo root: a
|
|
274
|
+
`build` job (no id-token) that runs `publish --build-only`, and an
|
|
275
|
+
environment-gated `publish` job whose only work is `publish --prebuilt` with
|
|
276
|
+
OIDC. `--promote` makes that publish go live and adds a `checks run` step after
|
|
277
|
+
it, when the manifest declares a check. It prints the `oxyc api …/publishers`
|
|
278
|
+
call that registers the workflow.
|
|
279
|
+
|
|
280
|
+
## Checks
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
oxyc checks run <org-slug>/<app-slug> # or an app UUID
|
|
284
|
+
oxyc checks run acme/dashboards --json --timeout 60
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Runs every function the app declared `"check": true` (in `oxy-app.json`), one
|
|
288
|
+
POST per check to start its run, then polls each run to a terminal status —
|
|
289
|
+
`done`, `failed`, `cancelled`, or a client-side `timed_out` past `--timeout`
|
|
290
|
+
seconds (default 300, per check). A check **passes** when its run finishes
|
|
291
|
+
`done` and its answer does not parse to an object with `ok: false`.
|
|
292
|
+
|
|
293
|
+
Human mode prints one line per check to stderr as it lands (`✓ canary 4.1s` /
|
|
294
|
+
`✗ canary failed: <reason>`), suppressed by `--quiet`. `--json` prints one
|
|
295
|
+
object to stdout instead:
|
|
296
|
+
|
|
297
|
+
```json
|
|
298
|
+
{ "app": "acme/dashboards", "appId": "…", "checks": [{ "name": "canary", "runId": "…", "status": "done", "passed": true, "durationMs": 4123 }] }
|
|
299
|
+
```
|
|
137
300
|
|
|
138
|
-
|
|
301
|
+
**Auth** follows the same rule as `oxyc api`'s bearer surface, with one
|
|
302
|
+
difference: `checks run` talks to `/api/admin/**`, which is not the
|
|
303
|
+
`/external/api/*` surface, so a configured API key is sent explicitly as
|
|
304
|
+
`X-API-Key` only when **no bearer resolves** — never both. A bearer from
|
|
305
|
+
`oxyc login` (or `--token-env`) always wins when one is present.
|
|
306
|
+
|
|
307
|
+
**In CI, nothing is stored.** With neither a token nor an API key set, a job
|
|
308
|
+
holding `id-token: write` exchanges its GitHub OIDC token for the same
|
|
309
|
+
short-lived, app-scoped publish credential `oxyc publish` uses, and drives
|
|
310
|
+
`/api/customer-apps/**` instead — the same three handlers, mounted where a
|
|
311
|
+
publish token may reach them. The exchange returns the app id, so no lookup is
|
|
312
|
+
needed, and `<app>` must be `<org-slug>/<app-slug>` in this mode (a UUID names
|
|
313
|
+
an app the exchange cannot verify a publisher for). A publish token set in
|
|
314
|
+
`OXY_TOKEN` takes the same surface without minting.
|
|
315
|
+
|
|
316
|
+
What that credential may do is deliberately small, and enforced server-side
|
|
317
|
+
rather than by the client. An app-scoped token is **confined to its app**: it
|
|
318
|
+
reaches `/customer-apps/{its own id}/…` and the upload, and answers `404` for
|
|
319
|
+
everything else on that surface — another app's id, the registry listing, and
|
|
320
|
+
the fleet-wide rollups. That is the same answer an out-of-scope App Operator
|
|
321
|
+
gets, so it cannot learn which apps exist. Within its app it may publish, read,
|
|
322
|
+
and run a function **the manifest marks `"check": true`**; a function without
|
|
323
|
+
the flag is `403`. The
|
|
324
|
+
argument for allowing the run at all is narrow: a token that may publish
|
|
325
|
+
arbitrary code to an app can already cause anything that app can, so running
|
|
326
|
+
that app's own declared checks adds nothing. It would stop being true the moment
|
|
327
|
+
it admitted any function.
|
|
328
|
+
|
|
329
|
+
**A check runs against the app's live build** — the server resolves
|
|
330
|
+
`published_build_id`, else `draft_build_id`, for the pre-flight lookup and for
|
|
331
|
+
the execution alike. So `checks run` after a plain `oxyc publish` verifies the
|
|
332
|
+
*previously promoted* code, not the draft just uploaded. `oxyc init-ci` writes
|
|
333
|
+
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.
|
|
335
|
+
|
|
336
|
+
With a publish token, `<app>` must be the **UUID** — resolving a slug means
|
|
337
|
+
listing every app, which such a token may not do. The OIDC path needs no id at
|
|
338
|
+
all (the exchange returns it) but does need `<org-slug>/<app-slug>`, since the
|
|
339
|
+
exchange is registered by slug.
|
|
340
|
+
|
|
341
|
+
**Exit codes:** `0` every check passed; `9` (`CHECK_FAILED`) at least one check
|
|
342
|
+
failed or timed out; `1` the app declares no checks; `5` the app was not
|
|
343
|
+
found; `4` no credential resolved, or the API rejected it (401/403 — an
|
|
344
|
+
expired 90-day API key reads this way, not as a missing one); `2` a bad
|
|
345
|
+
`--timeout`, or a slug where the credential needs a UUID; `7` (`UNAVAILABLE`)
|
|
346
|
+
the deployment answered the OIDC exchange without an `app_id`, meaning it
|
|
347
|
+
predates trusted checks.
|
|
348
|
+
|
|
349
|
+
## Development commands
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
oxyc validate [-f <file>] [--json]
|
|
353
|
+
oxyc mcp
|
|
354
|
+
oxyc guide
|
|
355
|
+
oxyc skills install | list
|
|
356
|
+
oxyc cache clear
|
|
357
|
+
oxyc exit-codes
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**`validate`** checks workspace YAML against `json-schemas/*.json`, which are
|
|
361
|
+
generated from the Rust config types. No network, no token.
|
|
362
|
+
|
|
363
|
+
| File | Schema |
|
|
139
364
|
| --- | --- |
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
143
|
-
| `
|
|
144
|
-
| `
|
|
145
|
-
|
|
365
|
+
| `config.yml` / `config.yaml` | `config.json` |
|
|
366
|
+
| `*.automation.yml`, `*.procedure.yml`, `*.workflow.yml` | `workflow.json` |
|
|
367
|
+
| `*.agentic.yml` | `agentic.json` |
|
|
368
|
+
| `*.app.yml` | `app.json` |
|
|
369
|
+
| `*.agent.test.yml` | `agent-test.json` |
|
|
370
|
+
|
|
371
|
+
Structural checks only. `oxy validate` additionally resolves `databases:` and
|
|
372
|
+
`llm.ref` against the loaded workspace, and wins where the two disagree.
|
|
373
|
+
|
|
374
|
+
Every **`oxy-app.json`** it finds is checked for data placement — the shape of
|
|
375
|
+
the data picks the store: facts and history in Airhouse (`ctx.airhouse`),
|
|
376
|
+
records in OLTP (`ctx.oltp`), files in `ctx.storage`, customer warehouses
|
|
377
|
+
read-only. These fail the run:
|
|
378
|
+
|
|
379
|
+
- `customerWarehouseWrites` that is not `{ "<database>": "<reason>" }`, or names
|
|
380
|
+
a database missing from the function's `destinations`;
|
|
381
|
+
- `"airhouse": { "enabled": true }` in an app whose slug cannot derive a writer
|
|
382
|
+
(`-` → `_`, 1–56 of `[a-z0-9_]`, starting with a letter; no `_` in the slug);
|
|
383
|
+
- `airhouseMigrations` without a `dir`, with one that does not exist, or with
|
|
384
|
+
`migrations.dir`'s; and in its `*.sql`, `PRIMARY KEY`, `UNIQUE`,
|
|
385
|
+
`CREATE INDEX`, `REFERENCES` or `FOREIGN KEY` (DuckLake has none), or a
|
|
386
|
+
`CREATE`/`ALTER`/`DROP`/`INSERT`/`UPDATE`/`DELETE`/`COMMENT ON` target not
|
|
387
|
+
written as `app_<writer>.<name>`.
|
|
388
|
+
|
|
389
|
+
Warnings print on stderr and do not fail: each customer-warehouse exception,
|
|
390
|
+
`CREATE SCHEMA` in a migration, and `ctx.secrets.set` holding state (a
|
|
391
|
+
`JSON.stringify` value, or a state-like key that is not a credential's). The SQL
|
|
392
|
+
checks are lexical; the server's parser is the authority.
|
|
393
|
+
|
|
394
|
+
Each declared **Oxy Function** is then linted — its entry and every relative
|
|
395
|
+
import it reaches, comments and strings masked — for the mistakes that work
|
|
396
|
+
locally and fail closed in production. These fail the run, each naming the
|
|
397
|
+
file, the line, the call and the fix:
|
|
398
|
+
|
|
399
|
+
- `capability` — a `ctx.<area>.<op>` call whose manifest capability the function
|
|
400
|
+
does not declare: `ctx.secrets.set` → `secrets.write`, `ctx.email.send` →
|
|
401
|
+
`email.send`, `ctx.org.*` → `org.read`, `ctx.storage.*` → `storage.read` /
|
|
402
|
+
`storage.write` (`copy` needs both), `ctx.oltp.*` → `oltp.enabled`,
|
|
403
|
+
`ctx.airhouse.*` → `airhouse.enabled`. The map is
|
|
404
|
+
`src/publish/capabilities.ts`, held to the host's `FunctionCapabilities` by a
|
|
405
|
+
Rust test.
|
|
406
|
+
- `isolate-global` — a value use of `Buffer`, `TextEncoder`, `TextDecoder`,
|
|
407
|
+
`Blob`, `File`, `FormData`, `crypto.subtle` or `process.*`: the isolate is bare
|
|
408
|
+
`deno_core` and each is a `ReferenceError` at runtime. A name the file declares
|
|
409
|
+
itself (`const Buffer = …`, `import { TextEncoder } from "./polyfill"`) is not
|
|
410
|
+
one; nor is a `typeof` guard or a type annotation.
|
|
411
|
+
- `destinations` — a `ctx.warehouse.insert` / `exec` / `upsert` or `ctx.tx` in a
|
|
412
|
+
function with no `destinations`, or naming a database not in them.
|
|
413
|
+
|
|
414
|
+
The engine half — `upsert` off Postgres / DuckDB, `ctx.tx` off Postgres, a
|
|
415
|
+
customer-warehouse write with no `customerWarehouseWrites` reason, a write to
|
|
416
|
+
the org's OLTP store — needs the database list, which only the server has;
|
|
417
|
+
`validate` says so and `oxyc publish` runs it before the upload.
|
|
418
|
+
|
|
419
|
+
**`proxy`** forwards a local dev server's Oxy calls to a cloud target with your
|
|
420
|
+
login token attached. Defaults: side-effecting calls are **held**, tracking
|
|
421
|
+
events are **dropped**, auth endpoints reach the backend unauthenticated so
|
|
422
|
+
sign-in works, and the cached token never overrides a real browser session. A
|
|
423
|
+
production target (any host under `oxygen-hq.com`) is refused without `--yes`.
|
|
424
|
+
Each Oxy Function call carries a W3C trace — the SDK's, or one minted here — and
|
|
425
|
+
prints `↳ <status> fn <name> request_id=… trace_id=…`.
|
|
426
|
+
|
|
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.
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
claude mcp add oxyc -- npx -y @oxy-hq/cli mcp --env production
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**`guide`** prints a page to paste into `AGENTS.md` / `CLAUDE.md`.
|
|
436
|
+
**`skills install`** symlinks the six bundled Claude skills into
|
|
437
|
+
`~/.claude/skills`; it refuses to run from an `npx` cache, whose symlinks dangle
|
|
438
|
+
once npm reclaims it.
|
|
146
439
|
|
|
147
|
-
##
|
|
440
|
+
## Exit codes
|
|
441
|
+
|
|
442
|
+
```
|
|
443
|
+
0 success
|
|
444
|
+
1 failure with nothing more specific to say
|
|
445
|
+
2 usage error — a bad flag, a missing argument, a malformed value
|
|
446
|
+
4 not authenticated, or the token was rejected (401/403)
|
|
447
|
+
5 not found (404), or an unknown customer
|
|
448
|
+
6 the request was malformed (4xx other than 401/403/404)
|
|
449
|
+
7 unavailable — 5xx, a timeout, or the network failed. Retryable.
|
|
450
|
+
8 refused — the operation would have destroyed or overwritten something
|
|
451
|
+
9 a check ran and failed or timed out (`oxyc checks run`)
|
|
452
|
+
```
|
|
148
453
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
454
|
+
`4` almost always means the wrong `--env`. `7` is worth retrying; `6` never is.
|
|
455
|
+
|
|
456
|
+
## Environment variables
|
|
457
|
+
|
|
458
|
+
| Variable | Default | Meaning |
|
|
459
|
+
| --- | --- | --- |
|
|
460
|
+
| `OXY_TOKEN` | — | bearer token, overriding the login cache (the CI path) |
|
|
461
|
+
| `OXY_API_KEY` | — | key for the `/external/api` surface |
|
|
462
|
+
| `OXY_CREDENTIALS_PATH` | OS config dir | the shared `credentials.json` |
|
|
463
|
+
| `OXYC_ORG` | `oxy-hq` | GitHub org the customer repos live in |
|
|
464
|
+
| `OXYC_CUSTOMER_TOPIC` | `oxy-customer` | topic that registers a customer repo |
|
|
465
|
+
| `OXYC_DOSSIER_ROOT` | `~/.oxyc/dossiers` | where customer clones go |
|
|
466
|
+
| `OXYC_REPO_ROOTS` | scanned | where to look for our own checkouts |
|
|
467
|
+
| `OXYC_TEMPLATE_DIR` | shipped `template/` | use a working copy instead |
|
|
468
|
+
| `OXYC_SCHEMAS_DIR` | shipped `json-schemas/` | use a working copy instead |
|
|
469
|
+
| `OXYC_SKILLS_DIR` | shipped `skills/` | use a working copy instead |
|
|
470
|
+
| `OXYC_SKILLS_TARGET` | `~/.claude/skills` | where `skills install` links |
|
|
471
|
+
| `OXYC_CACHE_DIR` | `<cache>/oxyc` | cache root |
|
|
472
|
+
| `OXYC_CACHE_TTL` | `3600` | seconds the customer listing is cached |
|
|
473
|
+
| `OXYC_LIST_LIMIT` | `1000` | max repos listed from GitHub |
|
|
474
|
+
| `OXYC_SEARCH_LIMIT` | `1000` | max pull requests searched by `activity` |
|
|
475
|
+
| `OXYC_DIFF_LINES` | — | cap on diff lines printed by `update` |
|
|
476
|
+
| `OXYC_DEBUG` | — | `1` keeps the stack on an unexpected throw |
|
|
477
|
+
| `OXYC_QUIET` | — | `1` is `--quiet` |
|
|
478
|
+
| `OXYC_DRY_RUN` | — | `1` makes `launch` print the command instead of running it |
|
|
479
|
+
| `NO_COLOR` / `FORCE_COLOR` | — | force plain / coloured output |
|
|
480
|
+
|
|
481
|
+
## Output contract
|
|
482
|
+
|
|
483
|
+
**stdout carries the response body and nothing else.** Progress, warnings,
|
|
484
|
+
errors and hints go to stderr, and a failure never exits `0`.
|
|
485
|
+
|
|
486
|
+
Attached to a TTY you get colour, aligned tables and a searchable picker.
|
|
487
|
+
Piped, you get markdown tables and no colour. Raw `api` response bodies are
|
|
488
|
+
byte-identical in both, so `| jq` always works.
|
|
153
489
|
|
|
154
|
-
|
|
155
|
-
response body and nothing else.
|
|
490
|
+
## Development
|
|
156
491
|
|
|
157
|
-
|
|
492
|
+
```bash
|
|
493
|
+
pnpm install
|
|
494
|
+
pnpm build # tsdown; `prebuild` runs codegen
|
|
495
|
+
pnpm test # vitest; `pretest` builds
|
|
496
|
+
pnpm typecheck
|
|
497
|
+
pnpm lint # biome
|
|
498
|
+
pnpm build:binary # standalone executables (needs bun)
|
|
499
|
+
```
|
|
158
500
|
|
|
159
|
-
|
|
160
|
-
`
|
|
161
|
-
|
|
501
|
+
The package ships four directories — `dist`, `json-schemas`, `skills`,
|
|
502
|
+
`template` — and the last three are resolved at runtime relative to
|
|
503
|
+
`package.json`. `scripts/ci/verify-cli-package.mjs` checks the packed tarball
|
|
504
|
+
on every PR.
|
|
162
505
|
|
|
163
|
-
Maintainer's notes
|
|
506
|
+
Maintainer's notes — release process, catalog generation, OpenAPI curation, CI
|
|
507
|
+
gates: [`internal-docs/oxy-api-cli.md`](../../internal-docs/oxy-api-cli.md).
|