commit de28b30eea95783b275c41a69724069a3310dbb5 Author: Jesse Date: Fri Aug 21 08:44:07 2026 -0400 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. diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f4213aa --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +.gitmodules +node_modules +*.s9pk +docker-images +javascript \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..8111d07 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,17 @@ +name: Build + +on: + workflow_dispatch: + pull_request: + paths-ignore: ['*.md'] + branches: ['master'] + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + build: + if: github.event.pull_request.draft == false + uses: Start9Labs/start-technologies/.github/workflows/build.yml@master + # No DEV_KEY — a PR build doesn't publish, so it doesn't need the signing key. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6dcef4a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,19 @@ +name: Release + +on: + push: + tags: + - 'v*.*' + +jobs: + release: + uses: Start9Labs/start-technologies/.github/workflows/release.yml@master + with: + RELEASE_REGISTRY: ${{ vars.RELEASE_REGISTRY }} + S3_S9PKS_BASE_URL: ${{ vars.S3_S9PKS_BASE_URL }} + secrets: + DEV_KEY: ${{ secrets.DEV_KEY }} + S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} + S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} + permissions: + contents: write diff --git a/.github/workflows/syncNext.yml b/.github/workflows/syncNext.yml new file mode 100644 index 0000000..14a28b8 --- /dev/null +++ b/.github/workflows/syncNext.yml @@ -0,0 +1,24 @@ +name: Sync next + +# Carries every change that lands on the base branch onto the paired `next` +# iteration branch, so `next` never drifts behind what has already shipped. +# `next` is created on the first run if the repo does not have one yet. +# +# List every base branch this package maintains. Most packages have one; the +# multi-branch packages list each major line or flavor (e.g. 28.x, 29.x). A +# package whose default branch is `main` must say `main` here — a workflow +# pointed at a branch the repo does not use never runs. +# +# No paths-ignore: a docs-only commit on the base still has to reach `next`, +# or the next merge back re-introduces the stale copy. +on: + push: + branches: ['master'] + workflow_dispatch: + +jobs: + sync: + uses: Start9Labs/start-technologies/.github/workflows/syncNext.yml@master + permissions: + contents: write + pull-requests: write diff --git a/.github/workflows/tagAndRelease.yml b/.github/workflows/tagAndRelease.yml new file mode 100644 index 0000000..f000851 --- /dev/null +++ b/.github/workflows/tagAndRelease.yml @@ -0,0 +1,24 @@ +name: Tag and Release + +on: + push: + branches: ['master'] + paths-ignore: ['*.md'] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + tag: + uses: Start9Labs/start-technologies/.github/workflows/tagAndRelease.yml@master + with: + REFERENCE_REGISTRY: ${{ vars.REFERENCE_REGISTRY }} + RELEASE_REGISTRY: ${{ vars.RELEASE_REGISTRY }} + S3_S9PKS_BASE_URL: ${{ vars.S3_S9PKS_BASE_URL }} + secrets: + DEV_KEY: ${{ secrets.DEV_KEY }} + S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} + S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} + permissions: + contents: write diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4dad48c --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.s9pk +startos/*.js +node_modules/ +.DS_Store +.vscode/ +docker-images +javascript +ncc-cache \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7cd3021 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "station-master"] + path = station-master + url = https://draco.local:53871/Jesse.Markowitz/station-master.git diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9a87a02 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,49 @@ +# AGENTS.md + +This is a StartOS service-package repository — it builds a `.s9pk` for StartOS. + +Develop it inside a StartOS packaging workspace created by `start-cli s9pk init-workspace`, +which provides the packaging guide and agent context one level up. If you're reading this in a +bare clone with no workspace, the full guide is at . + +**Start every task at the recipe index** — `../start-technologies/projects/start-sdk/docs/src/recipes.md` +(or ). It maps an intent ("prompt the user to create +admin credentials", "expose a web UI") to the constructs, the reference pages, and a named production +package to copy. Find the recipe before you read this package's neighbours: a package you reach by +grepping may be non-conformant, and the recipe outranks it. + +Freshly scaffolded? Work the +[New Package Checklist](../start-technologies/projects/start-sdk/docs/src/new-package-checklist.md) +(or ) from top to bottom. It is a +guide page, not a file in this repo — read it, don't copy it in. + +Keep `README.md` (technical reference for an AI support or administering agent) and +`instructions.md` (end-user docs) in sync with your changes. + +**Bugs and feature requests are issues on this repo** (self-hosted Gitea, not GitHub) — file +them as you find them. Don't record work in the repo instead: no `TODO.md`, no `NOTES.md`, no +`PLAN.md`. What you verified, tried, and decided belongs in the commit message and the PR body. + +## This repo + +- **Station Master is a git submodule (`station-master/`), not vendored source.** It tracks + [Jesse's own game repo](https://draco.local:53871/Jesse.Markowitz/station-master), self-hosted, + pinned to a tag. See `UPDATING.md` for how the pin is bumped — the `Dockerfile`, `main.ts`, and + the manifest should never need to change for an ordinary version bump. +- **Built from source, not a published image.** The `Dockerfile` is entirely this repo's own: + a `node:*-slim` builder stage runs the submodule's `npm run build:web` to produce the static + client, then a runtime stage copies only `package.json`, `src/`, and the built `dist/` — no + `node_modules`, since `src/server/index.ts` has zero runtime dependencies (see the submodule's + own `TODO.md` for why) and Node's native TypeScript type-stripping means it needs no compile + step either. +- **The join secret (D14 in the game's `docs/architecture/multiplayer.md`) is this package's one + piece of state beyond the game data itself.** `startos/fileModels/store.json.ts` holds it, + seeded on install (`startos/init/generateJoinSecret.ts`) since the server refuses to start + without one, and rotated by the **Get Join Secret** action + (`startos/actions/getJoinSecret.ts`) — the same action handles first retrieval and rotation, per + `recipe-admin-credentials.md`. +- **One volume, one interface, no dependencies.** Everything the server persists — + `store.json` and every game's own save data — lives on the `data` volume; the single `ui` + interface carries the browser client, the lobby/intent HTTP API, and the SSE game stream from + one port, which is what keeps players reachable from different addresses in the same game + (`docs/architecture/deployment.md` §3 in the game repo). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..380823a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +# Station Master's multiplayer server, built from source — there is no published image to wrap. +# Source is the `station-master` git submodule, pinned to a tag; see UPDATING.md for how that pin +# is bumped. This Dockerfile never needs to change for a version bump, only the submodule ref. + +# --- builder: produces the static browser client (dist/) --- +# The server itself has zero runtime dependencies (station-master/TODO.md), but `npm run +# build:web` shells out to `tsc`, which lives in devDependencies — hence the separate stage. +FROM node:22.23.1-slim AS builder +WORKDIR /build +COPY station-master/package.json station-master/package-lock.json ./ +RUN npm ci +COPY station-master/ ./ +RUN npm run build:web + +# --- runtime: the server, no node_modules needed at all --- +FROM node:22.23.1-slim +WORKDIR /app +# Node >=22.18 strips TypeScript types natively (station-master/package.json's engines field), +# so the server runs straight from source with no build/compile step. +COPY station-master/package.json ./ +COPY station-master/src/ ./src/ +COPY --from=builder /build/dist/ ./dist/ +CMD ["node", "src/server/index.ts"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..450e580 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2026 Jesse Markowitz + +All rights reserved. + +This software is proprietary. No license is granted to copy, modify, +distribute, or use this software, in whole or in part, without the express +prior written permission of the copyright holder. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8315da3 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +ARCHES := x86 arm +# overrides to s9pk.mk must precede the include statement +include node_modules/@start9labs/start-sdk/s9pk.mk diff --git a/README.md b/README.md new file mode 100644 index 0000000..f27ae3f --- /dev/null +++ b/README.md @@ -0,0 +1,160 @@ +

+ Station Master Logo +

+ +# Station Master on StartOS + +> Everything not listed in this document should behave the same as upstream +> Station Master. If a feature, setting, or behavior is not mentioned here, +> the upstream documentation is accurate and fully applicable — see the +> Documentation section of `instructions.md` for links. + +Station Master is a railroad operations board game with an authoritative multiplayer server; +solitaire needs no server and is not what this package is for. This package runs that server — +the browser client, the lobby, and the intent/SSE API — as a single StartOS service. + +--- + +## Table of Contents + +- [Image and Container Runtime](#image-and-container-runtime) +- [Volume and Data Layout](#volume-and-data-layout) +- [File Models](#file-models) +- [Dependencies](#dependencies) +- [Network Access and Interfaces](#network-access-and-interfaces) +- [Installation and First-Run Flow](#installation-and-first-run-flow) +- [Actions](#actions) +- [Tasks](#tasks) +- [Health Checks](#health-checks) +- [Backups and Restore](#backups-and-restore) +- [Limitations and Differences](#limitations-and-differences) +- [Quick Reference for AI Consumers](#quick-reference-for-ai-consumers) + +--- + +## Image and Container Runtime + +Built from source with a custom `Dockerfile` — there is no published Station Master image. + +| What to Document | Value | +| --- | --- | +| Image source | Custom multi-stage `Dockerfile`: `node:*-slim` builder runs `npm run build:web` to produce the static client, then a second `node:*-slim` stage copies only `package.json`, `src/`, and the built `dist/` — no `node_modules` in the runtime stage, since the server has zero runtime dependencies | +| Architectures | x86_64, aarch64 | +| Entrypoint | `node src/server/index.ts` — runs straight from TypeScript source; no compile step, see Limitations | + +One subcontainer: `station-master-sub`, running the single daemon `server`. + +## Volume and Data Layout + +One volume, `data`, mounted at `/data` (`DATA_DIR`). + +| What to Document | Value | +| --- | --- | +| Volume names | `data` | +| Mount points | `/data` | +| StartOS files | `store.json` — holds the join secret (see File Models) | +| Database | None — flat files. Each game is `games//game.json` (`{ engineVersion, seed, config, playerNames, history, status, createdAt }`) plus `turn-timings.json`, an `index.json` naming every game, and lobby state for games not yet started | + +## File Models + +One StartOS-managed file, `store.json`, on the `data` volume. + +- **`store.json`** — JSON, holds `joinSecret` (a string). Seeded on install with a random + 24-character value (`init/generateJoinSecret.ts`) — the daemon will not start without one, so + it is never left unset. Rewritten only by the **Get Join Secret** action, which generates a new + value on every run; the value is otherwise never touched, so a hand edit to it survives until + the action is next run. `main.ts` reads it reactively, so a rewrite restarts the daemon with the + new value automatically. + +Everything else under `/data` — `games/`, `index.json`, lobby and session state — is the +application's own persistence, written directly by the server process, not by a StartOS file +model. + +## Dependencies + +None. + +## Network Access and Interfaces + +One interface, `ui`, on the single port the daemon listens on. It serves the browser client, the +lobby and intent HTTP API, and the per-seat SSE game stream — all from the same origin, which is +what lets a player reach the server from a LAN address and another player reach it from a +different one in the same game. + +## Installation and First-Run Flow + +No setup wizard. On install, a join secret is generated and stored immediately (see File Models), +and a critical task is raised pointing at **Get Join Secret** — the service starts and is usable +the moment the daemon is healthy, but a player cannot create or join a game until the join secret +has been retrieved and shared with them. + +## Actions + +- **Get Join Secret** (`get-join-secret`) — run this any time you want to read the current join + secret, or to invalidate it and issue a new one. Every run **generates a fresh secret, + overwrites the stored one, and restarts the daemon** with it — there is no read-only mode. + Rotating does not disconnect players already seated in a running game (D14's join secret gates + the lobby door, not an in-progress session); it only invalidates the old value for anyone who + has not yet joined or created a game. Completes in a few seconds, safe to repeat. + +## Tasks + +- **Get the join secret to share with players** — raised on install, severity `critical`. Points + at the **Get Join Secret** action. Clears the moment that action is run for the first time; it + does not return afterward (running it again to rotate does not re-raise it). + +## Health Checks + +- **Multiplayer Server** — `checkPortListening` against the daemon's port. "Not ready" means the + process has not yet bound its port; on a normal start this clears within a second or two; on a + cold container start it briefly reflects the daemon starting up. + +## Backups and Restore + +Strategy: the entire `data` volume is backed up wholesale (`ofVolumes`) — every file copied +exactly as it sits on disk, nothing dumped and replayed. + +That includes every game's full intent history, so a restore can resume any in-progress game +exactly where it left off, and the join secret in `store.json`, so previously shared join links +keep working after a restore. Nothing needs to be rebuilt or re-entered after a restore; the +server resumes every saved game on boot the same way it does after an ordinary restart. + +## Limitations and Differences + +1. **No compile step.** The image runs the server directly from TypeScript source using Node's + native type stripping, rather than building a `dist/` for the server the way the browser + client is built. This is upstream's own deployment shape (`docs/architecture/deployment.md` in + the game repo), not something this package changed. +2. **The join secret has no per-player identity.** It is a single server-wide value (D14) that + gates who may create or join a game — it is not a username or password, and StartOS has no + visibility into who a player is once they've joined. +3. **No accounts, and one game at a time per person is expected but not enforced** — a deliberate + upstream design decision (`docs/architecture/multiplayer.md` §11 D13), not a StartOS-specific + limitation. + +--- + +## Quick Reference for AI Consumers + +```yaml +package_id: station-master +image: built from source (Dockerfile) +architectures: [x86_64, aarch64] +subcontainers: [station-master-sub] +volumes: + data: /data +file_models: + - store.json +startos_managed_env_vars: + - DATA_DIR + - JOIN_SECRET +dependencies: none +interfaces: + ui: { type: ui, port: 8081 } +actions: + - get-join-secret +tasks: + - { action: get-join-secret, severity: critical } +health_checks: + - Multiplayer Server +``` diff --git a/UPDATING.md b/UPDATING.md new file mode 100644 index 0000000..6320081 --- /dev/null +++ b/UPDATING.md @@ -0,0 +1,41 @@ +# Updating the upstream version + +Station Master is built from a git submodule at `station-master/` (tracking +[`Jesse.Markowitz/station-master`](https://draco.local:53871/Jesse.Markowitz/station-master), a +self-hosted Gitea repo — this is Jesse's own game, not a third-party upstream, but it is packaged +the same way: `Dockerfile` builds whatever commit the submodule points at, with no pinned +`dockerTag` in the manifest. The submodule pin **is** the version. + +Station Master tags its releases (`v0.5.1`, …) at the commit that bumped `package.json`. Nothing +else about this package should need to change for a bump — not the `Dockerfile`, not `main.ts`, +not the manifest — only the submodule pin and this package's own version (below). + +## Determining the upstream version + +```bash +cd station-master && git fetch --tags && git tag -l --sort=-v:refname | head -5 +``` + +The current pin is the submodule's checked-out commit: + +```bash +git submodule status station-master +``` + +## Applying the bump + +```bash +cd station-master && git fetch --tags && git checkout v +cd .. && git add station-master +``` + +Then bump this package's own version in `startos/versions/current.ts` (see +[Versions](../start-technologies/projects/start-sdk/docs/src/versions.md)) with release notes +naming the Station Master version now bundled — e.g. "Bundles Station Master v0.5.2." — and +rebuild (`make`). + +If the jump carries a multiplayer protocol or persistence-format change (check the game repo's +`CHANGELOG.md` and `docs/architecture/multiplayer.md`), read it before bumping: a running +server's saved games are stamped with `engineVersion` and refuse to resume under a mismatched +one (`src/server/index.ts` in the game repo) — nothing in this package works around that, so an +in-progress game must finish (or be accepted as lost) before an incompatible bump. diff --git a/assets/.gitkeep b/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..79990c3 --- /dev/null +++ b/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + Hello! + diff --git a/instructions.md b/instructions.md new file mode 100644 index 0000000..7bc7f72 --- /dev/null +++ b/instructions.md @@ -0,0 +1,38 @@ +# Station Master + +## Documentation + +- [Rules and design docs](https://draco.local:53871/Jesse.Markowitz/station-master/src/branch/main/docs) + — the rulebook implications, the multiplayer design, and the game's architecture. + +## What you get on StartOS + +This service runs the multiplayer server: a lobby, and a table for 2 to 4 players to play a +competitive or cooperative game together from their own browsers. Everything a player needs — the +game itself, the lobby, and the connection back to this server — is served from the single web +interface below; there is nothing else to install or configure on a player's side. + +## Getting set up + +1. Open this service's **Multiplayer Table** interface. +2. Run the **Get Join Secret** action from this service's Actions menu, and copy the value it + returns. +3. Share that secret, and the address you opened the interface at, with everyone you want to + play with. Anyone holding the secret can create a new game or join one that hasn't started yet. +4. In the browser, click **Multiplayer**, then **Create Game** (or **Join Game** with a game code + someone else created), and enter the join secret when asked. + +## Using Station Master + +### Multiplayer Table + +Create or join a game, seat players (including filling empty seats with a bot), and start. Once a +game is underway, this same interface is where every player plays their turns and watches the +table — there is nothing else to open. + +### Get Join Secret + +Run this whenever you need the current join secret again, or want to shut out anyone still +holding an old one. Every run replaces the secret and immediately puts the new one into effect — +players already seated in a game are unaffected, but the old secret stops working for creating or +joining games from that point on. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0402eae --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1670 @@ +{ + "name": "station-master-startos", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "station-master-startos", + "dependencies": { + "@start9labs/start-sdk": "2.0.9" + }, + "devDependencies": { + "@types/node": "^22.19.0", + "@vercel/ncc": "^0.38.4", + "prettier": "^3.6.2", + "typescript": "^6.0.3" + } + }, + "node_modules/@iarna/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==", + "license": "ISC" + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodable/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@start9labs/start-sdk/-/start-sdk-2.0.9.tgz", + "integrity": "sha512-HuSYrS10Bb+f6OyAMlCzd5Qyr+rIauiI0dF8MVh5Ny+eU0gF8/DeFAXlHFgL+9wkSv4UAP5q5yOzlBpXzsNbWQ==", + "bundleDependencies": [ + "@start9labs/start-core", + "eslint", + "typescript-eslint" + ], + "license": "MIT", + "dependencies": { + "@iarna/toml": "^3.0.0", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0", + "@start9labs/start-core": "file:./node_modules/@start9labs/start-core", + "@types/ini": "^4.1.1", + "deep-equality-data-structures": "^2.0.0", + "eslint": "^9.39.4", + "fast-xml-parser": "~5.7.0", + "ini": "^5.0.0", + "isomorphic-fetch": "^3.0.0", + "mime": "^4.1.0", + "typescript-eslint": "^8.61.0", + "yaml": "^2.8.3", + "zod": "4.4.3", + "zod-deep-partial": "^1.2.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@eslint/config-array": { + "version": "0.21.2", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.15", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@eslint/core": { + "version": "0.17.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "inBundle": true, + "license": "Python-2.0" + }, + "node_modules/@start9labs/start-sdk/node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.15", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.2.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "inBundle": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@eslint/js": { + "version": "9.39.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@eslint/object-schema": { + "version": "2.1.7", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@humanfs/core": { + "version": "0.19.2", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@humanfs/node": { + "version": "0.16.8", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@humanfs/types": { + "version": "0.15.0", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@types/estree": { + "version": "1.0.9", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk/node_modules/@types/json-schema": { + "version": "7.0.15", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.61.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/type-utils": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.61.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@typescript-eslint/parser": { + "version": "8.61.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@typescript-eslint/project-service": { + "version": "8.61.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.61.0", + "@typescript-eslint/types": "^8.61.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@typescript-eslint/scope-manager": { + "version": "8.61.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.61.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@typescript-eslint/type-utils": { + "version": "8.61.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@typescript-eslint/types": { + "version": "8.61.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.61.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.61.0", + "@typescript-eslint/tsconfig-utils": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/visitor-keys": "8.61.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.3", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@typescript-eslint/utils": { + "version": "8.61.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.61.0", + "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.61.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/acorn": { + "version": "8.16.0", + "inBundle": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/acorn-jsx": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/ajv": { + "version": "6.15.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk/node_modules/callsites": { + "version": "3.1.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/chalk": { + "version": "4.1.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk/node_modules/cross-spawn": { + "version": "7.0.6", + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/debug": { + "version": "4.4.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@start9labs/start-sdk/node_modules/deep-is": { + "version": "0.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk/node_modules/eslint": { + "version": "9.39.4", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/@start9labs/start-sdk/node_modules/eslint-scope": { + "version": "8.4.0", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.15", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/espree": { + "version": "10.4.0", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/esquery": { + "version": "1.7.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/esrecurse": { + "version": "4.3.0", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/estraverse": { + "version": "5.3.0", + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/esutils": { + "version": "2.0.3", + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk/node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk/node_modules/fast-levenshtein": { + "version": "2.0.6", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk/node_modules/fdir": { + "version": "6.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/@start9labs/start-sdk/node_modules/file-entry-cache": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/flat-cache": { + "version": "4.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/flatted": { + "version": "3.4.2", + "inBundle": true, + "license": "ISC" + }, + "node_modules/@start9labs/start-sdk/node_modules/globals": { + "version": "14.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/has-flag": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/import-fresh": { + "version": "3.3.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/imurmurhash": { + "version": "0.1.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/is-extglob": { + "version": "2.1.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/is-glob": { + "version": "4.0.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/isexe": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/@start9labs/start-sdk/node_modules/json-buffer": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk/node_modules/json-schema-traverse": { + "version": "0.4.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk/node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk/node_modules/keyv": { + "version": "4.5.4", + "inBundle": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/levn": { + "version": "0.4.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/lodash.merge": { + "version": "4.6.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk/node_modules/ms": { + "version": "2.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk/node_modules/natural-compare": { + "version": "1.4.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk/node_modules/optionator": { + "version": "0.9.4", + "inBundle": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/p-limit": { + "version": "3.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/parent-module": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/path-exists": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/path-key": { + "version": "3.1.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/picomatch": { + "version": "4.0.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/prelude-ls": { + "version": "1.2.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/shebang-command": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/shebang-regex": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/strip-json-comments": { + "version": "3.1.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/supports-color": { + "version": "7.2.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/tinyglobby": { + "version": "0.2.17", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/ts-api-utils": { + "version": "2.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/type-check": { + "version": "0.4.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/typescript-eslint": { + "version": "8.61.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.61.0", + "@typescript-eslint/parser": "8.61.0", + "@typescript-eslint/typescript-estree": "8.61.0", + "@typescript-eslint/utils": "8.61.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/uri-js": { + "version": "4.4.1", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/which": { + "version": "2.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/word-wrap": { + "version": "1.2.5", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/yocto-queue": { + "version": "0.1.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vercel/ncc": { + "version": "0.38.4", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz", + "integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==", + "dev": true, + "license": "MIT", + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/deep-equality-data-structures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/deep-equality-data-structures/-/deep-equality-data-structures-2.0.0.tgz", + "integrity": "sha512-qgrUr7MKXq7VRN+WUpQ48QlXVGL0KdibAoTX8KRg18lgOgqbEKMAW1WZsVCtakY4+XX42pbAJzTz/DlXEFM2Fg==", + "license": "MIT", + "dependencies": { + "object-hash": "^3.0.0" + } + }, + "node_modules/fast-xml-builder": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz", + "integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.6.2", + "xml-naming": "^0.3.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/mime": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/strnum": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.2.tgz", + "integrity": "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/xml-naming": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-deep-partial": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/zod-deep-partial/-/zod-deep-partial-1.4.4.tgz", + "integrity": "sha512-aWkPl7hVStgE01WzbbSxCgX4O+sSpgt8JOjvFUtMTF75VgL6MhWQbiZi+AWGN85SfSTtI9gsOtL1vInoqfDVaA==", + "license": "MIT", + "peerDependencies": { + "zod": "^4.1.13" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..808fb79 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "station-master-startos", + "scripts": { + "build": "rm -rf ./javascript && ncc build startos/index.ts -o ./javascript", + "prettier": "prettier --write startos", + "check": "tsc --noEmit" + }, + "dependencies": { + "@start9labs/start-sdk": "2.0.9" + }, + "overrides": { + "@start9labs/start-sdk": "$@start9labs/start-sdk" + }, + "devDependencies": { + "@types/node": "^22.19.0", + "@vercel/ncc": "^0.38.4", + "prettier": "^3.6.2", + "typescript": "^6.0.3" + }, + "prettier": { + "trailingComma": "all", + "tabWidth": 2, + "semi": false, + "singleQuote": true + } +} diff --git a/startos/actions/getJoinSecret.ts b/startos/actions/getJoinSecret.ts new file mode 100644 index 0000000..5ca4f58 --- /dev/null +++ b/startos/actions/getJoinSecret.ts @@ -0,0 +1,51 @@ +import { utils } from '@start9labs/start-sdk' +import { i18n } from '../i18n' +import { sdk } from '../sdk' +import { storeJson } from '../fileModels/store.json' + +/** + * Mints a fresh join secret on every run and returns it — first-set and rotation are the same + * action (recipe-admin-credentials.md). Writing a new value restarts the daemon with it: main.ts + * reads `joinSecret` reactively via `.const(effects)`, so a rotation here takes effect + * immediately rather than needing a manual restart. + * + * Rotating invalidates the old secret for anyone who hasn't joined yet, but never removes a + * player already seated in a game — D14's secret gates the lobby door only (join-secret ≠ + * session token, per `lobby-and-sessions.md` §1). + */ +export const getJoinSecret = sdk.Action.withoutInput( + 'get-join-secret', + + async () => ({ + name: i18n('Get Join Secret'), + description: i18n('Retrieve or rotate the secret players need to create or join a game'), + warning: null, + allowedStatuses: 'any', + group: null, + visibility: 'enabled', + }), + + async ({ effects }) => { + const joinSecret = utils.getDefaultString({ charset: 'a-z,A-Z,0-9', len: 24 }) + await storeJson.merge(effects, { joinSecret }) + + return { + version: '1', + title: 'Join Secret', + message: + 'Share this with anyone you want to be able to create or join a game on this server — ' + + "it doesn't identify a person or a seat, just who's allowed at the lobby door. Running " + + 'this action again generates a new one and restarts the server with it; anyone already ' + + 'seated in a game keeps playing, but the old secret stops working for new games and joins.', + result: { + type: 'single', + name: 'Join Secret', + description: null, + value: joinSecret, + masked: true, + copyable: true, + qr: false, + }, + } + }, +) diff --git a/startos/actions/index.ts b/startos/actions/index.ts new file mode 100644 index 0000000..bf57d09 --- /dev/null +++ b/startos/actions/index.ts @@ -0,0 +1,4 @@ +import { sdk } from '../sdk' +import { getJoinSecret } from './getJoinSecret' + +export const actions = sdk.Actions.of().addAction(getJoinSecret) diff --git a/startos/backups.ts b/startos/backups.ts new file mode 100644 index 0000000..07e9c95 --- /dev/null +++ b/startos/backups.ts @@ -0,0 +1,8 @@ +import { sdk } from './sdk' + +// Everything worth keeping is on this one volume: every game's { engineVersion, seed, config, +// history } and turn timings (src/server/persistence.ts in the game repo), plus this package's +// own store.json (the join secret). No database, no second volume — see deployment.md §1. +export const { createBackup, restoreInit } = sdk.setupBackups(async ({ effects }) => + sdk.Backups.ofVolumes('data'), +) diff --git a/startos/dependencies.ts b/startos/dependencies.ts new file mode 100644 index 0000000..7221c4b --- /dev/null +++ b/startos/dependencies.ts @@ -0,0 +1,5 @@ +import { sdk } from './sdk' + +export const setDependencies = sdk.setupDependencies( + async ({ effects }) => ({}), +) diff --git a/startos/fileModels/store.json.ts b/startos/fileModels/store.json.ts new file mode 100644 index 0000000..758f8e4 --- /dev/null +++ b/startos/fileModels/store.json.ts @@ -0,0 +1,14 @@ +import { FileHelper, z } from '@start9labs/start-sdk' +import { sdk } from '../sdk' + +/** + * `joinSecret` is D14's server-wide secret (`docs/architecture/multiplayer.md` in the game repo): + * whoever holds it may create a game, and may join any created game that has not started. The + * server refuses to start without one (`src/server/index.ts`), so `joinSecret` is seeded on + * install (`init/generateJoinSecret.ts`) and never left unset. + */ +const shape = z.object({ + joinSecret: z.string().optional().catch(undefined), +}) + +export const storeJson = FileHelper.json({ base: sdk.volumes.data, subpath: 'store.json' }, shape) diff --git a/startos/i18n/dictionaries/default.ts b/startos/i18n/dictionaries/default.ts new file mode 100644 index 0000000..46cecd8 --- /dev/null +++ b/startos/i18n/dictionaries/default.ts @@ -0,0 +1,24 @@ +export const DEFAULT_LANG = 'en_US' + +const dict = { + // main.ts + 'Starting Station Master!': 0, + 'Multiplayer Server': 1, + 'The multiplayer server is ready': 2, + 'The multiplayer server is not ready': 3, + // interfaces.ts + 'Multiplayer Table': 4, + 'Create or join a game, and play in the browser': 5, + // actions/getJoinSecret.ts + 'Get Join Secret': 6, + 'Retrieve or rotate the secret players need to create or join a game': 7, + // init/generateJoinSecret.ts + 'Get the join secret to share with players': 8, +} as const + +/** + * Plumbing. DO NOT EDIT. + */ +export type I18nKey = keyof typeof dict +export type LangDict = Record<(typeof dict)[I18nKey], string> +export default dict diff --git a/startos/i18n/dictionaries/translations.ts b/startos/i18n/dictionaries/translations.ts new file mode 100644 index 0000000..9a47188 --- /dev/null +++ b/startos/i18n/dictionaries/translations.ts @@ -0,0 +1,48 @@ +import { LangDict } from './default' + +export default { + es_ES: { + 0: '¡Iniciando Station Master!', + 1: 'Servidor multijugador', + 2: 'El servidor multijugador está listo', + 3: 'El servidor multijugador no está listo', + 4: 'Mesa multijugador', + 5: 'Crea o únete a una partida, y juega en el navegador', + 6: 'Obtener el secreto de acceso', + 7: 'Recuperar o rotar el secreto que los jugadores necesitan para crear o unirse a una partida', + 8: 'Obtén el secreto de acceso para compartirlo con los jugadores', + }, + de_DE: { + 0: 'Starte Station Master!', + 1: 'Mehrspieler-Server', + 2: 'Der Mehrspieler-Server ist bereit', + 3: 'Der Mehrspieler-Server ist nicht bereit', + 4: 'Mehrspielertisch', + 5: 'Spiel erstellen oder beitreten und im Browser spielen', + 6: 'Beitrittsgeheimnis abrufen', + 7: 'Das Geheimnis abrufen oder erneuern, das Spieler zum Erstellen oder Beitreten einer Partie benötigen', + 8: 'Hole das Beitrittsgeheimnis, um es mit Spielern zu teilen', + }, + pl_PL: { + 0: 'Uruchamianie Station Master!', + 1: 'Serwer wieloosobowy', + 2: 'Serwer wieloosobowy jest gotowy', + 3: 'Serwer wieloosobowy nie jest gotowy', + 4: 'Stół wieloosobowy', + 5: 'Utwórz lub dołącz do gry i graj w przeglądarce', + 6: 'Pobierz sekret dołączania', + 7: 'Pobierz lub wymień sekret potrzebny graczom do tworzenia gier i dołączania do nich', + 8: 'Pobierz sekret dołączania, aby udostępnić go graczom', + }, + fr_FR: { + 0: 'Démarrage de Station Master !', + 1: 'Serveur multijoueur', + 2: 'Le serveur multijoueur est prêt', + 3: "Le serveur multijoueur n'est pas prêt", + 4: 'Table multijoueur', + 5: 'Créez ou rejoignez une partie, et jouez dans le navigateur', + 6: 'Obtenir le secret de connexion', + 7: 'Récupérer ou renouveler le secret dont les joueurs ont besoin pour créer une partie ou la rejoindre', + 8: 'Obtenez le secret de connexion à partager avec les joueurs', + }, +} satisfies Record diff --git a/startos/i18n/index.ts b/startos/i18n/index.ts new file mode 100644 index 0000000..04cea20 --- /dev/null +++ b/startos/i18n/index.ts @@ -0,0 +1,8 @@ +/** + * Plumbing. DO NOT EDIT this file. + */ +import { setupI18n } from '@start9labs/start-sdk' +import defaultDict, { DEFAULT_LANG } from './dictionaries/default' +import translations from './dictionaries/translations' + +export const i18n = setupI18n(defaultDict, translations, DEFAULT_LANG) diff --git a/startos/index.ts b/startos/index.ts new file mode 100644 index 0000000..7af589b --- /dev/null +++ b/startos/index.ts @@ -0,0 +1,11 @@ +/** + * Plumbing. DO NOT EDIT. + */ +export { createBackup } from './backups' +export { main } from './main' +export { init, uninit } from './init' +export { actions } from './actions' +import { buildManifest } from '@start9labs/start-sdk' +import { manifest as sdkManifest } from './manifest' +import { versionGraph } from './versions' +export const manifest = buildManifest(versionGraph, sdkManifest) diff --git a/startos/init/generateJoinSecret.ts b/startos/init/generateJoinSecret.ts new file mode 100644 index 0000000..7989e2d --- /dev/null +++ b/startos/init/generateJoinSecret.ts @@ -0,0 +1,26 @@ +import { utils } from '@start9labs/start-sdk' +import { getJoinSecret } from '../actions/getJoinSecret' +import { i18n } from '../i18n' +import { sdk } from '../sdk' +import { storeJson } from '../fileModels/store.json' + +/** + * The server exits immediately if `JOIN_SECRET` is unset (`src/server/index.ts` in the game + * repo), so a value has to exist before main.ts's daemon ever starts — seeded here rather than + * left for the user to generate via the action first. + */ +export const seedJoinSecret = sdk.setupOnInit(async (effects, kind) => { + if (kind === 'install') { + await storeJson.merge(effects, { + joinSecret: utils.getDefaultString({ charset: 'a-z,A-Z,0-9', len: 24 }), + }) + await sdk.action.createOwnTask(effects, getJoinSecret, 'critical', { + reason: i18n('Get the join secret to share with players'), + }) + } else { + // 'update' and 'restore' — repairs a corrupted store.json without touching an existing + // joinSecret. A restored volume already carries one (it lives inside the backed-up volume, + // see backups.ts), so there is nothing to seed. + await storeJson.merge(effects, {}) + } +}) diff --git a/startos/init/index.ts b/startos/init/index.ts new file mode 100644 index 0000000..f5f5020 --- /dev/null +++ b/startos/init/index.ts @@ -0,0 +1,18 @@ +import { sdk } from '../sdk' +import { setDependencies } from '../dependencies' +import { setInterfaces } from '../interfaces' +import { versionGraph } from '../versions' +import { actions } from '../actions' +import { restoreInit } from '../backups' +import { seedJoinSecret } from './generateJoinSecret' + +export const init = sdk.setupInit( + restoreInit, + versionGraph, + seedJoinSecret, + setInterfaces, + setDependencies, + actions, +) + +export const uninit = sdk.setupUninit(versionGraph) diff --git a/startos/interfaces.ts b/startos/interfaces.ts new file mode 100644 index 0000000..68af7e2 --- /dev/null +++ b/startos/interfaces.ts @@ -0,0 +1,28 @@ +import { i18n } from './i18n' +import { sdk } from './sdk' +import { uiPort } from './utils' + +// One interface: the server serves the browser client, the lobby/intent HTTP API, and the SSE +// game stream all from this single port (deployment.md's same-origin rule in the game repo — +// baking in a second origin would break relative URLs). type: 'ui' is a label the client is +// meant for a browser; it does not stop the same port from also carrying the API calls that +// client makes back to itself. +export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => { + const uiMulti = sdk.MultiHost.of(effects, 'ui-multi') + const uiMultiOrigin = await uiMulti.bindPort(uiPort, { protocol: 'http' }) + const ui = sdk.createInterface(effects, { + name: i18n('Multiplayer Table'), + id: 'ui', + description: i18n('Create or join a game, and play in the browser'), + type: 'ui', + masked: false, + schemeOverride: null, + username: null, + path: '', + query: {}, + }) + + const uiReceipt = await uiMultiOrigin.export([ui]) + + return [uiReceipt] +}) diff --git a/startos/main.ts b/startos/main.ts new file mode 100644 index 0000000..191d89b --- /dev/null +++ b/startos/main.ts @@ -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`. + 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: [], + }) +}) diff --git a/startos/manifest/i18n.ts b/startos/manifest/i18n.ts new file mode 100644 index 0000000..1507df4 --- /dev/null +++ b/startos/manifest/i18n.ts @@ -0,0 +1,41 @@ +export const short = { + en_US: 'Railroad operations board game — solitaire or multiplayer', + es_ES: 'Juego de mesa de operaciones ferroviarias: solitario o multijugador', + de_DE: 'Eisenbahn-Betriebs-Brettspiel — Solitär oder Mehrspieler', + pl_PL: 'Gra planszowa o zarządzaniu koleją — pasjans lub tryb wieloosobowy', + fr_FR: 'Jeu de plateau de gestion ferroviaire — solo ou multijoueur', +} + +export const long = { + en_US: + 'Station Master is a railroad operations game: switch cars, load and unload freight, and ' + + 'dispatch trains through a shared Division. This package runs the authoritative multiplayer ' + + 'server — 2 to 4 players connect from their browsers and play a competitive or cooperative ' + + 'game together. Solitaire needs no server at all; it runs entirely in the browser from the ' + + 'static client this server also hosts.', + es_ES: + 'Station Master es un juego de operaciones ferroviarias: maniobra vagones, carga y descarga ' + + 'mercancías, y despacha trenes por una División compartida. Este paquete ejecuta el servidor ' + + 'multijugador autoritativo — de 2 a 4 jugadores se conectan desde sus navegadores y juegan una ' + + 'partida competitiva o cooperativa. El modo solitario no necesita servidor: se ejecuta ' + + 'enteramente en el navegador desde el mismo cliente estático que este servidor aloja.', + de_DE: + 'Station Master ist ein Eisenbahn-Betriebsspiel: Waggons rangieren, Fracht laden und löschen ' + + 'und Züge durch eine gemeinsame Division disponieren. Dieses Paket betreibt den ' + + 'autoritativen Mehrspieler-Server — 2 bis 4 Spieler verbinden sich über den Browser und ' + + 'spielen gemeinsam kompetitiv oder kooperativ. Solitär benötigt keinen Server; es läuft ' + + 'vollständig im Browser über denselben statischen Client, den dieser Server ebenfalls hostet.', + pl_PL: + 'Station Master to gra o zarządzaniu koleją: manewruj wagonami, załaduj i rozładuj towar oraz ' + + 'dysponuj pociągami we wspólnej Dywizji. Ten pakiet uruchamia autorytatywny serwer trybu ' + + 'wieloosobowego — od 2 do 4 graczy łączy się z przeglądarek i rozgrywa partię rywalizacyjną ' + + 'lub kooperacyjną. Pasjans nie wymaga serwera — działa w całości w przeglądarce, z tego ' + + 'samego statycznego klienta, który hostuje ten serwer.', + fr_FR: + "Station Master est un jeu de gestion ferroviaire : manœuvrez des wagons, chargez et " + + "déchargez du fret, et dispatchez des trains à travers une Division partagée. Ce paquet " + + "exécute le serveur multijoueur faisant autorité — 2 à 4 joueurs se connectent depuis leur " + + "navigateur pour une partie compétitive ou coopérative. Le mode solitaire ne nécessite aucun " + + "serveur : il tourne entièrement dans le navigateur, à partir du même client statique que ce " + + "serveur héberge.", +} diff --git a/startos/manifest/index.ts b/startos/manifest/index.ts new file mode 100644 index 0000000..ce4d3e5 --- /dev/null +++ b/startos/manifest/index.ts @@ -0,0 +1,29 @@ +import { setupManifest } from '@start9labs/start-sdk' +import { long, short } from './i18n' + +export const manifest = setupManifest({ + id: 'station-master', + title: 'Station Master', + // Not open source — Jesse's own project, packaged for his own StartOS box. + license: 'UNLICENSED', + packageRepo: 'https://draco.local:53871/Jesse.Markowitz/station-master-startos', + upstreamRepo: 'https://draco.local:53871/Jesse.Markowitz/station-master', + // No separate marketing site — the repo IS where there is more to learn. + marketingUrl: 'https://draco.local:53871/Jesse.Markowitz/station-master', + donationUrl: null, + description: { short, long }, + // Everything the server persists — game.json/index.json per game, turn timings, + // and this package's own store.json (the join secret) — lives on one volume. + volumes: ['data'], + images: { + // Built from source, not a published image: `main` here is the arbitrary + // image id, matched in main.ts's SubContainer.of({ imageId: 'main' }). + // The Dockerfile COPYs the pinned `station-master` git submodule — see + // UPDATING.md for how that pin is bumped. + main: { + source: { dockerBuild: { workdir: '.', dockerfile: './Dockerfile' } }, + arch: ['x86_64', 'aarch64'], + }, + }, + dependencies: {}, +}) diff --git a/startos/sdk.ts b/startos/sdk.ts new file mode 100644 index 0000000..04ae4b1 --- /dev/null +++ b/startos/sdk.ts @@ -0,0 +1,9 @@ +import { StartSdk } from '@start9labs/start-sdk' +import { manifest } from './manifest' + +/** + * Plumbing. DO NOT EDIT. + * + * The exported "sdk" const is used throughout this package codebase. + */ +export const sdk = StartSdk.of().withManifest(manifest).build(true) diff --git a/startos/utils.ts b/startos/utils.ts new file mode 100644 index 0000000..e4d2dd4 --- /dev/null +++ b/startos/utils.ts @@ -0,0 +1,8 @@ +// Station Master's server default (src/server/index.ts's `PORT` fallback) — kept identical here +// so the env var below is documentation, not a real override. +export const uiPort = 8081 + +// Where the volume is mounted inside the subcontainer, and so also `DATA_DIR`'s value. The +// server's own per-game persistence (`games//`, `index.json`) and this package's +// store.json (the join secret) share this one directory — see README's Volume and Data Layout. +export const dataDir = '/data' diff --git a/startos/versions/current.ts b/startos/versions/current.ts new file mode 100644 index 0000000..c1f06f8 --- /dev/null +++ b/startos/versions/current.ts @@ -0,0 +1,16 @@ +import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk' + +export const current = VersionInfo.of({ + version: '1.0.0:0', + releaseNotes: { + en_US: 'Initial StartOS package, bundling Station Master v0.5.1.', + es_ES: 'Paquete inicial para StartOS, con Station Master v0.5.1.', + de_DE: 'Erstes StartOS-Paket, mit Station Master v0.5.1.', + pl_PL: 'Pierwszy pakiet dla StartOS, zawiera Station Master v0.5.1.', + fr_FR: 'Premier paquet StartOS, avec Station Master v0.5.1.', + }, + migrations: { + up: async ({ effects }) => {}, + down: IMPOSSIBLE, + }, +}) diff --git a/startos/versions/index.ts b/startos/versions/index.ts new file mode 100644 index 0000000..e596b0c --- /dev/null +++ b/startos/versions/index.ts @@ -0,0 +1,7 @@ +import { VersionGraph } from '@start9labs/start-sdk' +import { current } from './current' + +export const versionGraph = VersionGraph.of({ + current, + other: [], +}) diff --git a/station-master b/station-master new file mode 160000 index 0000000..e76bd77 --- /dev/null +++ b/station-master @@ -0,0 +1 @@ +Subproject commit e76bd77099cf8427eb21b257daa5ca0329139c5f diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f032dc1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@start9labs/start-sdk/tsconfig.base.json", + "include": ["startos/**/*.ts", "node_modules/**/startos"] +}