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.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
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: [],
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user