Files
station-master-startos/startos/main.ts
T
Jesse de28b30eea Initial StartOS package for Station Master (v0.5.1)
Built from source via a git submodule pinned to a tag, not a published image
— the Dockerfile, main.ts, and manifest should never need to change for an
ordinary version bump, only the submodule pin (see UPDATING.md). One volume,
one interface serving the browser client + lobby/intent API + SSE stream
from a single origin, no dependencies. The server-wide join secret (D14) is
seeded on install, exposed via the Get Join Secret action, and blocks start
behind a critical task until retrieved — the same first-set/rotation pattern
as actual-budget-startos's admin password.

Verified on phoenix.local: installs, the critical task correctly blocks an
ordinary start, force-starting confirms the daemon binds its port and serves
the client, and store.json correctly holds the install-seeded join secret.
Not verified: the Get Join Secret action's execution end-to-end — start-cli's
`package action run` fails with a client-side deserialization error that
reproduces identically against actual-budget's already-shipped equivalent
action, so this looks like a start-cli issue rather than a defect here.

icon.svg is still the scaffold's hello-world placeholder — no real Station
Master icon exists yet to ship in its place.
2026-08-21 08:44:07 -04:00

51 lines
1.9 KiB
TypeScript

import { i18n } from './i18n'
import { sdk } from './sdk'
import { uiPort, dataDir } from './utils'
import { storeJson } from './fileModels/store.json'
export const main = sdk.setupMain(async ({ effects }) => {
console.info(i18n('Starting Station Master!'))
// Reactive, field-scoped: rotating the join secret (getJoinSecret.ts) rewrites store.json,
// which re-runs setupMain and restarts the daemon with the new value.
const joinSecret = await storeJson.read((s) => s.joinSecret).const(effects)
return sdk.Daemons.of(effects).addDaemon('server', {
subcontainer: sdk.SubContainer.of(
effects,
{ imageId: 'main' },
sdk.Mounts.of().mountVolume({
volumeId: 'data',
subpath: null,
mountpoint: dataDir,
readonly: false,
}),
'station-master-sub',
),
exec: {
command: ['node', 'src/server/index.ts'],
env: {
// PORT/BIND_ADDRESS/DIST_DIR are left at src/server/index.ts's own defaults (8081,
// 0.0.0.0, ./dist relative to the Dockerfile's WORKDIR) — nothing here needs to differ
// from them, so only what actually must be supplied is set explicitly.
DATA_DIR: dataDir,
// generateJoinSecret.ts seeds this before main.ts ever runs, so it is never actually
// empty — the fallback only avoids threading `string | undefined` through `env`, which
// wants `Record<string, string>`.
JOIN_SECRET: joinSecret ?? '',
},
},
// Health check, run on each polling interval. `checkPortListening` reports ready once the
// daemon binds `uiPort`; the 'ui' interface (interfaces.ts) exposes the same port.
ready: {
display: i18n('Multiplayer Server'),
fn: () =>
sdk.healthCheck.checkPortListening(effects, uiPort, {
successMessage: i18n('The multiplayer server is ready'),
errorMessage: i18n('The multiplayer server is not ready'),
}),
},
requires: [],
})
})