Your trusty automation assistant in TLE Community misadventures!
super-ui CLAUDE.md
8.3 kB
Markdown
at main

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 #

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:

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:

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/<name>.js, the registry entry, bin/super-cli-<name>.js (a two-line runner.run('<name>') stub), and lib/web/components/task-configs/<name>.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-<task>.js entry points throw. The web path (getTaskByName) is the maintained one; use getTaskByName if you pick up CLI work.