@oxy-hq/cli 0.1.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.
Files changed (39) hide show
  1. package/README.md +163 -0
  2. package/dist/main.mjs +6688 -0
  3. package/dist/main.mjs.map +1 -0
  4. package/dist/mcp-VdontkUr.mjs +316 -0
  5. package/dist/mcp-VdontkUr.mjs.map +1 -0
  6. package/json-schemas/agent-test.json +104 -0
  7. package/json-schemas/agentic.json +406 -0
  8. package/json-schemas/app.json +1884 -0
  9. package/json-schemas/config.json +2215 -0
  10. package/json-schemas/flow-test.json +124 -0
  11. package/json-schemas/global-semantics.json +782 -0
  12. package/json-schemas/workflow.json +1907 -0
  13. package/package.json +65 -0
  14. package/skills/oxy-airway-pipelines/SKILL.md +338 -0
  15. package/skills/oxy-automations/SKILL.md +67 -0
  16. package/skills/oxy-cli/SKILL.md +134 -0
  17. package/skills/oxy-custom-apps/SKILL.md +330 -0
  18. package/skills/oxy-data-architecture/SKILL.md +225 -0
  19. package/skills/oxy-semantic-modeling/SKILL.md +216 -0
  20. package/template/.gitattributes +2 -0
  21. package/template/.github/scripts/ci-publish.sh +77 -0
  22. package/template/.github/scripts/discover-apps.sh +175 -0
  23. package/template/.github/scripts/publish-env.sh +55 -0
  24. package/template/.github/workflows/publish.yaml +653 -0
  25. package/template/.github/workflows/validate.yaml +264 -0
  26. package/template/.nvmrc +1 -0
  27. package/template/.oxyc-managed +297 -0
  28. package/template/README.md +414 -0
  29. package/template/_gitignore +32 -0
  30. package/template/apps/.gitkeep +0 -0
  31. package/template/config.yml +53 -0
  32. package/template/memory/.gitkeep +0 -0
  33. package/template/package.json +17 -0
  34. package/template/pipelines/.gitkeep +0 -0
  35. package/template/pnpm-workspace.yaml +32 -0
  36. package/template/scripts/dev.sh +443 -0
  37. package/template/semantics/topics/.gitkeep +0 -0
  38. package/template/semantics/views/.gitkeep +0 -0
  39. package/template/workflows/.gitkeep +0 -0
@@ -0,0 +1,443 @@
1
+ #!/usr/bin/env bash
2
+ # Run one of this repo's custom apps on your machine, against a cloud oxy's
3
+ # real data.
4
+ #
5
+ # It is two processes, and neither is guessable from the outside — which is
6
+ # why this file exists at all. Before it, running an app locally needed three
7
+ # commands in two terminals and knowledge that was written down nowhere:
8
+ #
9
+ # oxy proxy --env <env> background, listens on :3000. Forwards the app's
10
+ # /api calls to the cloud, signed with the token
11
+ # `oxy login --env <env>` cached.
12
+ # pnpm dev (in the app) foreground, Vite on :5173, which proxies /api to
13
+ # :3000. That target is the Vite plugin's default,
14
+ # so the proxy's port is not really negotiable —
15
+ # hence no --port flag here.
16
+ #
17
+ # Started in that order and torn down together. The proxy is killed on EXIT,
18
+ # INT and TERM, because a proxy left holding :3000 after a Ctrl-C makes the
19
+ # NEXT run fail with a port conflict that names nothing anyone remembers.
20
+ #
21
+ # Usage: pnpm dev --env <env> [app] [--allow-writes] [--allow-events] [--yes]
22
+
23
+ set -euo pipefail
24
+
25
+ root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
26
+
27
+ # Discovery is NOT reimplemented here.
28
+ #
29
+ # `.github/scripts/discover-apps.sh` is this repo's one answer to "which apps
30
+ # does it hold, and where does each build land", and publish.yaml calls the
31
+ # same file — so a dev run and a publish can never disagree about what counts
32
+ # as an app. Calling a script out of .github/ from a dev script reads oddly,
33
+ # and the tidier-looking fix (move it to scripts/, repoint the workflow) trades
34
+ # that oddness for a workflow whose script path is wrong on the first push.
35
+ # The oddness is cheaper. It lives there because that is where the workflow
36
+ # that also uses it looks for it.
37
+ DISCOVER="$root/.github/scripts/discover-apps.sh"
38
+
39
+ # `oxy proxy`'s own default, and what @oxy-hq/vite-plugin proxies /api to.
40
+ PROXY_PORT=3000
41
+ # Vite's default. Printed, never bound by this script.
42
+ DEV_PORT=5173
43
+
44
+ # --- output -----------------------------------------------------------------
45
+
46
+ # Every line this script writes goes through one of these, so a reader can
47
+ # always tell our diagnostics from the two child processes' own output.
48
+ say() { printf '%s\n' "$@"; }
49
+ die() {
50
+ printf 'dev: %s\n' "$1" >&2
51
+ shift
52
+ if [ "$#" -gt 0 ]; then printf '%s\n' "$@" >&2; fi
53
+ exit 1
54
+ }
55
+
56
+ usage() {
57
+ cat <<EOF
58
+ Run one of this repo's custom apps locally, against a cloud oxy's real data.
59
+
60
+ pnpm dev --env <env> [app] [--allow-writes] [--allow-events] [--yes]
61
+
62
+ --env <env> REQUIRED. Which oxy to proxy to. There is deliberately no
63
+ default: \`oxy proxy\`'s own default is production, and a
64
+ \`pnpm dev\` that silently reads production is not something
65
+ this repo will do. One of:
66
+ dev staging production
67
+ a full URL (--env https://acme.oxygen-hq.com), or any name
68
+ the app's own oxy-app.json declares under "environments".
69
+ [app] Which app to run, when this repo holds more than one. Its
70
+ directory (apps/acme/reports) or just its name (reports).
71
+ --allow-writes Forward side-effecting calls instead of holding them.
72
+ --allow-events Forward tracking events instead of dropping them.
73
+ --yes Confirm that --env production is really what you meant.
74
+ -h, --help This text.
75
+
76
+ First run, once per env: oxy login --env <env>
77
+ EOF
78
+ }
79
+
80
+ # --- arguments --------------------------------------------------------------
81
+
82
+ env_name=""
83
+ app_arg=""
84
+ allow_writes=0
85
+ allow_events=0
86
+ confirm_yes=0
87
+
88
+ while [ "$#" -gt 0 ]; do
89
+ case "$1" in
90
+ --env)
91
+ if [ "$#" -lt 2 ] || [ -z "$2" ]; then
92
+ die "--env needs a value." "Run 'pnpm dev --help' for the choices."
93
+ fi
94
+ env_name="$2"
95
+ shift 2
96
+ ;;
97
+ --env=*)
98
+ env_name="${1#--env=}"
99
+ if [ -z "$env_name" ]; then
100
+ die "--env needs a value." "Run 'pnpm dev --help' for the choices."
101
+ fi
102
+ shift
103
+ ;;
104
+ --allow-writes) allow_writes=1; shift ;;
105
+ --allow-events) allow_events=1; shift ;;
106
+ --yes|-y) confirm_yes=1; shift ;;
107
+ -h|--help) usage; exit 0 ;;
108
+ --) shift ;;
109
+ -*)
110
+ die "unknown option: $1" "Run 'pnpm dev --help' to see what this accepts."
111
+ ;;
112
+ *)
113
+ if [ -n "$app_arg" ]; then
114
+ die "more than one app named: '$app_arg' and '$1'." \
115
+ "This runs one app at a time."
116
+ fi
117
+ app_arg="$1"
118
+ shift
119
+ ;;
120
+ esac
121
+ done
122
+
123
+ # --- teardown ---------------------------------------------------------------
124
+ #
125
+ # Armed BEFORE anything is started, so a temp directory is never left behind
126
+ # by an early exit either. `proxy_pid` stays empty until there is something to
127
+ # kill, and cleanup is idempotent — the INT and TERM handlers exit, which runs
128
+ # the EXIT handler on top of them.
129
+
130
+ tmpdir="$(mktemp -d)"
131
+ proxy_pid=""
132
+
133
+ # Has this child finished? An empty state means it is gone from the process
134
+ # table; `Z` means it exited and is waiting for this shell to reap it. `kill -0`
135
+ # can tell neither from a live process — it succeeds on a zombie — which is why
136
+ # this asks the table instead.
137
+ process_finished() {
138
+ local state
139
+ state="$(ps -o state= -p "$1" 2>/dev/null || true)"
140
+ state="${state// /}"
141
+ case "$state" in
142
+ ""|Z*) return 0 ;;
143
+ esac
144
+ return 1
145
+ }
146
+
147
+ cleanup() {
148
+ if [ -n "$proxy_pid" ]; then
149
+ # The proxy is the whole reason this trap exists. TERM, then reap it, so
150
+ # :3000 is actually released before this shell returns to the prompt.
151
+ kill "$proxy_pid" 2>/dev/null || true
152
+
153
+ # Bounded, and the bound is the point. A bare `wait` here is unbounded, so
154
+ # a proxy that ignores SIGTERM holds the terminal forever — which is WORSE
155
+ # than leaking one: the person is now stuck in their shell rather than
156
+ # merely running a stale process they can kill later. Ask for ~5s, then
157
+ # stop asking.
158
+ local ticks=0
159
+ while [ "$ticks" -lt 50 ] && ! process_finished "$proxy_pid"; do
160
+ sleep 0.1
161
+ ticks=$((ticks + 1))
162
+ done
163
+ if ! process_finished "$proxy_pid"; then
164
+ printf 'dev: the proxy (pid %s) ignored SIGTERM for 5s; sending SIGKILL.\n' \
165
+ "$proxy_pid" >&2
166
+ kill -9 "$proxy_pid" 2>/dev/null || true
167
+ fi
168
+
169
+ wait "$proxy_pid" 2>/dev/null || true
170
+ proxy_pid=""
171
+ fi
172
+ rm -rf "$tmpdir"
173
+ }
174
+
175
+ # The EXIT handler is spelled bare rather than quoted so that shellcheck can
176
+ # see cleanup is reachable (SC2329 otherwise reports it as dead code, and this
177
+ # suite treats a shellcheck finding as a failure). The signal handlers must
178
+ # exit as well as clean up: a returning INT handler would drop the shell back
179
+ # where it was, with the proxy already dead.
180
+ #
181
+ # What each of the three actually buys, measured rather than assumed — bash 3.2,
182
+ # signal delivered while this script sits in the liveness `sleep` below:
183
+ #
184
+ # EXIT does the work on every path tried, INCLUDING death by an untrapped
185
+ # SIGTERM: bash runs it there too. It is the one that matters.
186
+ # INT load-bearing, and not for the reason it looks. A job started in the
187
+ # background inherits SIGINT set to IGNORE, so WITHOUT this line a
188
+ # SIGINT aimed at this script alone is discarded — the script sails on
189
+ # and starts the dev server the person just tried to stop (measured:
190
+ # exit 0, no teardown). Installing the trap un-ignores the signal.
191
+ # TERM defensive, and honestly so: every window tried already produced 143
192
+ # and a clean teardown through EXIT. It is kept because it makes the
193
+ # status explicit instead of leaning on bash running an EXIT handler
194
+ # for a fatal signal, which is not a portable guarantee — but there is
195
+ # no test for it, because none could fail.
196
+ trap cleanup EXIT
197
+ trap 'cleanup; exit 130' INT
198
+ trap 'cleanup; exit 143' TERM
199
+
200
+ # --- which apps does this repo hold? ----------------------------------------
201
+
202
+ if [ ! -f "$DISCOVER" ]; then
203
+ die "this repo has no $DISCOVER." \
204
+ "That script is what both this and .github/workflows/publish.yaml use to" \
205
+ "find apps. Restore it from the customer-tooling template."
206
+ fi
207
+
208
+ discover_status=0
209
+ bash "$DISCOVER" "$root" >"$tmpdir/apps" 2>"$tmpdir/discover.err" || discover_status=$?
210
+
211
+ if [ "$discover_status" -ne 0 ]; then
212
+ cat "$tmpdir/discover.err" >&2
213
+ die "could not work out which apps this repo holds — see above."
214
+ fi
215
+
216
+ # discover-apps.sh speaks GitHub Actions. On the zero-app path it prints
217
+ # "publish: no custom apps under apps/ ...", which is the wrong words for a
218
+ # `pnpm dev` — this script says its own thing below. Everything else it puts
219
+ # on stderr is a real diagnostic (an app directory named `dist` that was
220
+ # skipped, say) and has to survive.
221
+ grep_status=0
222
+ grep -v '^publish: ' "$tmpdir/discover.err" >&2 || grep_status=$?
223
+ # grep exits 1 when it selects NO lines, which is the ordinary answer here:
224
+ # the only line may be the one just filtered out, or there may have been no
225
+ # stderr at all. Exactly that status is tolerated. Anything above it is grep
226
+ # itself failing, and swallowing that would hide a real diagnostic — the
227
+ # failure mode a bare `|| true` produces.
228
+ if [ "$grep_status" -gt 1 ]; then
229
+ die "could not read the discovery diagnostics (grep exited $grep_status)."
230
+ fi
231
+
232
+ app_dirs=()
233
+ while IFS=$'\t' read -r dir _; do
234
+ [ -n "$dir" ] || continue
235
+ app_dirs+=("$dir")
236
+ done < "$tmpdir/apps"
237
+
238
+ # Zero apps is checked FIRST, ahead of --env and ahead of every tool check: a
239
+ # freshly scaffolded repo has none, someone will type `pnpm dev` in it on day
240
+ # one, and "here is how to add an app" is a more useful answer than a lecture
241
+ # about a flag they have nothing to use it on. Exit 0 — day one is green.
242
+ if [ "${#app_dirs[@]}" -eq 0 ]; then
243
+ say "" \
244
+ "dev: this repo has no custom apps yet, so there is nothing to run." \
245
+ "" \
246
+ "An app is a React + Vite bundle in its own directory under apps/:" \
247
+ "" \
248
+ " apps/<org>/<app>/ e.g. apps/acme/sales-dashboard/" \
249
+ " oxy-app.json \"slug\" and \"orgSlug\" — the app's identity" \
250
+ " package.json with a \"dev\" script (Vite)" \
251
+ " src/…" \
252
+ "" \
253
+ "Scaffold one, from the repo root:" \
254
+ "" \
255
+ " pnpm dlx @oxy-hq/create-oxy-app apps/<org>/<app>" \
256
+ " pnpm install # writes pnpm-lock.yaml — commit it" \
257
+ " pnpm dev --env dev" \
258
+ ""
259
+ exit 0
260
+ fi
261
+
262
+ # --- which one? -------------------------------------------------------------
263
+
264
+ list_apps() {
265
+ local d
266
+ for d in "${app_dirs[@]}"; do
267
+ printf ' %s\n' "$d"
268
+ done
269
+ }
270
+
271
+ app_dir=""
272
+ if [ -n "$app_arg" ]; then
273
+ # Tab completion adds the trailing slash, and typing the whole path is the
274
+ # obvious thing to do; so is typing just the name.
275
+ want="${app_arg%/}"
276
+ matches=()
277
+ for d in "${app_dirs[@]}"; do
278
+ if [ "$want" = "$d" ] || [ "$want" = "${d#apps/}" ] || [ "$want" = "${d##*/}" ]; then
279
+ matches+=("$d")
280
+ fi
281
+ done
282
+ if [ "${#matches[@]}" -eq 0 ]; then
283
+ die "no app called '$app_arg' in this repo. It holds:" "$(list_apps)"
284
+ fi
285
+ if [ "${#matches[@]}" -gt 1 ]; then
286
+ printf "dev: '%s' matches more than one app — name one by its directory:\n" "$app_arg" >&2
287
+ printf ' %s\n' "${matches[@]}" >&2
288
+ exit 1
289
+ fi
290
+ app_dir="${matches[0]}"
291
+ elif [ "${#app_dirs[@]}" -eq 1 ]; then
292
+ app_dir="${app_dirs[0]}"
293
+ else
294
+ # Refusing rather than picking the first: which app you get would then depend
295
+ # on sort order, and the wrong one starts silently and looks like the right
296
+ # one until you notice the data.
297
+ printf 'dev: this repo holds %s apps — name the one to run:\n' "${#app_dirs[@]}" >&2
298
+ list_apps >&2
299
+ printf '\n pnpm dev --env dev %s\n' "${app_dirs[0]##*/}" >&2
300
+ exit 1
301
+ fi
302
+
303
+ if [ ! -f "$root/$app_dir/package.json" ]; then
304
+ die "$app_dir has an oxy-app.json but no package.json, so there is no dev server to start."
305
+ fi
306
+
307
+ # --- which oxy? -------------------------------------------------------------
308
+
309
+ if [ -z "$env_name" ]; then
310
+ die "--env is required." \
311
+ "" \
312
+ " pnpm dev --env dev ${app_dir##*/}" \
313
+ "" \
314
+ "It says which cloud oxy the proxy reads from: dev, staging, production," \
315
+ "a full URL (--env https://acme.oxygen-hq.com), or a name the app's" \
316
+ "oxy-app.json declares under \"environments\"." \
317
+ "" \
318
+ "There is no default on purpose. 'oxy proxy' defaults to PRODUCTION, and" \
319
+ "a 'pnpm dev' that silently serves production data is not a thing this" \
320
+ "repo will do quietly."
321
+ fi
322
+
323
+ if [ "$env_name" = "production" ] && [ "$confirm_yes" -ne 1 ]; then
324
+ # `oxy proxy` refuses a production target without --yes on its own. Catching
325
+ # it here matters because the proxy runs in the BACKGROUND: its refusal (or
326
+ # its prompt) would scroll past under Vite's banner, and the app would just
327
+ # fail every request with nothing on screen explaining why.
328
+ die "--env production is real customer data." \
329
+ "" \
330
+ " pnpm dev --env production --yes ${app_dir##*/}" \
331
+ "" \
332
+ "Add --yes if that is genuinely what you want. For everyday work use" \
333
+ "--env dev or --env staging."
334
+ fi
335
+
336
+ # --- preconditions ----------------------------------------------------------
337
+
338
+ if ! command -v oxy >/dev/null 2>&1; then
339
+ die "the 'oxy' CLI is not on your PATH, and the proxy is oxy." \
340
+ "" \
341
+ " curl -sSfL https://get.oxy.tech | bash" \
342
+ "" \
343
+ "It installs to ~/.local/bin; add that to PATH if it is not already there."
344
+ fi
345
+
346
+ if ! command -v pnpm >/dev/null 2>&1; then
347
+ die "'pnpm' is not on your PATH, and it is what runs the app's dev server." \
348
+ "This repo is pnpm-only — never npm or yarn." \
349
+ "" \
350
+ " corepack enable pnpm"
351
+ fi
352
+
353
+ # Who, if anyone, is already listening on a port. Empty when the port is free,
354
+ # when lsof is not installed, or when lsof cannot tell — this is a diagnostic,
355
+ # and it must never be the thing that stops a run.
356
+ port_holder=""
357
+ read_port_holder() {
358
+ local out="" st=0
359
+ command -v lsof >/dev/null 2>&1 || return 0
360
+ out="$(lsof -nP -iTCP:"$1" -sTCP:LISTEN 2>/dev/null)" || st=$?
361
+ # lsof exits 1 for "nothing matched" AND for its own errors, and there is no
362
+ # way to tell them apart, so both read as "nothing to report".
363
+ if [ "$st" -ne 0 ] || [ -z "$out" ]; then
364
+ return 0
365
+ fi
366
+ port_holder="$(printf '%s\n' "$out" |
367
+ awk 'NR > 1 { print $1 " (pid " $2 ")" }' | LC_ALL=C sort -u | tr '\n' ' ')"
368
+ }
369
+
370
+ read_port_holder "$PROXY_PORT"
371
+ if [ -n "$port_holder" ]; then
372
+ die "port $PROXY_PORT is already taken by: $port_holder" \
373
+ "" \
374
+ "'oxy proxy' has to listen there — the app's dev server proxies /api to" \
375
+ "that exact port — so this cannot just move out of the way. Usually it is" \
376
+ "a proxy from an earlier run that outlived its terminal, or a local" \
377
+ "'oxy serve'. Stop it and try again:" \
378
+ "" \
379
+ " lsof -nP -iTCP:$PROXY_PORT -sTCP:LISTEN"
380
+ fi
381
+
382
+ # --- go ---------------------------------------------------------------------
383
+
384
+ proxy_args=(proxy --env "$env_name")
385
+ if [ "$allow_writes" -eq 1 ]; then proxy_args+=(--allow-writes); fi
386
+ if [ "$allow_events" -eq 1 ]; then proxy_args+=(--allow-events); fi
387
+ if [ "$confirm_yes" -eq 1 ]; then proxy_args+=(--yes); fi
388
+
389
+ say "" \
390
+ " app $app_dir" \
391
+ " env $env_name" \
392
+ " proxy oxy ${proxy_args[*]} -> http://localhost:$PROXY_PORT" \
393
+ " dev pnpm dev in $app_dir -> http://localhost:$DEV_PORT" \
394
+ ""
395
+
396
+ # Said once, up front, and not negotiable: "my function isn't running" with no
397
+ # visible cause is an hour of someone's day. The proxy holds these back so an
398
+ # afternoon of local clicking cannot change cloud state.
399
+ if [ "$allow_writes" -eq 1 ]; then
400
+ say " ! --allow-writes: side-effecting calls (/fn handlers, agent runs," \
401
+ " automation runs) are being FORWARDED to $env_name for real."
402
+ else
403
+ say " Side-effecting calls — /fn handlers, agent runs, automation runs — are" \
404
+ " HELD, not sent. A function that 'does nothing' is usually this, not a" \
405
+ " bug. Pass --allow-writes to forward them."
406
+ fi
407
+ if [ "$allow_events" -eq 1 ]; then
408
+ say " ! --allow-events: tracking events are being FORWARDED to $env_name."
409
+ else
410
+ say " Tracking events are DROPPED, so local clicking never shows up in the" \
411
+ " customer's analytics. Pass --allow-events to forward them."
412
+ fi
413
+ say "" " Ctrl-C stops both. If requests come back 401, run: oxy login --env $env_name" ""
414
+
415
+ oxy "${proxy_args[@]}" &
416
+ proxy_pid=$!
417
+
418
+ # A proxy that dies on the first second — no cached token, a bad --env — would
419
+ # otherwise leave Vite up and every request failing, with the reason scrolled
420
+ # off the top. One second is enough to catch the immediate exits and cannot
421
+ # produce a false alarm: a proxy still running after it is simply left alone.
422
+ sleep 1
423
+ if process_finished "$proxy_pid"; then
424
+ proxy_pid=""
425
+ die "'oxy proxy --env $env_name' exited immediately — see its output above." \
426
+ "" \
427
+ "Most often there is no cached token for that env yet:" \
428
+ "" \
429
+ " oxy login --env $env_name"
430
+ fi
431
+
432
+ # The dev server, in the foreground, and the LAST statement in this file.
433
+ #
434
+ # A subshell so the script keeps its own cwd for the teardown, and `exec` so
435
+ # the dev server IS that process — a wrapper in between would swallow the exit
436
+ # status a Ctrl-C produces.
437
+ #
438
+ # Deliberately not followed by `exit "$status"`. Under errexit this line's
439
+ # status is already the script's, the EXIT trap still runs and still kills the
440
+ # proxy, and a trailing `exit` costs something real: shellcheck 0.11 stops
441
+ # tracing the EXIT trap past one, and reports `cleanup` as dead code (SC2329) —
442
+ # a finding this repo's suite treats as a failure.
443
+ ( cd "$root/$app_dir" && exec pnpm dev )
File without changes
File without changes
File without changes