mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-10 20:27:03 +01:00
pick and pick-manual no longer uninstall games that are already allowed: the outgoing assignment is kept as a released pick (installed, no lock) until 'pick --replace', done/check, or abandon-pick. The allowed set is capped at max_manual_picks + 1; a pick over it is refused before the candidate list / confirmation, naming the games. status and the web Picks card list kept games with an Abandon button. Fast install now tracks the installer log from the spawn offset: web jobs drive the progress bar (per-depot %, phase) and echo a line every 30 s; daemon-started runs are exposed via ctl status / /api/daemon and shown on the Dashboard. Also: web deps bumped for dependency freshness (react-query 5.104.5, typescript-eslint 8.72.0, vite 8.3.5, node 24.22.1 in CI). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CUaX4zAa6H4sMu3jd6RFdb
82 lines
3.3 KiB
TypeScript
82 lines
3.3 KiB
TypeScript
// Small HTTP helpers shared by the mock route handlers.
|
|
|
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
import type { CommandSpec, ParamSpec } from '../src/api/contract.ts'
|
|
import type { ApiErrorCode } from '../src/api/jobContract.ts'
|
|
import { nameOf } from './games.ts'
|
|
import { gameName, world } from './world.ts'
|
|
|
|
export class HttpError extends Error {
|
|
readonly status: number
|
|
readonly code: ApiErrorCode
|
|
constructor(status: number, code: ApiErrorCode, message: string) {
|
|
super(message)
|
|
this.status = status
|
|
this.code = code
|
|
}
|
|
}
|
|
|
|
export function send(res: ServerResponse, status: number, body?: unknown): void {
|
|
res.statusCode = status
|
|
if (body === undefined) {
|
|
res.end()
|
|
return
|
|
}
|
|
res.setHeader('Content-Type', 'application/json')
|
|
res.end(JSON.stringify(body))
|
|
}
|
|
|
|
export async function readJson(req: IncomingMessage): Promise<Record<string, unknown>> {
|
|
const chunks: Buffer[] = []
|
|
for await (const chunk of req) chunks.push(chunk as Buffer)
|
|
const text = Buffer.concat(chunks).toString('utf8')
|
|
if (!text) return {}
|
|
try {
|
|
return JSON.parse(text) as Record<string, unknown>
|
|
} catch {
|
|
throw new HttpError(400, 'invalid_params', 'Body is not valid JSON.')
|
|
}
|
|
}
|
|
|
|
/** Mirror of `_friction.phrase_matches`: only surrounding whitespace is forgiven. */
|
|
export function phraseMatches(expected: string, typed: unknown): boolean {
|
|
return typeof typed === 'string' && typed.trim() === expected
|
|
}
|
|
|
|
export function fillPhrase(template: string, params: Record<string, number | string>, extra: Record<string, string> = {}): string {
|
|
const fields: Record<string, string> = {
|
|
count: String(world.installed.filter((g) => !g.assigned && !g.protected).length),
|
|
...Object.fromEntries(Object.entries(params).map(([k, v]) => [k, String(v)])),
|
|
...extra,
|
|
}
|
|
if (params.app_id !== undefined) fields.game_name = gameName(Number(params.app_id))
|
|
return template.replace(/\{(\w+)\}/g, (_, key: string) => fields[key] ?? `{${key}}`)
|
|
}
|
|
|
|
function checkParam(p: ParamSpec, raw: unknown): number | string | undefined {
|
|
if (raw === undefined || raw === '') {
|
|
if (p.required && p.default === undefined) throw new HttpError(400, 'invalid_params', `${p.label} is required.`)
|
|
return p.default
|
|
}
|
|
if (p.type === 'string') return String(raw)
|
|
const n = Number(raw)
|
|
if (p.type === 'bool' && n !== 0 && n !== 1) throw new HttpError(400, 'invalid_params', `${p.label} must be true or false.`)
|
|
if (!Number.isInteger(n)) throw new HttpError(400, 'invalid_params', `${p.label} must be a whole number.`)
|
|
if (p.min !== undefined && n < p.min) throw new HttpError(400, 'invalid_params', `${p.label} must be at least ${p.min}.`)
|
|
if (p.max !== undefined && n > p.max) throw new HttpError(400, 'invalid_params', `${p.label} must be at most ${p.max}.`)
|
|
if (p.type === 'app_id' && nameOf(n) === null) {
|
|
throw new HttpError(400, 'invalid_params', `App ${n} is not in your library.`)
|
|
}
|
|
return n
|
|
}
|
|
|
|
export function validateParams(spec: CommandSpec, raw: unknown): Record<string, number | string> {
|
|
const input = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>
|
|
const out: Record<string, number | string> = {}
|
|
for (const p of spec.params) {
|
|
const v = checkParam(p, input[p.name])
|
|
if (v !== undefined) out[p.name] = v
|
|
}
|
|
return out
|
|
}
|