# OJPlay for agents

You were handed a cart on OJPlay (play.onejs.com), or asked to make one. This page is the whole loop. Every docs page is also plain Markdown; the list is at the end.

A cart is a small interactive program in React and TypeScript. Every cart is a git repository on the site, and a push to `main` builds it and makes it what runs.

## Log in

```bash
npx ojplay login
```

It prints a link. **Show it to the user**: they open it, signed in to play.onejs.com, and press Allow. The command waits (up to ten minutes) and then stores a token for this machine; git uses it for play.onejs.com from then on, so `git clone`, `pull` and `push` need nothing more. If your shell cannot wait that long, `npx ojplay login --no-wait` prints the link and exits, and `npx ojplay login --wait` collects the token once the user has pressed Allow (`--wait <code>` when more than one login is waiting on this machine).

- **What a login can do:** create carts, edit them, push and rebuild. Editing includes renaming: the name is `name` in `oj.json`. Publishing, deleting and the account settings stay with the user. They choose Private or Public, and delete, in the cart's Settings: the gear on its page or on [/manage](https://play.onejs.com/manage).
- **`main` takes fast forwards only** from a login: `git pull` first when the user may have edited in the browser.
- The Allow page shows the name the login sends, like `claude on mac`, read from your environment. `--name "<name>"` sets it.
- `npx ojplay logout` forgets the login, here and on the site. A login lasts 30 days. A refused login says why (replaced, revoked, logged out or expired); run `npx ojplay login` again.
- `OJ_TOKEN`, a token the user made at [/manage/tokens](https://play.onejs.com/manage/tokens), is used instead when set, by ojplay and by git in a clone ojplay made. Never commit a token, print it, or write it into a git config.

## The loop

```bash
npx ojplay clone <sid>           # git pull and push in it then work with the login or OJ_TOKEN
cd <sid>
npx ojplay init && npm install   # package.json, tsconfig.json, types; all gitignored
npx ojplay typecheck
npx ojplay test                  # boots it headless in the site's real container
git push origin main                  # the site builds it; the result is on the remote: lines
```

- **Run every command as `npx ojplay <command>`**, in a clone or outside one: inside a clone it runs the copy `npm install` put there. Never shorten it to `oj` after npx, which fetches an unrelated npm package, and never use onejs-play, ojplay's former name, which is frozen at an old version.
- `ojplay` needs Node 22 or newer. `run` and `test` need Chrome (`OJ_CHROME` names the binary when it is not in the usual place).
- `npx ojplay help` lists the rest: `build`, `run` (`--headed --watch` opens a window that swaps in each save), `runtime`.

## Using another cart

```bash
npx ojplay add @singtaa/lightning   # oj.json gets its newest version; its source and art go in .oj/carts
```

- Then import it: `import { Bolt } from "@singtaa/lightning"`. `add` prints what the cart exports and the import line. A cart that exports nothing is a whole cart, and `import "@singtaa/lightning"` runs it.
- In an empty folder, `add` starts a new cart that uses it: `oj.json` and an `index.tsx` that shows it. No login is needed for a public cart.
- `npx ojplay update` moves each used cart to its newest version in the same major (`--major` for the newest of all). `npx ojplay remove <address>` takes one out and names the files that still import it.
- A cart's own `exports` in `oj.json` names the file other carts import from. Its `version` is what `add` pins.

## A new cart

```bash
npx ojplay new "<name>"    # creates it from the starter, clones into ./<name>
```

Or over HTTP: `POST https://play.onejs.com/api/games` with `Authorization: Bearer <token>` and the JSON body `{"name": "<name>"}`. It answers `{"url", "sid", "clone"}`.

- **A name the account already has answers 409**, with that cart's `sid` and `url`. That cart is the user's own work: pick another name, or ask the user whether they meant it. Never push to a cart you did not make unless the user asked you to.
- **A new cart is private.** Only its owner can open it. Making it public is the user's call, in its Settings.

## The user's carts

```bash
npx ojplay list            # every cart on the account, private ones included
```

Or `GET https://play.onejs.com/api/me/carts` with the bearer token: `{"handle", "carts": [{"sid", "name", "url", "clone", "public", "head", "live", "buildError"}]}`.

- **Use this to answer "which carts do I have".** `GET /api/games` lists only public carts, from everyone, and misses every private one.

## What a push says

The site builds the pushed commit and answers on the push's own `remote:` lines.

```text
remote: built 3f2a91c: 2 files, 41 KB
remote: live at https://play.onejs.com/@<handle>/<name>
```

```text
remote: 3f2a91c did not build: index.tsx:12:5: Expected ";" but found "x"
remote: still running 9b04d1e
```

- **A push that does not build is still kept.** The last commit that built keeps running. Fix it and push again.
- `npx ojplay push` is `git push origin main` that exits 1 when the tip did not build, or when nothing was pushed because the changes are not committed.
- `npx ojplay status` says what runs and why the tip of `main` differs, in sentences. `npx ojplay status --json`, or `GET https://play.onejs.com/api/games/<sid>` with the bearer token, answers `head` (the tip of `main`), `live` (what runs), `buildError` (why `head` is not `live`, or null), `url` and `public`.

## Rules

- **Work on `main`.** The user's edits in the browser are a draft only they see, and reach `main` when they press Push, so `git pull` before you push when they may have pushed. Your push never loses their unpushed edits.
- **Never force-push.** A login cannot, and with a token the site accepts one, which throws away what the user pushed from the browser.
- **Check before you push.** `npx ojplay typecheck` checks by the Play editor's rules, and it and `npx ojplay test` catch what the site's build would reject, and the console errors it would not.
- **The user's open cart page swaps in each build as it goes live.** If they have edits they have not pushed, those stay, and their Push lists yours as coming in and asks about any file you both changed. `git pull` before your next push.
- **A cart is `index.tsx` and `oj.json`**, plus any files it imports or loads. What `init` writes stays out of git.

## Writing a cart

Everything comes from `"oj"`, and `mount()` renders the root.

```tsx
import { useRef } from "react"
import { View, Text, mount, useFrame, useStage, input, type VisualElement } from "oj"

function Game() {
    const stage = useStage()                  // { width, height }, the window in pixels
    const x = useRef(100)
    const box = useRef<VisualElement>(null)   // the element, once it mounts
    useFrame((dt) => {                        // every frame; dt in seconds
        x.current += input.keyboard.arrows().x * 300 * dt
        if (box.current) box.current.style.left = x.current   // move by style, not by React state
    })
    return (
        <View style={{ flexGrow: 1, backgroundColor: "#14181d" }}>
            <View ref={box} style={{ position: "absolute", top: stage.height / 2, width: 40, height: 40, backgroundColor: "#4ade80" }} />
            <Text style={{ color: "white", fontSize: 24, unityFontStyleAndWeight: "bold" }}>Score 0</Text>
        </View>
    )
}

mount(<Game />)
```

- **Keys are Unity's names:** `"LeftArrow"`, `"A"`, `"Space"`, `"Enter"`. `input.keyboard.isKeyDown(name)` while held, `wasKeyPressed(name)` and `wasKeyReleased(name)` for one frame, `keysPressed` for every key that went down this frame in the order it was typed (read it for text, since a fast typist puts several letters in one frame), `arrows()` and `wasd()` as a vector whose y is +1 for up, the opposite of the stage's y.
- **The mouse and touches:** `input.mouse.position`, `input.touches`, in window pixels. `useSwipe((direction) => ...)` for swipes.
- **Positive y is down.** The origin is the top left of the stage.
- **Styles are objects.** Numbers are pixels, colours are strings, layout is flexbox only. Bold is `unityFontStyleAndWeight: "bold"`, lower case; there is no `fontWeight`.
- **Per frame motion goes through a ref's `style`,** not `setState`. Re-render for things that change rarely, like a score.
- **Drawing:** `useDrawing(ref, (p) => p.fillColor("#fd0").beginPath().circle(x, y, r).fill(), "frame")` draws into a View and redraws every frame; pass deps instead of `"frame"` to redraw only when they change.
- **3D:** one `useScene()` per cart, near the top, and `useModel("ghost.glb")`, then `scene.spawn(model, { position, play })`. A scene with no options has a camera, a sun with soft shadows and ambient light; options only tune them, and `scene.sun()`, `scene.fog()` and friends change them live. An actor has `position`, `rotation`, `scale`, `speed` and `dissolve({ to, seconds })`; `scene.paused` holds them all. `scene.pick(e.x, e.y)` and `actor.screenPoint()` for clicks and labels. Details in [3D](https://play.onejs.com/docs/3d.md).
- **Files** (png, jpg, sounds, fonts, glb) sit in the repository and load by their path from the root: `useTexture("art/coin.png")` and `useSound("pop.wav")` (each null until it arrives), `assetUrl("logo.png")`. Never under a top level `assets/` folder.

## Testing with `npx ojplay test`

`npx ojplay test` with no script lets the cart run two seconds, then fails on a console error, an asset the site would not serve, or controls out of line. `npx ojplay test playtest.mjs` hands a script the running cart:

```js
export default async function (cart) {
    const before = await cart.read()                  // the text on screen, top to bottom
    const release = await cart.hold("ArrowRight")
    await cart.wait(500)
    await release()
    await cart.press("Space")
    await cart.until(async () => (await cart.read()).some((t) => t.includes("Score 1")), { what: "a coin" })
    await cart.shot(".oj/after.png")                  // look at it
}
```

`read()`, `find(text)` (where it is on screen, `{ x, y, width, height }`, once laid out), `click(x, y)`, `drag()`, `move(x, y)`, `press(code)`, `hold(code)` (returns a release function), `type(text)`, `wait(ms)`, `until(predicate, { timeoutMs, what })`, `stage()`, `eval(js)`, `shot(file)`, `reload()`, `errors`, `console`. Coordinates are stage pixels. A thrown error fails the run. A screenshot lands in `.oj/` either way.

## Every docs page, as Markdown

- [Start](https://play.onejs.com/docs/start.md): Open a cart, change a number, run it
- [Writing a Cart](https://play.onejs.com/docs/writing-a-cart.md): The entry point, the frame loop, and the manifest
- [Elements and Styling](https://play.onejs.com/docs/elements-and-styling.md): The elements a cart draws with, how styling works, and the traps in it
- [The Stage](https://play.onejs.com/docs/stage.md): The window your cart gets, and how to fit a fixed board inside it
- [Files](https://play.onejs.com/docs/files.md): Sprites, sounds, fonts and covers
- [Effects](https://play.onejs.com/docs/effects.md): Particles, textures and per pixel programs
- [3D](https://play.onejs.com/docs/3d.md): Models from .glb files, with a sun, shadows and lights
- [Opening in Unity](https://play.onejs.com/docs/unity.md): Add a cart to a Unity project
