Port phase 1 to TypeScript

Move the standalone GameSense scale lighting to TypeScript ahead of the
Max for Live work, so step 3 gets type declarations at the seam.

Build emits CommonJS at ES2020 into dist/, because Node for Max loads CJS
on a Node version we do not control. bin/apex7-scale.js becomes a plain-JS
launcher so `node bin/apex7-scale.js` keeps working; the CLI itself moves
to src/cli.ts.

Two modules make previously implicit structure explicit:

  - src/protocol.ts, the GameSense wire types, so the transport and the
    payload construction agree on shapes neither of them owns
  - src/errors.ts, since `catch (err)` binds `unknown` under strict

Typing surfaced a few real fixes:

  - ScaleLighting.started was a public field the CLI set by hand to make
    --off work without start(); that is now release(), with started
    private behind isStarted
  - readAddress() trusted JSON.parse output; the address is now checked
    for being a non-empty string
  - res.statusCode is number|undefined, so the old `>= 200` comparison
    coerced silently; a missing status now rejects
  - parseIntervals claimed to dedupe and sort, and never did (comment
    corrected; pitchClassesFor is what dedupes)

Behavior is otherwise unchanged: all 18 tests pass and --once, --off,
--demo, --key-test, the resident stdin loop and the error paths were
verified against tools/fake-gamesense.ts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-08-12 13:30:53 +00:00
parent 425eb86327
commit 3b02612461
18 changed files with 1550 additions and 837 deletions

View File

@@ -7,14 +7,18 @@ This is **build step 1** of [`apex7-ableton-scale-lighting.md`](./apex7-ableton-
a standalone script that talks to GameSense directly. Live/Max for Live is not
involved yet — you pass the scale on the command line.
No dependencies. Node 14+.
TypeScript, no runtime dependencies. Node 16+.
## Quick start (Windows, with SteelSeries GG running)
```powershell
npm install # also compiles src\*.ts to dist\
node bin\apex7-scale.js --root C --scale major
```
`npm install` builds via the `prepare` script; after editing any `.ts` file run
`npm run build` (or `npm run typecheck` for types only).
The board dims, the C-major keys light up (`A S D F G H J K`), the root is
orange. The process stays resident and heartbeats so the lighting sticks; you
can type new scales at the prompt:
@@ -78,12 +82,12 @@ Two design notes:
## Testing without hardware
`tools/fake-gamesense.js` stands in for the GameSense server: it accepts the
`tools/fake-gamesense.ts` stands in for the GameSense server: it accepts the
real endpoints, resolves the bound handlers against each frame, and renders the
resulting keyboard as ANSI color in the terminal.
```bash
node tools/fake-gamesense.js --port 51000 # terminal 1
npm run stub -- --port 51000 # terminal 1
node bin/apex7-scale.js --address 127.0.0.1:51000 --root F# --scale dorian # terminal 2
```
@@ -99,22 +103,31 @@ npm test
## Layout
```
bin/apex7-scale.js CLI
src/hid.js USB HID usage codes; the full-board key list
src/scale.js scale presets, root/interval parsing, pitch class -> QWERTY key
src/gamesense.js GameSense REST client (coreProps discovery, heartbeat, cleanup)
src/lighting.js handler + frame construction
src/index.js ScaleLighting — the API step 3 will call from Node for Max
tools/fake-gamesense.js terminal simulator of the GameSense server
test/logic.test.js unit tests
bin/apex7-scale.js launcher (plain JS) — runs dist/src/cli.js
src/cli.ts argument parsing, output, the resident stdin loop
src/protocol.ts GameSense wire types (Rgb, Frame, Handler, …)
src/hid.ts USB HID usage codes; the full-board key list
src/scale.ts scale presets, root/interval parsing, pitch class -> QWERTY key
src/gamesense.ts GameSense REST client (coreProps discovery, heartbeat, cleanup)
src/lighting.ts handler + frame construction
src/errors.ts `catch (err: unknown)` helpers
src/index.ts ScaleLighting — the API step 3 will call from Node for Max
tools/fake-gamesense.ts terminal simulator of the GameSense server
test/logic.test.ts unit tests
dist/ compiled CommonJS + .d.ts (gitignored)
```
`tsconfig.json` emits CommonJS at ES2020, because Node for Max loads CJS on a
Node version we do not control.
## Reusing this from Max for Live (step 3)
`src/index.js` is the seam — no CLI concerns in it:
`src/index.ts` is the seam — no CLI concerns in it, and it ships type
declarations alongside the compiled JS:
```js
const { ScaleLighting } = require('./src');
```ts
import { ScaleLighting } from 'steelseries-live-scale';
// or, from a Node for Max script: require('<repo>/dist/src')
const lights = new ScaleLighting();
await lights.start(); // register + bind + heartbeat
@@ -135,5 +148,6 @@ as `root_note` and `scale_intervals`.
holds an exclusive lighting profile.
- **Lighting reverts after ~15s** — that's GameSense's deactivation timeout;
it only happens with `--once`, since resident mode heartbeats every 5s.
- **A key stays dark** — it may not be in `src/hid.js`. Add its HID code there;
- **A key stays dark** — it may not be in `src/hid.ts`. Add its HID code there;
keys not addressed by any handler go black in GameSense mode.
- **`apex7-scale is not built yet`** — run `npm run build`.