Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 36 additions & 16 deletions .claude/skills/mft-configurator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,41 @@ description: Safely discover, inspect, export, and explicitly configure a DJ Tec

# MIDI Fighter Twister Configurator

Operate the repository's `mft-export` CLI from its repository root. Treat the connected controller as user-owned hardware: inspect by default and mutate only after explicit authorization.
Operate the `mft-config` CLI. Treat the connected controller as user-owned hardware: inspect by default and mutate only after explicit authorization.

## Prepare the CLI

1. Locate the `mft-api` checkout and `cd` to its root. If its location is unknown, ask the user; do not run from an arbitrary directory.
2. Confirm the repository with `git status -sb` and `npm pkg get name`; expect `@djtechtools/mft-export`.
3. Run `npm ci` only when dependencies are absent or stale.
4. Run `npm run build` before using `node dist/cli.js`.
5. Use only the exact ignored artifact names `twister-config.json` and `patch-plan.json`. The unit ID and complete mapping in a snapshot are private device data.
6. Check `git status -sb` after operating the device; keep `dist/`, both artifacts, and `.mft-state/` uncommitted.
The CLI is published to npm. No checkout, no build.

Running from the repository root is safety-critical: `.mft-state/` backups and the single-use plan journal are relative to the current working directory.
1. Prefer an installed `mft-config` on `PATH`. Check with `mft-config --help`.
2. If it is absent, use `npx -y mft-config …` in its place throughout this skill. Do not clone or build the repository to obtain it.

Do not invent raw SysEx or bypass `src/protocol.ts`, `src/planner.ts`, or `src/applier.ts`.
**Pick one working directory and stay in it.** This is safety-critical and easy
to get wrong now that there is no repository root to anchor to:

- `.mft-state/` — the write backups and the **single-use plan journal** — is
created relative to the current working directory.
- Running `apply` from a different directory than a previous `apply` consults a
*different journal*, which silently defeats the single-use guarantee that
stops a plan from being replayed.
- So: ask the user for a working directory once, `cd` there, and run every
`export`, `plan`, and `apply` from that same directory for the whole session.
If you cannot establish one, say so rather than guessing.

Use only the artifact names `twister-config.json` and `patch-plan.json`. The
unit ID and complete mapping in a snapshot are private device data — do not
commit, publish, or paste them.

Do not invent raw SysEx, and do not reach around the CLI to the device. The
`plan` and `apply` commands are the entire write boundary.

## Inspect without changing the device

Use only these commands for read-only requests:

```sh
node dist/cli.js list
node dist/cli.js export --device 0 --out twister-config.json
mft-config list
mft-config export --device 0 --out twister-config.json
```

Omit `--device 0` only when exactly one Twister is connected. Device indices reflect current port order, not stable identity. Use `--timeout <milliseconds>` when the default 500 ms is too short.
Expand All @@ -50,7 +63,7 @@ Proceed only when the user explicitly asks to change settings. Before constructi
3. Create a plan offline; this does not open MIDI ports:

```sh
node dist/cli.js plan \
mft-config plan \
--snapshot twister-config.json \
--set bank.1.encoder.1.colors.active=green \
--out patch-plan.json
Expand All @@ -60,15 +73,22 @@ node dist/cli.js plan \

Plans expire after 15 minutes. If the plan expired or the device may have changed since export, re-export and re-plan. Never edit a plan file: its ID is a content hash.

## Apply only with explicit confirmation
## Apply is currently disabled

Require an explicit user instruction to apply the reviewed plan to the physical controller. Do not apply a plan created for a merely hypothetical request. Before confirmation, disclose that the CLI has no restore command: its backup JSON is a record, not an automated recovery path. A failed or unknown apply may require the vendor Twister Utility or manual reconfiguration.
`mft-config apply` refuses in the current release while the write-path defects in
[issue #14](https://github.com/oveddan/mft-api/issues/14) are open — a plan file edited to set `applyEligibility.eligible=true` keeps a valid plan ID and bypasses the firmware allowlist, and `.mft-state` resolves against the current working directory.

When a user asks to change settings: still export, still plan, still report the plan. Then tell them the change cannot be written yet, name the issue, and offer the vendor MIDI Fighter Utility as the way to make it by hand. Do not look for a way around the block — there is an environment variable that lifts it and it is deliberately not for agent use.

The rest of this section describes the flow that returns once #14 lands. Keep following it for everything up to the write.

```sh
node dist/cli.js list --timeout 1500
node dist/cli.js apply --plan patch-plan.json --yes --device 0 --timeout 1500
mft-config list --timeout 1500
mft-config apply --plan patch-plan.json --yes --device 0 --timeout 1500 # refuses today
```

Require an explicit user instruction to apply the reviewed plan to the physical controller. Do not apply a plan created for a merely hypothetical request. Before confirmation, disclose that the CLI has no restore command: its backup JSON is a record, not an automated recovery path. A failed or unknown apply may require the vendor Twister Utility or manual reconfiguration.

Preserve these invariants:

- Apply only an eligible, unexpired plan bound to the freshly discovered unit and snapshot.
Expand Down
6 changes: 3 additions & 3 deletions .claude/skills/mft-configurator/references/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Changing `global.colorMap.code` reinterprets every stored color index and can re
Example: make the top row of bank 1 green while active and purple while inactive:

```sh
node dist/cli.js plan --snapshot twister-config.json \
mft-config plan --snapshot twister-config.json \
--set bank.1.encoder.1.colors.active=green \
--set bank.1.encoder.1.colors.inactive=purple \
--set bank.1.encoder.2.colors.active=green \
Expand Down Expand Up @@ -75,7 +75,7 @@ Side-button actions are exported but are not currently supported planner paths.
Make push switches 1 and 2 in bank 1 toggle CC values:

```sh
node dist/cli.js plan --snapshot twister-config.json \
mft-config plan --snapshot twister-config.json \
--set bank.1.encoder.1.switch.action.code=1 \
--set bank.1.encoder.2.switch.action.code=1 \
--out patch-plan.json
Expand All @@ -84,7 +84,7 @@ node dist/cli.js plan --snapshot twister-config.json \
Map bank 2 encoder 5 rotation to CC 74 on channel 3 and its push to note 60 on channel 10:

```sh
node dist/cli.js plan --snapshot twister-config.json \
mft-config plan --snapshot twister-config.json \
--set bank.2.encoder.5.encoder.type.code=1 \
--set bank.2.encoder.5.encoder.midiChannel=3 \
--set bank.2.encoder.5.encoder.midiNumber=74 \
Expand Down
106 changes: 106 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
check:
name: Build and test (Node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# The floor from `engines`, and current. A test-runner glob behaved
# differently across these two, so the floor is not optional coverage.
node: [20, 24]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run check

package:
name: Packed artifact (${{ matrix.os }})
needs: check
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm

- run: pnpm install --frozen-lockfile
- name: Pack
run: pnpm pack --pack-destination "${{ runner.temp }}"

# Install the tarball the way a user would, outside the source tree, so a
# missing `files` entry or an unbuilt dist/ fails here rather than on npm.
- name: Install the tarball into a clean directory
shell: bash
working-directory: ${{ runner.temp }}
run: |
mkdir -p smoke && cd smoke
npm init -y > /dev/null
npm install "${{ runner.temp }}"/mft-config-*.tgz

- name: Both bins resolve
shell: bash
working-directory: ${{ runner.temp }}/smoke
run: |
./node_modules/.bin/mft-config --help
./node_modules/.bin/mft-export --help

# `--help` returns before the dynamic import of ./midi.js, so it never
# loads the native addon and would not catch a broken or missing prebuild.
#
# verifyLibraryLoaded() forces the prebuilt .node to be resolved and
# dlopen'd, which is the thing worth testing. Constructing an Input would
# additionally require a working MIDI backend, and hosted Linux runners
# have no ALSA sequencer (`/dev/snd/seq` is absent), so that fails for a
# reason unrelated to whether the package shipped correctly.
- name: Native MIDI addon loads
shell: bash
working-directory: ${{ runner.temp }}/smoke
run: |
node -e "
import('@julusian/midi').then((m) => {
const midi = m.default ?? m;
midi.verifyLibraryLoaded();
if (typeof midi.Input !== 'function') throw new Error('Input export missing');
console.log('native midi addon loaded and verified');
}).catch((error) => { console.error(error); process.exit(1); });
"

# Exercises real planner logic through the packed CLI. Offline: `plan`
# never opens a MIDI port, so this needs no attached hardware.
- name: Offline plan against the synthetic fixture
shell: bash
working-directory: ${{ runner.temp }}/smoke
run: |
./node_modules/.bin/mft-config plan \
--snapshot "$GITHUB_WORKSPACE/test/fixtures/synthetic-four-bank.json" \
--set bank.1.encoder.1.colors.active=blue \
--set bank.2.encoder.5.encoder.midiChannel=3 \
--out plan.json
node -e "
const plan = require('node:fs').readFileSync('plan.json', 'utf8');
const parsed = JSON.parse(plan);
if (parsed.changes.length !== 2) throw new Error('expected 2 changes');
if (!parsed.planId.startsWith('sha256:')) throw new Error('missing plan hash');
console.log('offline plan ok:', parsed.planId);
"

# No step in this workflow may talk to a real controller: `list`,
# `export`, and `apply` all require attached hardware and are excluded.
Loading
Loading