# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What this is `super-ui` automates tasks in the game Lacuna Expanse. The same task implementations run in two front ends: a Commander-based CLI (`bin/`) and a Vue 3 + Vite web app (`lib/web/`). `README.md` is a changelog, not usage docs. ## Commands ```sh npm run dev # Vite dev server on port 3000 npm run build # Vite build (base is '/super-ui/') npm run docs # JSDoc from lib/ into docs/ npm run deploy # build + docs npm run prettier # format everything npm test # unit + integration ``` Unit tests are mocha 2.x. There is no `.mocharc`, so it defaults to `test/*.js`: ```sh npm run test:unit # all npm run test:unit -- test/util-test.js # one file npm run test:unit -- --grep 'commify' # one case ``` **On Windows `npm run test:unit` does not work.** The script runs `node node_modules/.bin/_mocha`, which on Windows is a `sh` wrapper, not JS, so Node throws `SyntaxError: missing ) after argument list`. Pointing Node at `node_modules/mocha/bin/_mocha` instead fixes that but breaks the `.ts` task: `extensionless` reads its `lookFor` config by walking up from `process.argv[1]`, so it finds mocha's own `package.json` first, falls back to `["js"]`, and dies with `ERR_MODULE_NOT_FOUND` on `lib/tasks/glyph-report`. Launching via `-e` keeps `argv[1]` non-absolute, so the config lookup falls back to `cwd()` and everything resolves: ```sh node --experimental-strip-types --import=extensionless/register --input-type=module \ -e "process.argv.splice(1,0,'mocha'); await import('./node_modules/mocha/bin/_mocha')" \ [test/some-test.js] [--grep 'pattern'] ``` `npm run test:integration` (`test/scripts/lacuna-test.js`) is **not hermetic** — it logs into the live server configured in `test/scripts/test-instance.js` and fails offline. Node >= 24 is required and `.npmrc` sets `engine-strict=true`, so a mismatched Node aborts installs. `@tlecommunity/*` resolves to a private Gitea registry (also in `.npmrc`), so `npm install` needs network access to it. ## Module system quirks - ESM (`"type": "module"`) with **extension-less relative imports** (`import log from '../log'`). Node needs `--import=extensionless/register` (already in the npm scripts); Vite resolves them natively. Match this style in new files. - `lib/tasks/glyph-report.ts` is TypeScript run through Node's `--experimental-strip-types` and esbuild under Vite. There is no tsconfig and no build step — type-annotation syntax only, no enums/namespaces/decorators, and no type checking happens anywhere. ## Architecture ### The task registry is the hub `lib/tasks/index.js` is the single list both front ends read. Each entry has `name`, `title`, `description`, `platforms` (`'cli'` / `'web'`), `TaskClass`, and `defaults`. Its `handleTaskRun` merges defaults into options, calls `validateOptions()` then `run()`, and owns the surrounding logging and error handling — tasks should not do that themselves. Exports `getAllTasks`, `getTasksForPlatform`, `getTaskByName`. A task is a class taking `options` in the constructor with `validateOptions()` and `run()`, both returning promises. `test/tasks-test.js` asserts this shape for every registered task, so a task that omits `validateOptions` fails the suite. Adding a task touches four places: `lib/tasks/.js`, the registry entry, `bin/super-cli-.js` (a two-line `runner.run('')` stub), and `lib/web/components/task-configs/.vue` registered in the `components` map of `lib/web/components/screens/task-configuration.vue` under the task's `name`. ### One API client: `@tlecommunity/client` Every backend call goes through `@tlecommunity/client`, re-exported from `@tlecommunity/super-core` as `lacuna`. The old hand-rolled client (`packages/core/src/lacuna/`, `emissary.serverCall`, the `apiMethods` snake_case→camelCase binding, `memory-cache`) is gone. - `packages/core/src/client.js` builds the singleton `Lacuna`, keeps the empire/password in a module closure for re-login, and installs an `onResponse` handler that reproduces the old click-limit behaviour (`Slow down` / code 1010 → wait 61s → `event.retry()`). It imports the client from `../../../node_modules/@tlecommunity/client/dist/client.js` by relative path because the package's `exports` map (v1.4.0) points at files it doesn't ship. Exports `login(config)`, `relogin()`, `logout()`. - `packages/core/src/call.js` — `call(() => lacuna.body.getStatus({ body_id }))` is the wrapper every task uses. The new client never throws: it returns `{ result, error }`. `call()` unwraps to the bare `result`, re-logs-in and retries once on `Session expired.`, and otherwise throws `error.message` as a **string** (so `util.handlePromiseError` / `eachPlanet` keep swallowing per-item failures). Pass a thunk, not a bare promise, or the session-expiry retry can't re-issue. - Endpoint methods take a **single named-object param** (`{ body_id }`, `{ building_id, ore_type }`, …) and modules are flat properties (`lacuna.spacePort`, `lacuna.archaeology`, …). `lacuna.buildingFromUrl(url)` replaces the old `lacuna.buildings.generic(url)`. `lacuna.server.callLegacy({ module, method, params: [...], addSession: true })` is the escape hatch for RPCs with no working `/v2` method (`body/view_laws`, `archaeology/get_glyphs`, `trade/get_glyph_summary`). ### Reuse these rather than reimplementing The old `lacuna.empire.*` / `lacuna.body.*` helpers are now plain functions exported from `@tlecommunity/super-core`: - `findPlanets(planet, skip)` / `findPlanet(name)` (`packages/core/src/planets.js`) — resolves the `--planet` argument in all its forms: `'all'`, a name, or an array of names. - `eachPlanet(planets, cb, {force})` — fetches each planet's buildings, skips unhappy planets unless `force`, and isolates per-planet errors so one failure doesn't kill the task. - `colonies()` / `planets()` / `stations()` / `homePlanet()` / `getAllBuildings()`. - `findBuilding(bodyIdOrBuildings, 'Space Port')` / `findBuildings(...)` / `bodyBuildings(...)` / `archGetInventory(buildingId)` (`packages/core/src/buildings.js`). - `captchaPrompt()` (`packages/core/src/captcha-prompt.js`) — no-op in the browser, stdin readline prompt in the CLI. - `packages/core/src/util.js`: `array`, `commify`, `int`, `objectToArray`, `handlePromiseError`, `regexMatch`, `isCLI` / `isWeb`, `formatServerDate`. - `packages/core/src/types.js` `translateShipType` and `packages/core/src/constants.js` `shipTypes` / `serverDateFormat`. ### Logging is the task output channel `lib/log.js` is a singleton (`error` → `silly`, level from `LOG_LEVEL`) with a `subscribe` / `unsubscribeAll` pub/sub. `lib/web/components/screens/task-runner.vue` subscribes to it to render task output in the browser, so tasks must report progress via `log.*` — `console.log` never reaches the web UI. Tabular output uses `cli-table3` rendered into `log.info(table.toString())`. ### Web app `index.html` sets `window.SUPER_UI_WEB` and loads `lib/web/index.js` → `app.js`: Vue 3 with `vue-router` hash history, flowing login → `/task-selection` → `/task-configuration/:task` → `/task-runner/:task/:config`, where `:config` is a serialized query string produced by `$(form).serialize()`. jQuery and Bootstrap 3 come from unpkg in `index.html`, not from npm. Stores in `lib/web/stores/` are hand-rolled singleton classes wrapping Vue `reactive` and the `store` localStorage shim. Captchas: `captchaPrompt()` (`packages/core/src/captcha-prompt.js`) is a no-op in the browser (the config screen's `task-configs/helpers/captcha.vue` + `stores/captcha.js` handle it) and prompts on stdin via readline in the CLI. ## Conventions and known rough edges - Prettier config is non-default and `.vscode/settings.json` formats on save: no semicolons, single quotes, 100 columns, es5 trailing commas. - `lib/cli/task-runner.js` does `tasks[name].run(options)` but `getTasksForPlatform('cli')` returns an array, so the `bin/super-cli-.js` entry points throw. The web path (`getTaskByName`) is the maintained one; use `getTaskByName` if you pick up CLI work.