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.tsis TypeScript run through Node's--experimental-strip-typesand 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.jsbuilds the singletonLacuna, keeps the empire/password in a module closure for re-login, and installs anonResponsehandler 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.jsby relative path because the package'sexportsmap (v1.4.0) points at files it doesn't ship. Exportslogin(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 bareresult, re-logs-in and retries once onSession expired., and otherwise throwserror.messageas a string (soutil.handlePromiseError/eachPlanetkeep 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 oldlacuna.buildings.generic(url).lacuna.server.callLegacy({ module, method, params: [...], addSession: true })is the escape hatch for RPCs with no working/v2method (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--planetargument in all its forms:'all', a name, or an array of names.eachPlanet(planets, cb, {force})— fetches each planet's buildings, skips unhappy planets unlessforce, 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.jstranslateShipTypeandpackages/core/src/constants.jsshipTypes/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.jsonformats on save: no semicolons, single quotes, 100 columns, es5 trailing commas. lib/cli/task-runner.jsdoestasks[name].run(options)butgetTasksForPlatform('cli')returns an array, so thebin/super-cli-<task>.jsentry points throw. The web path (getTaskByName) is the maintained one; usegetTaskByNameif you pick up CLI work.