cloudstudio/hq

Your infrastructure as a colony on Mars. Real data, straight off your machine.

21

stars

0

commits

TypeScript

primary language

Aug 25, 2026

updated

README

HQ

Your infrastructure as a colony on Mars.

Every project in production, every repository and every Stripe account exists as a module you can walk up to. When something falls over, you see it from the far side of the valley. The data is real: it comes off your own machine and never travels anywhere.

The same colony at 0% of the MRR goal and at 100%: bare Mars is the honest reading, the green valley is the destination

pnpm install
cp data/projects.example.json data/projects.json   # your inventory; gitignored
pnpm start       # collect, then open the colony
pnpm test        # no network

pnpm start is pnpm run collect && pnpm run dev. Node 22 or newer.

The colony draws your machine: the projects you list, the repos under ~/Sites, the gh and stripe sessions already logged in. With none of those yet, the world is a small colony over whatever it could read — every module that does appear is real.

What you're looking at

Habitat domeA project in production. Cyan is answering, magenta is a warning, a white strobe is down
GreenhouseA repository. Warnings are open issues and PRs
Ice extractorA Stripe account
WorkbenchA repo that only exists on this disk, with no remote to speak for it
Civic avenueWhat HQ knows about itself: last check per source, headline figures, power as data freshness
QuarryWork in local — the commits that haven't left your machine
Terraforming plantHow far the MRR is from the goal you set. The valley greens as that figure climbs, and at 0 the world is bare Mars, which is the honest reading
RoverDrive it with C. Boost included

Not one built volume is without data behind it. The full vocabulary — what each piece stands for and where it comes from — lives in docs/COLONIA.md; the visual inventory opens at /catalog.html.

Controls

DragPan the map
Right buttonOrbit
WheelZoom to the cursor
WASD / arrowsMove, relative to the view
Q ETurn · R Shift+F tilt · 0 or F frames the colony
Double clickTravel to that spot
CDrive the rover · Space boost · Escape let go
LEvery name at once · also the toggle beside the search field
PTable panel
GForce a collection now
Search fieldJump to any module, civic building or sector by name
Toggle beside itTurns on the name of every single thing, spaced so none covers another

There is no toolbar on purpose: the world takes the whole screen and the scheduler collects by itself. What chrome there is has a job — the search field reaches a module without hunting for it, and the activity log flies you to whatever just happened. A payment you click gets celebrated and never shown again; anything bad opens the module's card instead.

Where the data comes from

SourceEveryHow
Production60 sA real HTTP request to each domain in data/projects.json
Local repos5 minReading git off disk
GitHub15 minThe gh CLI session
Stripe60 minStripe CLI profiles, in live mode
Certificates12 hTLS handshake

Those rhythms are measured, not guessed, and the reasoning for each one is written down in src/lib/sources.ts. The important one: the limit that actually binds on Stripe is not the 100 requests per second, it's the monthly read allowance, 10,000 a month. Polling every five minutes burned 432 % of it.

A source failing never wipes its data: the last valid value is kept and flagged stale.

What counts as a warning

  • A certificate warns from 14 days out, not 30. Let's Encrypt issues at 90 days and renews on its own at 30, so a 30-day threshold put every healthy certificate in warning for a third of its life. Fourteen days leaves room for a failed renewal to surface and still means something when it fires. The constant is TLS_WARN_DAYS in src/lib/sources.ts.
  • The activity log shows charges from the last 7 days. The collector still keeps 90 days of them for the totals, but a payment from two months ago sitting in the log asking to be dismissed only teaches you to ignore the log. The label is "unseen", not "to collect": nothing here moves money, it's a record you haven't looked at yet.

Agents: the MCP

mcp/server.mjs is HQ's board. It decides nothing. It says what is pending and lets whichever agent connects decide what to do about it — like a board on the wall of a base: the list of breakdowns is in plain sight and whoever walks past takes one.

Ten tools, over stdio:

ToolWhat it does
pending_workEverything asking for attention, sorted by urgency: services down, certificates about to expire, open issues and PRs. Items someone has already claimed are marked, not hidden
colony_statusModule counts, what's down, when each source was last collected, who is working right now
issue_detailOpen issues of a repo, with body and author
pr_detailOpen PRs of a repo, with the changed lines of the diff
commentComment on an issue or PR
close_issueClose an issue, with the reason posted as a comment first
merge_prSquash-merge a PR and delete the branch
propose_workPut something on the board no collector found. Nobody can take it until a person approves it with pnpm board
claim_workWrite down that you're taking something on
finish_workClose a job you took, saying how it went

What it can touch is the safety model. On GitHub it can comment, close and merge — all things that undo: an issue reopens, a comment gets deleted, a merge gets reverted. There is nothing here that restarts a server, renews a certificate or touches production, because that doesn't undo. Safety isn't the agent behaving well; it's which tools exist. More gets added deliberately and one at a time.

claim_work and finish_work write to data/agents.json. The collector reads that file into the snapshot, and the rover hangar shows it: who is out on the surface and on what. That's how the colony can say "one agent out there" without lying — because there is one, and the board is what proves it.

Connecting an agent

Any MCP client speaks to it the same way: node mcp/server.mjs over stdio, with HQ_HOME pointing at this repo. It needs a snapshot to read (pnpm run collect at least once) and the gh CLI signed in for the GitHub tools.

Claude Code, one line:

claude mcp add hq-colony -e HQ_HOME=/path/to/hq -- node /path/to/hq/mcp/server.mjs

Claude Desktop, Cursor, or any client with a JSON config — copy mcp.json.example and fix the paths:

{
  "mcpServers": {
    "hq-colony": {
      "command": "node",
      "args": ["/path/to/hq/mcp/server.mjs"],
      "env": { "HQ_HOME": "/path/to/hq" }
    }
  }
}

A scheduled agent (Hermes or similar) is how this repo actually runs it: ops/hermes/ collects on a cadence, compares the signals against what an agent has already answered for, and only wakes the model when there is new work. Empty output costs nothing.

Reads go through the same gh session as the collector, so there's no key in here either.

What a connected agent has already done, from data/agents.json: merged four Dependabot PRs on cloudstudio/laravel-html-crawler (two of them only after the token got the workflow scope it was missing), triaged two Nova 4 issues on cloudstudio/resource-generator and cloudstudio/nova-card-extended, and found the TLS threshold bug above — workee.es and a client's site had been flagged for weeks and both were renewing right on time.

Showing it: ?public

Open the colony with ?public and you get the real snapshot with the parts that aren't yours to show taken out (src/lib/redact.ts):

  • areas marked "private": true in data/projects.json disappear, along with the projects in them;
  • the host keeps its provider and loses the server IP;
  • the payer's name on a charge becomes "Customer";
  • everything an agent wrote in its own words — outcomes, proposals, the task it is on — keeps its shape and loses its words;
  • the workbench keeps only repos you named in localRepos, without this disk's paths or the last commit message;
  • of the goal, only the fraction survives: the valley still greens for a reason, and a percentage of an undisclosed target discloses nothing.

Everything else stays: your domains, your repos with their real stars, the service that is actually down right now. This is for recording and screenshots. It is deliberately not a made-up world — a fabricated map would prove nothing about the map being real, which is the whole point of the project.

Always on

The dev server is for working on the colony; server/serve.mjs is for living in it. It serves the build and the same API the Vite plugin serves — one implementation, in server/api.mjs, because two copies of it would drift the first week.

pnpm run build
node server/serve.mjs        # http://localhost:5199

A LaunchAgent starts it at login and brings it back if it dies, so the colony is up after a reboot without anyone remembering to start it:

cp ops/es.cloudstudio.hq.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/es.cloudstudio.hq.plist

The plist sets PATH on purpose. launchd hands a job /usr/bin:/bin and nothing else, and the collectors shell out to gh, stripe and git. The scheduler rounds learned the same lesson the hard way: one of them ran for ten minutes collecting nothing, comparing nothing and waking the model anyway, and exited 0 so everything upstream believed it had worked.

The colony watches its own watchmen

A round that has missed three of its own turns is not late, it is broken, and the hangar says so. This exists because of a real morning: Hermes kept a stale module in memory across an update, its scheduler threw on every tick, and hermes cron status went on reporting that jobs would fire automatically. It took someone asking to find out. The one thing worse than a watchman asleep is a watchman asleep reporting that all is well.

Credentials

There is no key in this repository. It uses the sessions already on your machine:

  • GitHub — the gh CLI, already authenticated.
  • Stripe — the CLI profiles. The keys live in the system keychain and never leave it: the API is reached through stripe itself.

data/snapshot.json is in .gitignore on purpose: it holds local paths, identities and figures. It lives in data/, outside Vite's public/, so a build can never freeze a copy of it into dist/. data/projects.json is gitignored for the same reason — it names your clients and your hosts; the repo ships data/projects.example.json instead.

Adding a project

data/projects.json is the single source of truth for what's in production, and it's edited by hand. Everything else fills itself in on each collection.

{
  "id": "new",
  "name": "New",
  "url": "https://new.com",
  "repo": "cloudstudio/new",
  "area": "products",
  "stack": ["Laravel 12"],
  "host": "forge:cloudstudio",
  "stripe": null
}

Each area is its own sector in the colony. It appears on its own as soon as it has at least one project.

Local-only repos

The local collector reads every git repo directly under ~/Sites for the aggregate figures the quarry and workshop show. Getting a module of your own in the Workbench sector is opt-in: name the directory in localRepos at the bottom of the same file.

"localRepos": ["hq"]

The whole directory is deliberately not turned into modules. There is client work in there with no business on a map that gets shown.

The goal

HQ is very good at reporting what is happening and would be useless without a way of saying how far along you are. That is one fraction: the MRR the products collect, against a monthly goal you chose.

"mrrGoal": 3000

One number in data/projects.json, typed by a person; the MRR is collected from Stripe. The fraction between them is what the terraforming plant reads and what greens the valley: nothing at 0, a green valley at 1. No goal set means no gauge — rather than a gauge reading zero against a target nobody chose.

Tests

pnpm test

Two hundred and some, in test/, and none of them touch the network. They cover the parts where being wrong has a cost outside the screen:

redactwhat ?public must never let through: a client's name, a private domain, a payer, a server IP, a path off this disk, a word an agent wrote
sourcesthe certificate threshold and the refresh rhythms, including the Stripe read allowance
eventsa refund is not a sale; a sale ages out of the log; a stale reading is not an outage; a new account's history is not today's news
entitiesone built volume per real thing, and the state each one is in
stripequantity multiplies the price, and an account is not the product billing through it
mergea partial reading keeps the last valid value and never becomes a zero; euros and dollars are never one total
mcpthe board over stdio, and a claim letting go of itself
boardone reader and one writer for the board, atomic, shared by all three processes
localreal git repos in a tmpdir: no remote, no commits, not a repo at all
sanitizethe form cannot corrupt the inventory, and hand-typed config rides through a save
labelsthat turning every name on never puts one label on top of another
bakea pivot flagged keep survives the merge whole, role and joint included
terraformthat at 0 the world is untouched, and that things arrive in an order that means something
roundsthat a dead scheduler is noticed, and that a merely late round is not cried wolf over

Several of these are regression tests with a story: the certificate threshold, the 90-day sale window and the reused Stripe account were all live bugs. The comment above each says which.

The graphics have no tests. A frozen drone and a flying one look identical to a type checker, so that check is still a person opening the page.

Structure

test/               the tests, no network
collect/            collectors: production, GitHub, Stripe, local repos
  merge.mjs         the snapshot's arithmetic, pure and tested
server/             always-on: the API, and the process a LaunchAgent starts
ops/                the LaunchAgent and the Hermes rounds
mcp/server.mjs      the board: what is pending, and the tools to act on it
lib/                shared by every process: repo root, the board, thresholds
data/               projects.json (hand-edited, gitignored; example provided),
                    snapshot.json and agents.json (machine state, gitignored)
src/lib/            shared snapshot: events, rhythms, geometry, formatting
  sources.ts        refresh rhythms and the TLS warning threshold
  redact.ts         what `?public` takes out
src/mars/           the colony
  palette.ts        palette and state language
  entities.ts       from snapshot to buildable
  colony.ts         layout, sectors and plots
  modules.ts        one module per real entity
  civic.ts          what HQ knows about itself
  terrain.ts        valley, craters, wall and roads
  fleet.ts          rover, astronauts, drones, mining crew and effects
  world.ts          assembly and repainting
  hud.ts            DOM layer anchored to the world
  panel.ts          table view
  pieces.ts         piece library
  catalog.ts        the inventory page at /catalog.html
  main.ts           boot, controls and loop

Principles

  1. One built volume = one real entity. A module is never invented to make the colony look full. Few entities means a small colony, and it grows on its own. The only things without data behind them are scenery nobody would mistake for a module.
  2. States are seen before they're read. Colour, light and movement first; text second.
  3. Local and private. The data never leaves your machine.

Licence

MIT. See LICENSE.

cloudstudio/hq

Your infrastructure as a colony on Mars. Real data, straight off your machine.

21

stars

0

commits

TypeScript

primary language

Aug 25, 2026

updated

README

HQ

Your infrastructure as a colony on Mars.

Every project in production, every repository and every Stripe account exists as a module you can walk up to. When something falls over, you see it from the far side of the valley. The data is real: it comes off your own machine and never travels anywhere.

The same colony at 0% of the MRR goal and at 100%: bare Mars is the honest reading, the green valley is the destination

pnpm install
cp data/projects.example.json data/projects.json   # your inventory; gitignored
pnpm start       # collect, then open the colony
pnpm test        # no network

pnpm start is pnpm run collect && pnpm run dev. Node 22 or newer.

The colony draws your machine: the projects you list, the repos under ~/Sites, the gh and stripe sessions already logged in. With none of those yet, the world is a small colony over whatever it could read — every module that does appear is real.

What you're looking at

Habitat domeA project in production. Cyan is answering, magenta is a warning, a white strobe is down
GreenhouseA repository. Warnings are open issues and PRs
Ice extractorA Stripe account
WorkbenchA repo that only exists on this disk, with no remote to speak for it
Civic avenueWhat HQ knows about itself: last check per source, headline figures, power as data freshness
QuarryWork in local — the commits that haven't left your machine
Terraforming plantHow far the MRR is from the goal you set. The valley greens as that figure climbs, and at 0 the world is bare Mars, which is the honest reading
RoverDrive it with C. Boost included

Not one built volume is without data behind it. The full vocabulary — what each piece stands for and where it comes from — lives in docs/COLONIA.md; the visual inventory opens at /catalog.html.

Controls

DragPan the map
Right buttonOrbit
WheelZoom to the cursor
WASD / arrowsMove, relative to the view
Q ETurn · R Shift+F tilt · 0 or F frames the colony
Double clickTravel to that spot
CDrive the rover · Space boost · Escape let go
LEvery name at once · also the toggle beside the search field
PTable panel
GForce a collection now
Search fieldJump to any module, civic building or sector by name
Toggle beside itTurns on the name of every single thing, spaced so none covers another

There is no toolbar on purpose: the world takes the whole screen and the scheduler collects by itself. What chrome there is has a job — the search field reaches a module without hunting for it, and the activity log flies you to whatever just happened. A payment you click gets celebrated and never shown again; anything bad opens the module's card instead.

Where the data comes from

SourceEveryHow
Production60 sA real HTTP request to each domain in data/projects.json
Local repos5 minReading git off disk
GitHub15 minThe gh CLI session
Stripe60 minStripe CLI profiles, in live mode
Certificates12 hTLS handshake

Those rhythms are measured, not guessed, and the reasoning for each one is written down in src/lib/sources.ts. The important one: the limit that actually binds on Stripe is not the 100 requests per second, it's the monthly read allowance, 10,000 a month. Polling every five minutes burned 432 % of it.

A source failing never wipes its data: the last valid value is kept and flagged stale.

What counts as a warning

  • A certificate warns from 14 days out, not 30. Let's Encrypt issues at 90 days and renews on its own at 30, so a 30-day threshold put every healthy certificate in warning for a third of its life. Fourteen days leaves room for a failed renewal to surface and still means something when it fires. The constant is TLS_WARN_DAYS in src/lib/sources.ts.
  • The activity log shows charges from the last 7 days. The collector still keeps 90 days of them for the totals, but a payment from two months ago sitting in the log asking to be dismissed only teaches you to ignore the log. The label is "unseen", not "to collect": nothing here moves money, it's a record you haven't looked at yet.

Agents: the MCP

mcp/server.mjs is HQ's board. It decides nothing. It says what is pending and lets whichever agent connects decide what to do about it — like a board on the wall of a base: the list of breakdowns is in plain sight and whoever walks past takes one.

Ten tools, over stdio:

ToolWhat it does
pending_workEverything asking for attention, sorted by urgency: services down, certificates about to expire, open issues and PRs. Items someone has already claimed are marked, not hidden
colony_statusModule counts, what's down, when each source was last collected, who is working right now
issue_detailOpen issues of a repo, with body and author
pr_detailOpen PRs of a repo, with the changed lines of the diff
commentComment on an issue or PR
close_issueClose an issue, with the reason posted as a comment first
merge_prSquash-merge a PR and delete the branch
propose_workPut something on the board no collector found. Nobody can take it until a person approves it with pnpm board
claim_workWrite down that you're taking something on
finish_workClose a job you took, saying how it went

What it can touch is the safety model. On GitHub it can comment, close and merge — all things that undo: an issue reopens, a comment gets deleted, a merge gets reverted. There is nothing here that restarts a server, renews a certificate or touches production, because that doesn't undo. Safety isn't the agent behaving well; it's which tools exist. More gets added deliberately and one at a time.

claim_work and finish_work write to data/agents.json. The collector reads that file into the snapshot, and the rover hangar shows it: who is out on the surface and on what. That's how the colony can say "one agent out there" without lying — because there is one, and the board is what proves it.

Connecting an agent

Any MCP client speaks to it the same way: node mcp/server.mjs over stdio, with HQ_HOME pointing at this repo. It needs a snapshot to read (pnpm run collect at least once) and the gh CLI signed in for the GitHub tools.

Claude Code, one line:

claude mcp add hq-colony -e HQ_HOME=/path/to/hq -- node /path/to/hq/mcp/server.mjs

Claude Desktop, Cursor, or any client with a JSON config — copy mcp.json.example and fix the paths:

{
  "mcpServers": {
    "hq-colony": {
      "command": "node",
      "args": ["/path/to/hq/mcp/server.mjs"],
      "env": { "HQ_HOME": "/path/to/hq" }
    }
  }
}

A scheduled agent (Hermes or similar) is how this repo actually runs it: ops/hermes/ collects on a cadence, compares the signals against what an agent has already answered for, and only wakes the model when there is new work. Empty output costs nothing.

Reads go through the same gh session as the collector, so there's no key in here either.

What a connected agent has already done, from data/agents.json: merged four Dependabot PRs on cloudstudio/laravel-html-crawler (two of them only after the token got the workflow scope it was missing), triaged two Nova 4 issues on cloudstudio/resource-generator and cloudstudio/nova-card-extended, and found the TLS threshold bug above — workee.es and a client's site had been flagged for weeks and both were renewing right on time.

Showing it: ?public

Open the colony with ?public and you get the real snapshot with the parts that aren't yours to show taken out (src/lib/redact.ts):

  • areas marked "private": true in data/projects.json disappear, along with the projects in them;
  • the host keeps its provider and loses the server IP;
  • the payer's name on a charge becomes "Customer";
  • everything an agent wrote in its own words — outcomes, proposals, the task it is on — keeps its shape and loses its words;
  • the workbench keeps only repos you named in localRepos, without this disk's paths or the last commit message;
  • of the goal, only the fraction survives: the valley still greens for a reason, and a percentage of an undisclosed target discloses nothing.

Everything else stays: your domains, your repos with their real stars, the service that is actually down right now. This is for recording and screenshots. It is deliberately not a made-up world — a fabricated map would prove nothing about the map being real, which is the whole point of the project.

Always on

The dev server is for working on the colony; server/serve.mjs is for living in it. It serves the build and the same API the Vite plugin serves — one implementation, in server/api.mjs, because two copies of it would drift the first week.

pnpm run build
node server/serve.mjs        # http://localhost:5199

A LaunchAgent starts it at login and brings it back if it dies, so the colony is up after a reboot without anyone remembering to start it:

cp ops/es.cloudstudio.hq.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/es.cloudstudio.hq.plist

The plist sets PATH on purpose. launchd hands a job /usr/bin:/bin and nothing else, and the collectors shell out to gh, stripe and git. The scheduler rounds learned the same lesson the hard way: one of them ran for ten minutes collecting nothing, comparing nothing and waking the model anyway, and exited 0 so everything upstream believed it had worked.

The colony watches its own watchmen

A round that has missed three of its own turns is not late, it is broken, and the hangar says so. This exists because of a real morning: Hermes kept a stale module in memory across an update, its scheduler threw on every tick, and hermes cron status went on reporting that jobs would fire automatically. It took someone asking to find out. The one thing worse than a watchman asleep is a watchman asleep reporting that all is well.

Credentials

There is no key in this repository. It uses the sessions already on your machine:

  • GitHub — the gh CLI, already authenticated.
  • Stripe — the CLI profiles. The keys live in the system keychain and never leave it: the API is reached through stripe itself.

data/snapshot.json is in .gitignore on purpose: it holds local paths, identities and figures. It lives in data/, outside Vite's public/, so a build can never freeze a copy of it into dist/. data/projects.json is gitignored for the same reason — it names your clients and your hosts; the repo ships data/projects.example.json instead.

Adding a project

data/projects.json is the single source of truth for what's in production, and it's edited by hand. Everything else fills itself in on each collection.

{
  "id": "new",
  "name": "New",
  "url": "https://new.com",
  "repo": "cloudstudio/new",
  "area": "products",
  "stack": ["Laravel 12"],
  "host": "forge:cloudstudio",
  "stripe": null
}

Each area is its own sector in the colony. It appears on its own as soon as it has at least one project.

Local-only repos

The local collector reads every git repo directly under ~/Sites for the aggregate figures the quarry and workshop show. Getting a module of your own in the Workbench sector is opt-in: name the directory in localRepos at the bottom of the same file.

"localRepos": ["hq"]

The whole directory is deliberately not turned into modules. There is client work in there with no business on a map that gets shown.

The goal

HQ is very good at reporting what is happening and would be useless without a way of saying how far along you are. That is one fraction: the MRR the products collect, against a monthly goal you chose.

"mrrGoal": 3000

One number in data/projects.json, typed by a person; the MRR is collected from Stripe. The fraction between them is what the terraforming plant reads and what greens the valley: nothing at 0, a green valley at 1. No goal set means no gauge — rather than a gauge reading zero against a target nobody chose.

Tests

pnpm test

Two hundred and some, in test/, and none of them touch the network. They cover the parts where being wrong has a cost outside the screen:

redactwhat ?public must never let through: a client's name, a private domain, a payer, a server IP, a path off this disk, a word an agent wrote
sourcesthe certificate threshold and the refresh rhythms, including the Stripe read allowance
eventsa refund is not a sale; a sale ages out of the log; a stale reading is not an outage; a new account's history is not today's news
entitiesone built volume per real thing, and the state each one is in
stripequantity multiplies the price, and an account is not the product billing through it
mergea partial reading keeps the last valid value and never becomes a zero; euros and dollars are never one total
mcpthe board over stdio, and a claim letting go of itself
boardone reader and one writer for the board, atomic, shared by all three processes
localreal git repos in a tmpdir: no remote, no commits, not a repo at all
sanitizethe form cannot corrupt the inventory, and hand-typed config rides through a save
labelsthat turning every name on never puts one label on top of another
bakea pivot flagged keep survives the merge whole, role and joint included
terraformthat at 0 the world is untouched, and that things arrive in an order that means something
roundsthat a dead scheduler is noticed, and that a merely late round is not cried wolf over

Several of these are regression tests with a story: the certificate threshold, the 90-day sale window and the reused Stripe account were all live bugs. The comment above each says which.

The graphics have no tests. A frozen drone and a flying one look identical to a type checker, so that check is still a person opening the page.

Structure

test/               the tests, no network
collect/            collectors: production, GitHub, Stripe, local repos
  merge.mjs         the snapshot's arithmetic, pure and tested
server/             always-on: the API, and the process a LaunchAgent starts
ops/                the LaunchAgent and the Hermes rounds
mcp/server.mjs      the board: what is pending, and the tools to act on it
lib/                shared by every process: repo root, the board, thresholds
data/               projects.json (hand-edited, gitignored; example provided),
                    snapshot.json and agents.json (machine state, gitignored)
src/lib/            shared snapshot: events, rhythms, geometry, formatting
  sources.ts        refresh rhythms and the TLS warning threshold
  redact.ts         what `?public` takes out
src/mars/           the colony
  palette.ts        palette and state language
  entities.ts       from snapshot to buildable
  colony.ts         layout, sectors and plots
  modules.ts        one module per real entity
  civic.ts          what HQ knows about itself
  terrain.ts        valley, craters, wall and roads
  fleet.ts          rover, astronauts, drones, mining crew and effects
  world.ts          assembly and repainting
  hud.ts            DOM layer anchored to the world
  panel.ts          table view
  pieces.ts         piece library
  catalog.ts        the inventory page at /catalog.html
  main.ts           boot, controls and loop

Principles

  1. One built volume = one real entity. A module is never invented to make the colony look full. Few entities means a small colony, and it grows on its own. The only things without data behind them are scenery nobody would mistake for a module.
  2. States are seen before they're read. Colour, light and movement first; text second.
  3. Local and private. The data never leaves your machine.

Licence

MIT. See LICENSE.

Languages

TypeScript

70.7%

JavaScript

23.5%

HTML

4.9%